diff --git a/backend/cli.py b/backend/cli.py
new file mode 100755
index 0000000..79ff515
--- /dev/null
+++ b/backend/cli.py
@@ -0,0 +1,434 @@
+#!/usr/bin/env python3
+"""
+MIND CLI (v0) — supports Ops 1–8 via REST calls to the Go backend.
+
+Assumptions:
+ - API base: http://localhost:8080 (change with \\set api ...)
+ - llama.cpp server: http://localhost:8081 (change with \\set llama ...)
+ - Auth: dev token ("Bearer dev"). Adjust as needed.
+
+Core usage model:
+ - Free text (no leading '\\') calls /completion (Op 2 helper) on the active conversation/branch.
+ - Backslash commands perform explicit ops.
+
+Operations → Commands mapping
+ 1) Start conversation → \\new
+ 2) Commit (prompt/answer or node)→ free text → /completion; or \\node [parent_id]
+ 3) Fork branch → \\branch [head_node_id]
+ 4) Delete branch or node → \\delbranch | \\delnode
+ 5) Extract subset as new tree → \\extract
+ 6) Merge two branches → \\merge
+ 7) Detach branch to new convo → \\detach
+ 8) Append/copy tree to another → \\append-tree [dst_conv [dst_branch]]
+
+Helpful extras:
+ - \\listconvs
+ - \\listbranches
+ - \\linearize (active conv/branch)
+ - \\llama (debug llama-server directly)
+ - \\useconv ; \\usebranch ; \\who ; \\quit
+"""
+
+import json
+import os
+import shlex
+import sys
+from typing import Dict, Any, List
+
+import requests
+
+DEFAULT_API = os.environ.get("MIND_API", "http://localhost:8080")
+DEFAULT_LLAMA = os.environ.get("LLAMA_API", "http://localhost:8081")
+
+auth_headers = lambda: {"Authorization": "Bearer dev", "Content-Type": "application/json"}
+
+class State:
+ def __init__(self):
+ self.api = DEFAULT_API.rstrip('/')
+ self.llama = DEFAULT_LLAMA.rstrip('/')
+ self.conversation_id = None # int
+ self.branch = None # str
+
+ def as_dict(self):
+ return {
+ "api": self.api,
+ "llama": self.llama,
+ "conversation_id": self.conversation_id,
+ "branch": self.branch,
+ }
+
+
+def pretty(obj: Any) -> str:
+ try:
+ return json.dumps(obj, indent=2, ensure_ascii=False)
+ except Exception:
+ return str(obj)
+
+
+def post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
+ r = requests.post(url, headers=auth_headers(), data=json.dumps(payload), timeout=60)
+ r.raise_for_status()
+ if r.text.strip() == "":
+ return {}
+ return r.json()
+
+
+def delete_json(url: str, payload: Dict[str, Any] | None = None) -> Dict[str, Any]:
+ r = requests.delete(url, headers=auth_headers(), data=json.dumps(payload) if payload else None, timeout=60)
+ r.raise_for_status()
+ return r.json() if r.text.strip() else {}
+
+
+def get_json(url: str, params: Dict[str, Any]) -> Dict[str, Any] | List[Any]:
+ r = requests.get(url, headers=auth_headers(), params=params, timeout=60)
+ r.raise_for_status()
+ ct = r.headers.get('Content-Type','')
+ if 'application/json' in ct:
+ return r.json()
+ return {"raw": r.text}
+
+
+# ---------- Commands ----------
+
+def cmd_help():
+ print(__doc__)
+
+
+def cmd_set(st: State, args):
+ if len(args) < 2:
+ print("usage: \\set ")
+ return
+ key, val = args[0], args[1]
+ if key == 'api':
+ st.api = val.rstrip('/')
+ elif key == 'llama':
+ st.llama = val.rstrip('/')
+ else:
+ print("unknown key; use 'api' or 'llama'")
+ return
+ print("ok:", pretty(st.as_dict()))
+
+
+# (1) Start conversation
+
+def cmd_new(st: State, args):
+ if len(args) < 2:
+ print("usage: \\new ")
+ return
+ title = args[0]
+ try:
+ owner_id = int(args[1])
+ except ValueError:
+ print("owner_id must be an integer")
+ return
+ out = post_json(f"{st.api}/conversations", {"title": title, "owner_id": owner_id})
+ cid = out.get("id")
+ st.conversation_id = cid
+ print("created conversation:", cid)
+
+
+def cmd_listconvs(st: State, args):
+ if not args:
+ print("usage: \\listconvs ")
+ return
+ owner_id = int(args[0])
+ out = get_json(f"{st.api}/conversations", {"owner_id": owner_id})
+ print(pretty(out))
+
+
+# (3) Fork branch
+
+def cmd_branch(st: State, args):
+ if st.conversation_id is None:
+ print("set a conversation first: \\useconv or \\new ")
+ return
+ if len(args) < 1:
+ print("usage: \\branch [head_node_id]")
+ return
+ name = args[0]
+ head = int(args[1]) if len(args) > 1 else 0
+ payload = {
+ "conversation_id": st.conversation_id,
+ "name": name,
+ "head_node_id": head
+ }
+ out = post_json(f"{st.api}/branches", payload)
+ st.branch = out.get("name", name)
+ print("active branch:", st.branch, pretty(out))
+
+
+def cmd_usebranch(st: State, args):
+ if not args:
+ print("usage: \\usebranch ")
+ return
+ st.branch = args[0]
+ print("active branch:", st.branch)
+
+
+def cmd_listbranches(st: State, args):
+ if not args:
+ if st.conversation_id is None:
+ print("usage: \\listbranches (or set active with \\useconv)")
+ return
+ conv = st.conversation_id
+ else:
+ conv = int(args[0])
+ out = get_json(f"{st.api}/branches", {"conversation_id": conv})
+ print(pretty(out))
+
+
+def cmd_useconv(st: State, args):
+ if not args:
+ print("usage: \\useconv ")
+ return
+ st.conversation_id = int(args[0])
+ print("active conversation:", st.conversation_id)
+
+
+# (2) Commit helpers
+
+def cmd_node(st: State, args):
+ """Create a single node (explicit Op 2). usage: \\node [parent_id]"""
+ if st.conversation_id is None:
+ print("set active conversation first")
+ return
+ if len(args) < 2:
+ print("usage: \\node [parent_id]")
+ return
+ author = args[0]
+ content = args[1]
+ parent_id = int(args[2]) if len(args) > 2 else None
+ payload = {
+ "conversation_id": st.conversation_id,
+ "author_kind": author,
+ "content": content
+ }
+ if parent_id is not None:
+ payload["parent_id"] = parent_id
+ out = post_json(f"{st.api}/nodes", payload)
+ print(pretty(out))
+
+
+def send_prompt(st: State, line: str):
+ # Free text → /completion (with auto-branch-create retry)
+ if st.conversation_id is None or not st.branch:
+ print("need active conversation and branch (\\new/\\useconv and \\branch/\\usebranch)")
+ return
+ payload = {
+ "conversation_id": st.conversation_id,
+ "branch": st.branch,
+ "prompt": line.strip(),
+ }
+ try:
+ out = post_json(f"{st.api}/completion", payload)
+ except requests.HTTPError as e:
+ body = e.response.text if e.response is not None else ""
+ # If branch doesn't exist yet, create it and retry once
+ if e.response is not None and e.response.status_code in (400, 404, 500) and "branch" in body.lower():
+ _ = post_json(f"{st.api}/branches", {
+ "conversation_id": st.conversation_id,
+ "name": st.branch,
+ "head_node_id": 0
+ })
+ out = post_json(f"{st.api}/completion", payload)
+ else:
+ raise
+ print("assistant:\n", out.get("answer"))
+
+
+# (4) Delete branch / node
+def cmd_delbranch(st: State, args):
+ if st.conversation_id is None:
+ print("set active conversation first")
+ return
+ if not args:
+ print("usage: \\delbranch ")
+ return
+ name = args[0]
+ out = delete_json(f"{st.api}/branches", {"conversation_id": st.conversation_id, "name": name})
+ if st.branch == name:
+ st.branch = None
+ print("deleted branch:", name, pretty(out))
+
+
+def cmd_delnode(st: State, args):
+ if not args:
+ print("usage: \\delnode ")
+ return
+ node_id = int(args[0])
+ out = delete_json(f"{st.api}/nodes/{node_id}")
+ print(pretty(out))
+
+
+# (5) Extract selected nodes to new conversation
+
+def cmd_extract(st: State, args):
+ if len(args) < 2:
+ print("usage: \\extract ")
+ return
+ title = args[0]
+ node_ids = [int(x) for x in args[1:]]
+ payload = {"conversation_id": st.conversation_id, "title": title, "node_ids": node_ids}
+ out = post_json(f"{st.api}/extract", payload)
+ new_cid = out.get("new_conversation_id")
+ print("extracted to conversation:", new_cid, pretty(out))
+
+
+# (6) Merge two branches (concat order)
+
+def cmd_merge(st: State, args):
+ if st.conversation_id is None:
+ print("set active conversation first")
+ return
+ if len(args) < 3:
+ print("usage: \\merge ")
+ return
+ left, right, order = args[0], args[1], args[2]
+ payload = {"conversation_id": st.conversation_id, "left_branch": left, "right_branch": right, "order": order}
+ out = post_json(f"{st.api}/merge", payload)
+ print("merge result:", pretty(out))
+
+
+# (7) Detach branch → new conversation
+
+def cmd_detach(st: State, args):
+ if st.conversation_id is None:
+ print("set active conversation first")
+ return
+ if not args:
+ print("usage: \\detach ")
+ return
+ payload = {"conversation_id": st.conversation_id, "branch": args[0]}
+ out = post_json(f"{st.api}/detach", payload)
+ print("detached to:", pretty(out))
+
+
+# (8) Append/copy tree
+
+def cmd_append_tree(st: State, args):
+ if len(args) < 2:
+ print("usage: \\append-tree [dst_conv [dst_branch]]")
+ return
+ src_conv = int(args[0])
+ src_branch = args[1]
+ dst_conv = int(args[2]) if len(args) > 2 else st.conversation_id
+ dst_branch = args[3] if len(args) > 3 else st.branch
+ payload = {"src_conversation_id": src_conv, "src_branch": src_branch, "dst_conversation_id": dst_conv, "dst_branch": dst_branch}
+ out = post_json(f"{st.api}/append-tree", payload)
+ print("append-tree result:", pretty(out))
+
+
+# Linearize & llama & misc
+
+def cmd_linearize(st: State):
+ if st.conversation_id is None or not st.branch:
+ print("need active conversation and branch (\\useconv / \\usebranch)")
+ return
+ out = get_json(f"{st.api}/linearize", {"conversation_id": st.conversation_id, "branch": st.branch})
+ if isinstance(out, dict):
+ print("==== Transcript ====")
+ print(out.get("text", ""))
+ else:
+ print(pretty(out))
+
+
+def cmd_llama(st: State, args):
+ if not args:
+ print("usage: \\llama ")
+ return
+ prompt = " ".join(args)
+ payload = {"prompt": prompt}
+ try:
+ r = requests.post(f"{st.llama}/completion", headers={"Content-Type": "application/json"}, data=json.dumps(payload), timeout=60)
+ r.raise_for_status()
+ data = r.json()
+ text = None
+ if isinstance(data, dict):
+ if 'completion' in data:
+ text = data['completion']
+ elif 'content' in data and isinstance(data['content'], list) and data['content']:
+ seg = data['content'][0]
+ if isinstance(seg, dict) and 'text' in seg:
+ text = seg['text']
+ print("llama says:", text or pretty(data))
+ except Exception as e:
+ print("llama error:", e)
+
+
+def cmd_who(st: State):
+ print(pretty(st.as_dict()))
+
+
+# ---------- REPL ----------
+
+def repl():
+ st = State()
+ print("MIND CLI — type \\help for commands. Free text sends /completion.")
+ while True:
+ try:
+ line = input("> ").strip()
+ except (EOFError, KeyboardInterrupt):
+ print()
+ break
+ if not line:
+ continue
+ if line.startswith("\\"):
+ parts = shlex.split(line[1:])
+ if not parts:
+ continue
+ cmd, *args = parts
+ cmd = cmd.lower()
+ try:
+ if cmd == 'help':
+ cmd_help()
+ elif cmd == 'set':
+ cmd_set(st, args)
+ elif cmd == 'new':
+ cmd_new(st, args)
+ elif cmd == 'listconvs':
+ cmd_listconvs(st, args)
+ elif cmd == 'listbranches':
+ cmd_listbranches(st, args)
+ elif cmd == 'useconv':
+ cmd_useconv(st, args)
+ elif cmd == 'branch':
+ cmd_branch(st, args)
+ elif cmd == 'usebranch':
+ cmd_usebranch(st, args)
+ elif cmd == 'node':
+ cmd_node(st, args)
+ elif cmd == 'delbranch':
+ cmd_delbranch(st, args)
+ elif cmd == 'delnode':
+ cmd_delnode(st, args)
+ elif cmd == 'extract':
+ cmd_extract(st, args)
+ elif cmd == 'merge':
+ cmd_merge(st, args)
+ elif cmd == 'detach':
+ cmd_detach(st, args)
+ elif cmd in ('append-tree', 'appendtree', 'append'):
+ cmd_append_tree(st, args)
+ elif cmd == 'linearize':
+ cmd_linearize(st)
+ elif cmd == 'llama':
+ cmd_llama(st, args)
+ elif cmd == 'who':
+ cmd_who(st)
+ elif cmd in ('quit', 'exit', 'q'):
+ break
+ else:
+ print("unknown command, try \\help")
+ except requests.HTTPError as e:
+ try:
+ print("HTTP", e.response.status_code, e.response.text)
+ except Exception:
+ print("HTTP error:", e)
+ except Exception as e:
+ print("error:", e)
+ else:
+ send_prompt(st, line)
+
+
+if __name__ == '__main__':
+ repl()
diff --git a/backend/cmd/main.go b/backend/cmd/main.go
index a913128..6fd6dac 100644
--- a/backend/cmd/main.go
+++ b/backend/cmd/main.go
@@ -26,7 +26,7 @@ func main() {
}
repo := db.NewRepo(database)
- g := glue.NewGlue(repo)
+ g := glue.NewGlue(repo, cfg)
r := httpapi.NewRouter(logger, cfg, g)
srv := &http.Server{
diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go
index 6647377..747a161 100644
--- a/backend/internal/config/config.go
+++ b/backend/internal/config/config.go
@@ -5,19 +5,21 @@ import (
)
type Config struct {
- DSN string // e.g. mysql: "user:pass@tcp(127.0.0.1:3306)/mind?parseTime=true"; sqlite: "file:mind.db?_pragma=busy_timeout(5000)"
+ DSN string
Driver string // "mysql" or "sqlite"
JWTSecret string
- Port string // default 8080
+ Port string
+ LlamaURL string
}
func getenv(k, def string) string { v := os.Getenv(k); if v == "" { return def }; return v }
func Load() Config {
- return Config{
- DSN: getenv("DB_DSN", "file:mind.db?_pragma=busy_timeout(5000)"),
- Driver: getenv("DB_DRIVER", "sqlite"),
- JWTSecret: getenv("JWT_SECRET", "devsecret"),
- Port: getenv("PORT", "8080"),
- }
+ return Config{
+ DSN: getenv("DB_DSN", "file:mind.db?_pragma=busy_timeout(5000)"),
+ Driver: getenv("DB_DRIVER", "sqlite"),
+ JWTSecret: getenv("JWT_SECRET", "devsecret"),
+ Port: getenv("PORT", "8080"),
+ LlamaURL: getenv("LLAMA_URL", "http://localhost:8081"),
+ }
}
diff --git a/backend/internal/db/repo.go b/backend/internal/db/repo.go
index 3a5a712..7842cb8 100644
--- a/backend/internal/db/repo.go
+++ b/backend/internal/db/repo.go
@@ -86,3 +86,29 @@ func (r *Repo) LatestParent(ctx context.Context, childID int64) (models.Node, bo
}
return n, true, nil
}
+
+func (r *Repo) DeleteBranch(ctx context.Context, convID int64, name string) error {
+ _, err := r.db.ExecContext(ctx, `DELETE FROM branches WHERE conversation_id=? AND name=?`, convID, name)
+ return err
+}
+
+// ListBranches returns all branches for a conversation, newest first.
+func (r *Repo) ListBranches(ctx context.Context, convID int64) ([]models.Branch, error) {
+ rows, err := r.db.QueryContext(ctx, `
+ SELECT id, conversation_id, name, head_node_id
+ FROM branches
+ WHERE conversation_id=?
+ ORDER BY id DESC`, convID)
+ if err != nil { return nil, err }
+ defer rows.Close()
+
+ var out []models.Branch
+ for rows.Next() {
+ var b models.Branch
+ if err := rows.Scan(&b.ID, &b.ConversationID, &b.Name, &b.HeadNodeID); err != nil {
+ return nil, err
+ }
+ out = append(out, b)
+ }
+ return out, nil
+}
diff --git a/backend/internal/glue/branches.go b/backend/internal/glue/branches.go
index 759385e..08ebc6b 100644
--- a/backend/internal/glue/branches.go
+++ b/backend/internal/glue/branches.go
@@ -5,8 +5,20 @@ import (
"mind/internal/models"
)
-type ForkReq struct { ConversationID int64; Name string; HeadNodeID int64 }
+type ForkReq struct {
+ ConversationID int64 `json:"conversation_id"`
+ Name string `json:"name"`
+ HeadNodeID int64 `json:"head_node_id"`
+}
func (g *Glue) ForkBranch(ctx context.Context, fr ForkReq) (models.Branch, error) {
return g.repo.CreateOrGetBranch(ctx, fr.ConversationID, fr.Name, fr.HeadNodeID)
}
+
+func (g *Glue) DeleteBranch(ctx context.Context, conversationID int64, name string) error {
+ return g.repo.DeleteBranch(ctx, conversationID, name)
+}
+
+func (g *Glue) ListBranches(ctx context.Context, conversationID int64) ([]models.Branch, error) {
+ return g.repo.ListBranches(ctx, conversationID)
+}
diff --git a/backend/internal/glue/completion.go b/backend/internal/glue/completion.go
index 95dc19c..04c263c 100644
--- a/backend/internal/glue/completion.go
+++ b/backend/internal/glue/completion.go
@@ -1,9 +1,34 @@
package glue
import (
+ "bytes"
"context"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "time"
+
+ "mind/internal/config"
+ "mind/internal/db"
+ "mind/internal/models"
)
+type Glue struct {
+ repo *db.Repo
+ cfg config.Config
+}
+
+// NewGlue now receives cfg
+func NewGlue(r *db.Repo, cfg config.Config) *Glue { return &Glue{repo: r, cfg: cfg} }
+
+func (g *Glue) CreateConversation(ctx context.Context, ownerID int64, title string) (int64, error) {
+ return g.repo.CreateConversation(ctx, ownerID, title)
+}
+
+func (g *Glue) ListConversations(ctx context.Context, ownerID int64) ([]models.Conversation, error) {
+ return g.repo.ListConversations(ctx, ownerID)
+}
+
type CompletionReq struct {
ConversationID int64 `json:"conversation_id"`
BranchName string `json:"branch"`
@@ -11,29 +36,78 @@ type CompletionReq struct {
}
type CompletionResp struct {
- PromptNodeID int64 `json:"prompt_node_id"`
- AnswerNodeID int64 `json:"answer_node_id"`
- Answer string `json:"answer"`
+ PromptNodeID int64 `json:"prompt_node_id"`
+ AnswerNodeID int64 `json:"answer_node_id"`
+ Answer string `json:"answer"`
+}
+
+// llama request/response (support two common shapes)
+type llamaReq struct {
+ Prompt string `json:"prompt"`
+}
+type llamaResp struct {
+ Content string `json:"content"`
+}
+
+func (g *Glue) callLlama(ctx context.Context, prompt string) (string, error) {
+ if g.cfg.LlamaURL == "" {
+ return "", errors.New("LLAMA_URL not set")
+ }
+ body, _ := json.Marshal(llamaReq{Prompt: prompt})
+ url := g.cfg.LlamaURL + "/completion"
+
+ httpc := &http.Client{Timeout: 45 * time.Second}
+
+ req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := httpc.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ dec := json.NewDecoder(resp.Body)
+ var a llamaResp
+ if err := dec.Decode(&a); err == nil && a.Content != "" {
+ return a.Content, nil
+ }
+
+ return "", errors.New("unexpected llama response")
}
-// For v0 we stub the answer as a simple echo with a prefix.
func (g *Glue) AppendCompletion(ctx context.Context, req CompletionReq) (CompletionResp, error) {
b, err := g.repo.GetBranch(ctx, req.ConversationID, req.BranchName)
- if err != nil { return CompletionResp{}, err }
+ if err != nil {
+ return CompletionResp{}, err
+ }
- // 1) create user prompt node
+ // 1) user prompt node
promptID, err := g.repo.CreateNode(ctx, req.ConversationID, "user", req.Prompt)
- if err != nil { return CompletionResp{}, err }
- if err := g.repo.Link(ctx, b.HeadNodeID, promptID); err != nil { return CompletionResp{}, err }
+ if err != nil {
+ return CompletionResp{}, err
+ }
+ if err := g.repo.Link(ctx, b.HeadNodeID, promptID); err != nil {
+ return CompletionResp{}, err
+ }
+
+ // 2) real llama call (fallback to stub if it fails)
+ answerText, err := g.callLlama(ctx, req.Prompt)
+ if err != nil {
+ answerText = "(stub) You said: " + req.Prompt
+ }
- // 2) create assistant answer node (stub)
- answerText := "(stub) You said: " + req.Prompt
answerID, err := g.repo.CreateNode(ctx, req.ConversationID, "assistant", answerText)
- if err != nil { return CompletionResp{}, err }
- if err := g.repo.Link(ctx, promptID, answerID); err != nil { return CompletionResp{}, err }
+ if err != nil {
+ return CompletionResp{}, err
+ }
+ if err := g.repo.Link(ctx, promptID, answerID); err != nil {
+ return CompletionResp{}, err
+ }
// 3) move branch head
- if err := g.repo.MoveBranchHead(ctx, b.ID, answerID); err != nil { return CompletionResp{}, err }
+ if err := g.repo.MoveBranchHead(ctx, b.ID, answerID); err != nil {
+ return CompletionResp{}, err
+ }
return CompletionResp{PromptNodeID: promptID, AnswerNodeID: answerID, Answer: answerText}, nil
}
diff --git a/backend/internal/glue/conversations.go b/backend/internal/glue/conversations.go
deleted file mode 100644
index 74f9c21..0000000
--- a/backend/internal/glue/conversations.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package glue
-
-import (
- "context"
- "mind/internal/db"
- "mind/internal/models"
-)
-
-type Glue struct { repo *db.Repo }
-
-func NewGlue(r *db.Repo) *Glue { return &Glue{repo: r} }
-
-func (g *Glue) CreateConversation(ctx context.Context, ownerID int64, title string) (int64, error) {
- return g.repo.CreateConversation(ctx, ownerID, title)
-}
-
-func (g *Glue) ListConversations(ctx context.Context, ownerID int64) ([]models.Conversation, error) {
- return g.repo.ListConversations(ctx, ownerID)
-}
diff --git a/backend/internal/http/handlers.go b/backend/internal/http/handlers.go
index 4e5441f..6bc02d0 100644
--- a/backend/internal/http/handlers.go
+++ b/backend/internal/http/handlers.go
@@ -30,13 +30,51 @@ func (s *server) conversations(w http.ResponseWriter, r *http.Request) {
}
// POST /branches {conversation_id, name, head_node_id}
+// GET /branches?conversation_id=...
+// DELETE /branches {conversation_id, name}
func (s *server) branches(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost { w.WriteHeader(405); return }
- var in glue.ForkReq
- if err := json.NewDecoder(r.Body).Decode(&in); err != nil { writeJSON(w, 400, map[string]string{"error":"bad json"}); return }
- b, err := s.glue.ForkBranch(r.Context(), in)
- if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}); return }
- writeJSON(w, 200, b)
+ switch r.Method {
+ case http.MethodPost:
+ var in struct {
+ ConversationID int64 `json:"conversation_id"`
+ Name string `json:"name"`
+ HeadNodeID int64 `json:"head_node_id"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
+ writeJSON(w, 400, map[string]string{"error": "bad json"}); return
+ }
+ b, err := s.glue.ForkBranch(r.Context(), glue.ForkReq{
+ ConversationID: in.ConversationID,
+ Name: in.Name,
+ HeadNodeID: in.HeadNodeID,
+ })
+ if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}); return }
+ writeJSON(w, 200, b)
+
+ case http.MethodGet:
+ qs := r.URL.Query().Get("conversation_id")
+ if qs == "" { writeJSON(w, 400, map[string]string{"error":"conversation_id required"}); return }
+ convID, _ := strconv.ParseInt(qs, 10, 64)
+ out, err := s.glue.ListBranches(r.Context(), convID)
+ if err != nil { writeJSON(w, 500, map[string]string{"error": err.Error()}); return }
+ writeJSON(w, 200, out)
+
+ case http.MethodDelete:
+ var in struct {
+ ConversationID int64 `json:"conversation_id"`
+ Name string `json:"name"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
+ writeJSON(w, 400, map[string]string{"error":"bad json"}); return
+ }
+ if err := s.glue.DeleteBranch(r.Context(), in.ConversationID, in.Name); err != nil {
+ writeJSON(w, 500, map[string]string{"error": err.Error()}); return
+ }
+ writeJSON(w, 200, map[string]string{"ok":"true"})
+
+ default:
+ w.WriteHeader(405)
+ }
}
// POST /completion {conversation_id, branch, prompt}
diff --git a/backend/mind.db b/backend/mind.db
deleted file mode 100644
index ee85de9..0000000
Binary files a/backend/mind.db and /dev/null differ