diff --git a/index.html b/index.html
new file mode 100644
index 0000000..bab85e1
--- /dev/null
+++ b/index.html
@@ -0,0 +1,348 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main.go b/main.go
index 96e1bfc..9f3ae00 100644
--- a/main.go
+++ b/main.go
@@ -2,12 +2,17 @@ package main
import (
"bytes"
+ "encoding/base64"
"encoding/json"
+ "errors"
"html/template"
"io"
+ "io/fs"
"log"
"net/http"
"os"
+ "path/filepath"
+ "strings"
"time"
)
@@ -40,6 +45,8 @@ type ChatCompletionResponse struct {
var (
openaiAPIKey string
indexTmpl *template.Template
+ workDir string // Path to the workspace directory.
+ absWorkDir string // Absolute workspace path.
)
func init() {
@@ -47,16 +54,42 @@ func init() {
if openaiAPIKey == "" {
log.Fatal("Environment variable OPENAI_API_KEY is not set")
}
- // Parse the HTML template with responsive layout updates.
- indexTmpl = template.Must(template.New("index").Parse(indexHTML))
+ workDir = os.Getenv("WORK_DIR")
+ if workDir == "" {
+ workDir = "./workspace"
+ }
+ var err error
+ absWorkDir, err = filepath.Abs(workDir)
+ if err != nil {
+ log.Fatalf("Error getting absolute path for workspace: %v", err)
+ }
+ if err := os.MkdirAll(absWorkDir, os.ModePerm); err != nil {
+ log.Fatalf("Error creating workspace directory: %v", err)
+ }
+
+ // Load the HTML template from an external file.
+ tmplBytes, err := os.ReadFile("index.html")
+ if err != nil {
+ log.Fatalf("Error reading index.html: %v", err)
+ }
+ indexTmpl, err = template.New("index").Parse(string(tmplBytes))
+ if err != nil {
+ log.Fatalf("Error parsing index.html template: %v", err)
+ }
}
func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/chat", chatHandler)
http.HandleFunc("/title", titleHandler)
+
+ // File system endpoints (sandboxed within absWorkDir)
+ http.HandleFunc("/files", filesListHandler) // GET: list files.
+ http.HandleFunc("/file", fileHandler) // GET for read; POST for write.
+
addr := ":8080"
log.Println("Server starting on", addr)
+ log.Println("Workspace directory:", absWorkDir)
log.Fatal(http.ListenAndServe(addr, nil))
}
@@ -141,7 +174,7 @@ func titleHandler(w http.ResponseWriter, r *http.Request) {
titleRequestMessages := []Message{
{
Role: "system",
- Content: "You are an AI that generates conversation titles. Based on the following conversation, please provide a short, descriptive title of no more than five words with no extra commentary.",
+ Content: "You are an AI that generates conversation titles. Based on the conversation, please provide a short, descriptive title (five words maximum) with no extra commentary.",
},
}
titleRequestMessages = append(titleRequestMessages, clientRequest.Messages...)
@@ -193,333 +226,119 @@ func titleHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(responsePayload)
}
-const indexHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`
+//
+// File system endpoints
+//
+
+// filesListHandler lists all files (recursively) in the workspace.
+func filesListHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Only GET allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ var files []string
+ err := filepath.WalkDir(absWorkDir, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if !d.IsDir() {
+ rel, err := filepath.Rel(absWorkDir, path)
+ if err != nil {
+ return err
+ }
+ files = append(files, rel)
+ }
+ return nil
+ })
+ if err != nil {
+ http.Error(w, "Error listing files: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{"files": files})
+}
+
+// fileHandler supports file read (GET) and write (POST) operations.
+func fileHandler(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodGet:
+ handleFileRead(w, r)
+ case http.MethodPost:
+ handleFileWrite(w, r)
+ default:
+ http.Error(w, "Only GET and POST allowed", http.StatusMethodNotAllowed)
+ }
+}
+
+func handleFileRead(w http.ResponseWriter, r *http.Request) {
+ filename := r.URL.Query().Get("filename")
+ if filename == "" {
+ http.Error(w, "Missing filename", http.StatusBadRequest)
+ return
+ }
+ fullPath, err := secureFilePath(filename)
+ if err != nil {
+ http.Error(w, "Invalid filename: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ data, err := os.ReadFile(fullPath)
+ if err != nil {
+ http.Error(w, "Error reading file: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ encoded := base64.StdEncoding.EncodeToString(data)
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"content_b64": encoded})
+}
+
+func handleFileWrite(w http.ResponseWriter, r *http.Request) {
+ var payload struct {
+ Filename string `json:"filename"`
+ ContentB64 string `json:"content_b64"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
+ http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ if payload.Filename == "" || payload.ContentB64 == "" {
+ http.Error(w, "Filename and content_b64 are required", http.StatusBadRequest)
+ return
+ }
+ decoded, err := base64.StdEncoding.DecodeString(payload.ContentB64)
+ if err != nil {
+ http.Error(w, "Error decoding Base64 content: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ fullPath, err := secureFilePath(payload.Filename)
+ if err != nil {
+ http.Error(w, "Invalid filename: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ dir := filepath.Dir(fullPath)
+ if err := os.MkdirAll(dir, os.ModePerm); err != nil {
+ http.Error(w, "Error creating directory: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := os.WriteFile(fullPath, decoded, 0644); err != nil {
+ http.Error(w, "Error writing file: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "success"})
+}
+
+func secureFilePath(relPath string) (string, error) {
+ relPath = filepath.ToSlash(relPath)
+ if strings.Contains(relPath, "..") {
+ return "", errors.New("relative paths outside the workspace are not allowed")
+ }
+ fullPath := filepath.Join(absWorkDir, relPath)
+ absFullPath, err := filepath.Abs(fullPath)
+ if err != nil {
+ return "", err
+ }
+ if !strings.HasPrefix(absFullPath, absWorkDir) {
+ return "", errors.New("attempt to access files outside the workspace")
+ }
+ return absFullPath, nil
+}