Spaces:
Runtime error
Runtime error
Merge pull request #83 from EnvisionMindCa/codex/create-cross-platform-cli-app-for-api
Browse files- cli-go/README.md +33 -0
- cli-go/cmd/cat.go +27 -0
- cli-go/cmd/chat.go +83 -0
- cli-go/cmd/ls.go +37 -0
- cli-go/cmd/rm.go +21 -0
- cli-go/cmd/root.go +39 -0
- cli-go/cmd/upload.go +28 -0
- cli-go/cmd/write.go +26 -0
- cli-go/go.mod +16 -0
- cli-go/go.sum +21 -0
- cli-go/internal/client/client.go +218 -0
- cli-go/main.go +7 -0
cli-go/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Go CLI for LLM Backend
|
| 2 |
+
|
| 3 |
+
This folder contains a cross-platform command line client written in Go. The CLI can be built as a single executable for Windows, Linux and macOS.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- Interactive colour-coded chat sessions
|
| 8 |
+
- Upload documents to the API
|
| 9 |
+
- List, read, write and delete files in the VM
|
| 10 |
+
|
| 11 |
+
## Building
|
| 12 |
+
|
| 13 |
+
Install Go 1.20 or later and run:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
cd cli-go
|
| 17 |
+
go build -o llmcli
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
For other platforms pass `GOOS` and `GOARCH` environment variables:
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
GOOS=windows GOARCH=amd64 go build -o llmcli.exe
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## Usage
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
./llmcli --user yourname --server http://localhost:8000 chat
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Use `--help` on any subcommand for details.
|
| 33 |
+
|
cli-go/cmd/cat.go
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
|
| 7 |
+
"github.com/spf13/cobra"
|
| 8 |
+
|
| 9 |
+
"llm-cli/internal/client"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func newCatCmd() *cobra.Command {
|
| 13 |
+
return &cobra.Command{
|
| 14 |
+
Use: "cat <path>",
|
| 15 |
+
Short: "Print a file from the VM",
|
| 16 |
+
Args: cobra.ExactArgs(1),
|
| 17 |
+
RunE: func(cmd *cobra.Command, args []string) error {
|
| 18 |
+
c := client.New(server)
|
| 19 |
+
content, err := c.ReadFile(context.Background(), user, args[0])
|
| 20 |
+
if err != nil {
|
| 21 |
+
return err
|
| 22 |
+
}
|
| 23 |
+
fmt.Print(content)
|
| 24 |
+
return nil
|
| 25 |
+
},
|
| 26 |
+
}
|
| 27 |
+
}
|
cli-go/cmd/chat.go
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"fmt"
|
| 6 |
+
"os"
|
| 7 |
+
"strconv"
|
| 8 |
+
|
| 9 |
+
"github.com/fatih/color"
|
| 10 |
+
"github.com/spf13/cobra"
|
| 11 |
+
|
| 12 |
+
"llm-cli/internal/client"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
func newChatCmd() *cobra.Command {
|
| 16 |
+
cmd := &cobra.Command{
|
| 17 |
+
Use: "chat",
|
| 18 |
+
Short: "Start an interactive chat session",
|
| 19 |
+
RunE: runChat,
|
| 20 |
+
}
|
| 21 |
+
return cmd
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func runChat(cmd *cobra.Command, args []string) error {
|
| 25 |
+
ctx := cmd.Context()
|
| 26 |
+
c := client.New(server)
|
| 27 |
+
|
| 28 |
+
sessions, err := c.ListSessions(ctx, user)
|
| 29 |
+
if err != nil {
|
| 30 |
+
return err
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
session := "default"
|
| 34 |
+
if len(sessions) > 0 {
|
| 35 |
+
fmt.Println("Existing sessions:")
|
| 36 |
+
for i, s := range sessions {
|
| 37 |
+
fmt.Printf(" %d. %s\n", i+1, s)
|
| 38 |
+
}
|
| 39 |
+
fmt.Printf("Select session number or enter new name [%d]: ", len(sessions))
|
| 40 |
+
var choice string
|
| 41 |
+
fmt.Scanln(&choice)
|
| 42 |
+
if n, err := strconv.Atoi(choice); err == nil && n >= 1 && n <= len(sessions) {
|
| 43 |
+
session = sessions[n-1]
|
| 44 |
+
} else if choice != "" {
|
| 45 |
+
session = choice
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
cyan := color.New(color.FgCyan).SprintFunc()
|
| 50 |
+
green := color.New(color.FgGreen).SprintFunc()
|
| 51 |
+
yellow := color.New(color.FgYellow).SprintFunc()
|
| 52 |
+
|
| 53 |
+
fmt.Printf("Chatting as %s in session '%s'\n", green(user), session)
|
| 54 |
+
|
| 55 |
+
scanner := bufio.NewScanner(os.Stdin)
|
| 56 |
+
for {
|
| 57 |
+
fmt.Printf("%s> ", cyan("You"))
|
| 58 |
+
if !scanner.Scan() {
|
| 59 |
+
break
|
| 60 |
+
}
|
| 61 |
+
line := scanner.Text()
|
| 62 |
+
if line == "exit" || line == "quit" {
|
| 63 |
+
break
|
| 64 |
+
}
|
| 65 |
+
stream, err := c.ChatStream(ctx, user, session, line)
|
| 66 |
+
if err != nil {
|
| 67 |
+
fmt.Println("error:", err)
|
| 68 |
+
continue
|
| 69 |
+
}
|
| 70 |
+
r := bufio.NewReader(stream)
|
| 71 |
+
for {
|
| 72 |
+
part, err := r.ReadString('\n')
|
| 73 |
+
if len(part) > 0 {
|
| 74 |
+
fmt.Print(yellow(part))
|
| 75 |
+
}
|
| 76 |
+
if err != nil {
|
| 77 |
+
break
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
stream.Close()
|
| 81 |
+
}
|
| 82 |
+
return nil
|
| 83 |
+
}
|
cli-go/cmd/ls.go
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
|
| 7 |
+
"github.com/spf13/cobra"
|
| 8 |
+
|
| 9 |
+
"llm-cli/internal/client"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func newLsCmd() *cobra.Command {
|
| 13 |
+
return &cobra.Command{
|
| 14 |
+
Use: "ls [path]",
|
| 15 |
+
Short: "List directory contents in the VM",
|
| 16 |
+
Args: cobra.MaximumNArgs(1),
|
| 17 |
+
RunE: func(cmd *cobra.Command, args []string) error {
|
| 18 |
+
path := "/data"
|
| 19 |
+
if len(args) == 1 {
|
| 20 |
+
path = args[0]
|
| 21 |
+
}
|
| 22 |
+
c := client.New(server)
|
| 23 |
+
entries, err := c.ListDir(context.Background(), user, path)
|
| 24 |
+
if err != nil {
|
| 25 |
+
return err
|
| 26 |
+
}
|
| 27 |
+
for _, e := range entries {
|
| 28 |
+
if e.IsDir {
|
| 29 |
+
fmt.Println(e.Name + "/")
|
| 30 |
+
} else {
|
| 31 |
+
fmt.Println(e.Name)
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
return nil
|
| 35 |
+
},
|
| 36 |
+
}
|
| 37 |
+
}
|
cli-go/cmd/rm.go
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
|
| 6 |
+
"github.com/spf13/cobra"
|
| 7 |
+
|
| 8 |
+
"llm-cli/internal/client"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func newRmCmd() *cobra.Command {
|
| 12 |
+
return &cobra.Command{
|
| 13 |
+
Use: "rm <path>",
|
| 14 |
+
Short: "Remove a file or directory in the VM",
|
| 15 |
+
Args: cobra.ExactArgs(1),
|
| 16 |
+
RunE: func(cmd *cobra.Command, args []string) error {
|
| 17 |
+
c := client.New(server)
|
| 18 |
+
return c.DeleteFile(context.Background(), user, args[0])
|
| 19 |
+
},
|
| 20 |
+
}
|
| 21 |
+
}
|
cli-go/cmd/root.go
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
|
| 7 |
+
"github.com/spf13/cobra"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
var (
|
| 11 |
+
server string
|
| 12 |
+
user string
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
func NewRootCmd() *cobra.Command {
|
| 16 |
+
cmd := &cobra.Command{
|
| 17 |
+
Use: "llmcli",
|
| 18 |
+
Short: "CLI client for the LLM backend",
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
cmd.PersistentFlags().StringVarP(&server, "server", "s", "http://localhost:8000", "API server URL")
|
| 22 |
+
cmd.PersistentFlags().StringVarP(&user, "user", "u", "default", "User name")
|
| 23 |
+
|
| 24 |
+
cmd.AddCommand(newChatCmd())
|
| 25 |
+
cmd.AddCommand(newUploadCmd())
|
| 26 |
+
cmd.AddCommand(newLsCmd())
|
| 27 |
+
cmd.AddCommand(newCatCmd())
|
| 28 |
+
cmd.AddCommand(newWriteCmd())
|
| 29 |
+
cmd.AddCommand(newRmCmd())
|
| 30 |
+
|
| 31 |
+
return cmd
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func Execute() {
|
| 35 |
+
if err := NewRootCmd().Execute(); err != nil {
|
| 36 |
+
fmt.Fprintln(os.Stderr, err)
|
| 37 |
+
os.Exit(1)
|
| 38 |
+
}
|
| 39 |
+
}
|
cli-go/cmd/upload.go
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
|
| 7 |
+
"github.com/spf13/cobra"
|
| 8 |
+
|
| 9 |
+
"llm-cli/internal/client"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func newUploadCmd() *cobra.Command {
|
| 13 |
+
return &cobra.Command{
|
| 14 |
+
Use: "upload [file]",
|
| 15 |
+
Short: "Upload a document to the VM",
|
| 16 |
+
Args: cobra.ExactArgs(1),
|
| 17 |
+
RunE: func(cmd *cobra.Command, args []string) error {
|
| 18 |
+
ctx := context.Background()
|
| 19 |
+
c := client.New(server)
|
| 20 |
+
path, err := c.UploadDocument(ctx, user, "default", args[0])
|
| 21 |
+
if err != nil {
|
| 22 |
+
return err
|
| 23 |
+
}
|
| 24 |
+
fmt.Println("Uploaded to", path)
|
| 25 |
+
return nil
|
| 26 |
+
},
|
| 27 |
+
}
|
| 28 |
+
}
|
cli-go/cmd/write.go
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"os"
|
| 6 |
+
|
| 7 |
+
"github.com/spf13/cobra"
|
| 8 |
+
|
| 9 |
+
"llm-cli/internal/client"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func newWriteCmd() *cobra.Command {
|
| 13 |
+
return &cobra.Command{
|
| 14 |
+
Use: "write <path> <file>",
|
| 15 |
+
Short: "Write a local file to a path in the VM",
|
| 16 |
+
Args: cobra.ExactArgs(2),
|
| 17 |
+
RunE: func(cmd *cobra.Command, args []string) error {
|
| 18 |
+
data, err := os.ReadFile(args[1])
|
| 19 |
+
if err != nil {
|
| 20 |
+
return err
|
| 21 |
+
}
|
| 22 |
+
c := client.New(server)
|
| 23 |
+
return c.WriteFile(context.Background(), user, args[0], string(data))
|
| 24 |
+
},
|
| 25 |
+
}
|
| 26 |
+
}
|
cli-go/go.mod
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module llm-cli
|
| 2 |
+
|
| 3 |
+
go 1.23.8
|
| 4 |
+
|
| 5 |
+
require (
|
| 6 |
+
github.com/fatih/color v1.16.0
|
| 7 |
+
github.com/spf13/cobra v1.7.0
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
require (
|
| 11 |
+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
| 12 |
+
github.com/mattn/go-colorable v0.1.13 // indirect
|
| 13 |
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
| 14 |
+
github.com/spf13/pflag v1.0.5 // indirect
|
| 15 |
+
golang.org/x/sys v0.14.0 // indirect
|
| 16 |
+
)
|
cli-go/go.sum
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
| 2 |
+
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
| 3 |
+
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
| 4 |
+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
| 5 |
+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
| 6 |
+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
| 7 |
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
| 8 |
+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
| 9 |
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
| 10 |
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 11 |
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
| 12 |
+
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
| 13 |
+
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
| 14 |
+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
| 15 |
+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
| 16 |
+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 17 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 18 |
+
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
| 19 |
+
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
| 20 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 21 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
cli-go/internal/client/client.go
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package client
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"mime/multipart"
|
| 10 |
+
"net/http"
|
| 11 |
+
"os"
|
| 12 |
+
"path/filepath"
|
| 13 |
+
"time"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
type Client struct {
|
| 17 |
+
baseURL string
|
| 18 |
+
httpClient *http.Client
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
func New(baseURL string) *Client {
|
| 22 |
+
return &Client{
|
| 23 |
+
baseURL: baseURL,
|
| 24 |
+
httpClient: &http.Client{Timeout: 30 * time.Second},
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
type SessionInfo struct {
|
| 29 |
+
Sessions []string `json:"sessions"`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func (c *Client) ListSessions(ctx context.Context, user string) ([]string, error) {
|
| 33 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
| 34 |
+
fmt.Sprintf("%s/sessions/%s", c.baseURL, user), nil)
|
| 35 |
+
if err != nil {
|
| 36 |
+
return nil, err
|
| 37 |
+
}
|
| 38 |
+
resp, err := c.httpClient.Do(req)
|
| 39 |
+
if err != nil {
|
| 40 |
+
return nil, err
|
| 41 |
+
}
|
| 42 |
+
defer resp.Body.Close()
|
| 43 |
+
if resp.StatusCode != http.StatusOK {
|
| 44 |
+
return nil, fmt.Errorf("list sessions failed: %s", resp.Status)
|
| 45 |
+
}
|
| 46 |
+
var data SessionInfo
|
| 47 |
+
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
| 48 |
+
return nil, err
|
| 49 |
+
}
|
| 50 |
+
return data.Sessions, nil
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
type ChatRequest struct {
|
| 54 |
+
User string `json:"user"`
|
| 55 |
+
Session string `json:"session"`
|
| 56 |
+
Prompt string `json:"prompt"`
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func (c *Client) ChatStream(ctx context.Context, user, session, prompt string) (io.ReadCloser, error) {
|
| 60 |
+
body, err := json.Marshal(ChatRequest{User: user, Session: session, Prompt: prompt})
|
| 61 |
+
if err != nil {
|
| 62 |
+
return nil, err
|
| 63 |
+
}
|
| 64 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/chat/stream", bytes.NewReader(body))
|
| 65 |
+
if err != nil {
|
| 66 |
+
return nil, err
|
| 67 |
+
}
|
| 68 |
+
req.Header.Set("Content-Type", "application/json")
|
| 69 |
+
resp, err := c.httpClient.Do(req)
|
| 70 |
+
if err != nil {
|
| 71 |
+
return nil, err
|
| 72 |
+
}
|
| 73 |
+
if resp.StatusCode != http.StatusOK {
|
| 74 |
+
defer resp.Body.Close()
|
| 75 |
+
b, _ := io.ReadAll(resp.Body)
|
| 76 |
+
return nil, fmt.Errorf("chat failed: %s - %s", resp.Status, string(b))
|
| 77 |
+
}
|
| 78 |
+
return resp.Body, nil
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
type UploadResp struct {
|
| 82 |
+
Path string `json:"path"`
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
func (c *Client) UploadDocument(ctx context.Context, user, session, path string) (string, error) {
|
| 86 |
+
file, err := os.Open(path)
|
| 87 |
+
if err != nil {
|
| 88 |
+
return "", err
|
| 89 |
+
}
|
| 90 |
+
defer file.Close()
|
| 91 |
+
|
| 92 |
+
buf := &bytes.Buffer{}
|
| 93 |
+
writer := multipart.NewWriter(buf)
|
| 94 |
+
_ = writer.WriteField("user", user)
|
| 95 |
+
_ = writer.WriteField("session", session)
|
| 96 |
+
fw, err := writer.CreateFormFile("file", filepath.Base(path))
|
| 97 |
+
if err != nil {
|
| 98 |
+
return "", err
|
| 99 |
+
}
|
| 100 |
+
if _, err = io.Copy(fw, file); err != nil {
|
| 101 |
+
return "", err
|
| 102 |
+
}
|
| 103 |
+
writer.Close()
|
| 104 |
+
|
| 105 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/upload", buf)
|
| 106 |
+
if err != nil {
|
| 107 |
+
return "", err
|
| 108 |
+
}
|
| 109 |
+
req.Header.Set("Content-Type", writer.FormDataContentType())
|
| 110 |
+
|
| 111 |
+
resp, err := c.httpClient.Do(req)
|
| 112 |
+
if err != nil {
|
| 113 |
+
return "", err
|
| 114 |
+
}
|
| 115 |
+
defer resp.Body.Close()
|
| 116 |
+
if resp.StatusCode != http.StatusOK {
|
| 117 |
+
b, _ := io.ReadAll(resp.Body)
|
| 118 |
+
return "", fmt.Errorf("upload failed: %s - %s", resp.Status, string(b))
|
| 119 |
+
}
|
| 120 |
+
var out UploadResp
|
| 121 |
+
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
| 122 |
+
return "", err
|
| 123 |
+
}
|
| 124 |
+
return out.Path, nil
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
type DirEntry struct {
|
| 128 |
+
Name string `json:"name"`
|
| 129 |
+
IsDir bool `json:"is_dir"`
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
type DirList struct {
|
| 133 |
+
Entries []DirEntry `json:"entries"`
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
func (c *Client) ListDir(ctx context.Context, user, path string) ([]DirEntry, error) {
|
| 137 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
| 138 |
+
fmt.Sprintf("%s/vm/%s/list?path=%s", c.baseURL, user, path), nil)
|
| 139 |
+
if err != nil {
|
| 140 |
+
return nil, err
|
| 141 |
+
}
|
| 142 |
+
resp, err := c.httpClient.Do(req)
|
| 143 |
+
if err != nil {
|
| 144 |
+
return nil, err
|
| 145 |
+
}
|
| 146 |
+
defer resp.Body.Close()
|
| 147 |
+
if resp.StatusCode != http.StatusOK {
|
| 148 |
+
return nil, fmt.Errorf("list dir failed: %s", resp.Status)
|
| 149 |
+
}
|
| 150 |
+
var out DirList
|
| 151 |
+
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
| 152 |
+
return nil, err
|
| 153 |
+
}
|
| 154 |
+
return out.Entries, nil
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
type FileContent struct {
|
| 158 |
+
Content string `json:"content"`
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
func (c *Client) ReadFile(ctx context.Context, user, path string) (string, error) {
|
| 162 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
| 163 |
+
fmt.Sprintf("%s/vm/%s/file?path=%s", c.baseURL, user, path), nil)
|
| 164 |
+
if err != nil {
|
| 165 |
+
return "", err
|
| 166 |
+
}
|
| 167 |
+
resp, err := c.httpClient.Do(req)
|
| 168 |
+
if err != nil {
|
| 169 |
+
return "", err
|
| 170 |
+
}
|
| 171 |
+
defer resp.Body.Close()
|
| 172 |
+
if resp.StatusCode != http.StatusOK {
|
| 173 |
+
return "", fmt.Errorf("read file failed: %s", resp.Status)
|
| 174 |
+
}
|
| 175 |
+
var out FileContent
|
| 176 |
+
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
| 177 |
+
return "", err
|
| 178 |
+
}
|
| 179 |
+
return out.Content, nil
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
func (c *Client) WriteFile(ctx context.Context, user, path, content string) error {
|
| 183 |
+
data, _ := json.Marshal(map[string]string{"path": path, "content": content})
|
| 184 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
| 185 |
+
fmt.Sprintf("%s/vm/%s/file", c.baseURL, user), bytes.NewReader(data))
|
| 186 |
+
if err != nil {
|
| 187 |
+
return err
|
| 188 |
+
}
|
| 189 |
+
req.Header.Set("Content-Type", "application/json")
|
| 190 |
+
resp, err := c.httpClient.Do(req)
|
| 191 |
+
if err != nil {
|
| 192 |
+
return err
|
| 193 |
+
}
|
| 194 |
+
defer resp.Body.Close()
|
| 195 |
+
if resp.StatusCode != http.StatusOK {
|
| 196 |
+
b, _ := io.ReadAll(resp.Body)
|
| 197 |
+
return fmt.Errorf("write file failed: %s - %s", resp.Status, string(b))
|
| 198 |
+
}
|
| 199 |
+
return nil
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
func (c *Client) DeleteFile(ctx context.Context, user, path string) error {
|
| 203 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodDelete,
|
| 204 |
+
fmt.Sprintf("%s/vm/%s/file?path=%s", c.baseURL, user, path), nil)
|
| 205 |
+
if err != nil {
|
| 206 |
+
return err
|
| 207 |
+
}
|
| 208 |
+
resp, err := c.httpClient.Do(req)
|
| 209 |
+
if err != nil {
|
| 210 |
+
return err
|
| 211 |
+
}
|
| 212 |
+
defer resp.Body.Close()
|
| 213 |
+
if resp.StatusCode != http.StatusOK {
|
| 214 |
+
b, _ := io.ReadAll(resp.Body)
|
| 215 |
+
return fmt.Errorf("delete file failed: %s - %s", resp.Status, string(b))
|
| 216 |
+
}
|
| 217 |
+
return nil
|
| 218 |
+
}
|
cli-go/main.go
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import "llm-cli/cmd"
|
| 4 |
+
|
| 5 |
+
func main() {
|
| 6 |
+
cmd.Execute()
|
| 7 |
+
}
|