package main import ( "encoding/json" "fmt" "sort" "strings" ) // ToolResult is the uniform shape returned to the chat loop after a tool call. type ToolResult struct { OK bool `json:"ok"` Result any `json:"result,omitempty"` Error string `json:"error,omitempty"` } func okResult(v any) ToolResult { return ToolResult{OK: true, Result: v} } func errResult(err error) ToolResult { return ToolResult{OK: false, Error: err.Error()} } func errMsg(msg string) ToolResult { return ToolResult{OK: false, Error: msg} } // executeTool dispatches a tool by name with raw JSON input and returns a ToolResult. // Tools that mutate the board return ok=true on success; read-only tools include their data in result. func executeTool(db *DB, name string, input json.RawMessage) ToolResult { switch name { case "list_board": return toolListBoard(db) case "create_column": return toolCreateColumn(db, input) case "update_column": return toolUpdateColumn(db, input) case "rename_column": // alias for backwards compat return toolUpdateColumn(db, input) case "delete_column": return toolDeleteColumn(db, input) case "reorder_columns": return toolReorderColumns(db, input) case "create_card": return toolCreateCard(db, input) case "update_card": return toolUpdateCard(db, input) case "delete_card": return toolDeleteCard(db, input) case "move_card": return toolMoveCard(db, input) case "card_history": return toolCardHistory(db, input) case "find_cards": return toolFindCards(db, input) case "list_users": return toolListUsers(db) case "assign_card": return toolAssignCard(db, input) default: return errMsg("unknown tool: " + name) } } // toolMutates reports whether a successful invocation modifies the board state. func toolMutates(name string) bool { switch name { case "create_column", "update_column", "rename_column", "delete_column", "reorder_columns", "create_card", "update_card", "delete_card", "move_card", "assign_card": return true } return false } func toolListBoard(db *DB) ToolResult { cols, err := db.ListColumns() if err != nil { return errResult(err) } cards, err := db.ListCardsWithTime() if err != nil { return errResult(err) } return okResult(map[string]any{"columns": cols, "cards": cards}) } func toolCreateColumn(db *DB, input json.RawMessage) ToolResult { var in struct{ Name string `json:"name"` } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if strings.TrimSpace(in.Name) == "" { return errMsg("name required") } c, err := db.CreateColumn(in.Name) if err != nil { return errResult(err) } return okResult(c) } func toolUpdateColumn(db *DB, input json.RawMessage) ToolResult { var in struct { ID string `json:"id"` Name *string `json:"name"` Location *string `json:"location"` Width *int `json:"width"` WIPLimit *int `json:"wip_limit"` IsDone *bool `json:"is_done"` } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if in.ID == "" { return errMsg("id required") } if in.Name == nil && in.Location == nil && in.Width == nil && in.WIPLimit == nil && in.IsDone == nil { return errMsg("at least one of name/location/width/wip_limit/is_done required") } if err := db.UpdateColumn(in.ID, ColumnPatch{Name: in.Name, Location: in.Location, Width: in.Width, WIPLimit: in.WIPLimit, IsDone: in.IsDone}); err != nil { return errResult(err) } return okResult(nil) } func toolDeleteColumn(db *DB, input json.RawMessage) ToolResult { var in struct{ ID string } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if in.ID == "" { return errMsg("id required") } if err := db.DeleteColumn(in.ID); err != nil { return errResult(err) } return okResult(nil) } func toolReorderColumns(db *DB, input json.RawMessage) ToolResult { var in struct{ IDs []string } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if len(in.IDs) == 0 { return errMsg("ids required") } if err := db.ReorderColumns(in.IDs); err != nil { return errResult(err) } return okResult(nil) } func toolCreateCard(db *DB, input json.RawMessage) ToolResult { var in struct { ColumnID string `json:"column_id"` Requester string `json:"requester"` Title string `json:"title"` Description string `json:"description"` } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if in.ColumnID == "" || strings.TrimSpace(in.Title) == "" { return errMsg("column_id and title required") } c, err := db.CreateCard(in.ColumnID, in.Requester, in.Title, in.Description, "") if err != nil { return errResult(err) } return okResult(c) } func toolUpdateCard(db *DB, input json.RawMessage) ToolResult { var raw map[string]any if err := json.Unmarshal(input, &raw); err != nil { return errResult(err) } id, _ := raw["id"].(string) if id == "" { return errMsg("id required") } patch := CardPatch{} if v, ok := raw["requester"].(string); ok { patch.Requester = &v } if v, ok := raw["title"].(string); ok { patch.Title = &v } if v, ok := raw["description"].(string); ok { patch.Description = &v } if v, ok := raw["color"].(string); ok { patch.Color = &v } if v, ok := raw["locked"].(bool); ok { patch.Locked = &v } if v, present := raw["assignee_id"]; present { patch.HasAssignee = true if v == nil { empty := "" patch.AssigneeID = &empty } else if s, ok := v.(string); ok { patch.AssigneeID = &s } } if err := db.UpdateCard(id, patch); err != nil { return errResult(err) } return okResult(nil) } func toolListUsers(db *DB) ToolResult { users, err := db.ListUsers() if err != nil { return errResult(err) } return okResult(users) } func toolAssignCard(db *DB, input json.RawMessage) ToolResult { var in struct { ID string `json:"id"` AssigneeID *string `json:"assignee_id"` } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if in.ID == "" { return errMsg("id required") } patch := CardPatch{HasAssignee: true} if in.AssigneeID == nil { empty := "" patch.AssigneeID = &empty } else { patch.AssigneeID = in.AssigneeID } if err := db.UpdateCard(in.ID, patch); err != nil { return errResult(err) } return okResult(nil) } func toolDeleteCard(db *DB, input json.RawMessage) ToolResult { var in struct{ ID string } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if in.ID == "" { return errMsg("id required") } if err := db.DeleteCard(in.ID); err != nil { return errResult(err) } return okResult(nil) } // toolMoveCard accepts {id, column_id, ordered_ids?}. If ordered_ids is missing, // the card is appended to the end of the destination column. func toolMoveCard(db *DB, input json.RawMessage) ToolResult { var in struct { ID string `json:"id"` ColumnID string `json:"column_id"` OrderedIDs []string `json:"ordered_ids"` } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if in.ID == "" || in.ColumnID == "" { return errMsg("id and column_id required") } if len(in.OrderedIDs) == 0 { cards, err := db.ListCardsWithTime() if err != nil { return errResult(err) } var dest []Card for _, c := range cards { if c.ColumnID == in.ColumnID && c.ID != in.ID { dest = append(dest, c) } } sort.Slice(dest, func(i, j int) bool { return dest[i].Position < dest[j].Position }) ids := make([]string, 0, len(dest)+1) for _, c := range dest { ids = append(ids, c.ID) } ids = append(ids, in.ID) in.OrderedIDs = ids } if err := db.MoveCard(in.ID, in.ColumnID, in.OrderedIDs, ""); err != nil { return errResult(err) } return okResult(nil) } func toolCardHistory(db *DB, input json.RawMessage) ToolResult { var in struct{ ID string } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } if in.ID == "" { return errMsg("id required") } hist, err := db.CardHistory(in.ID) if err != nil { return errResult(err) } return okResult(hist) } func toolFindCards(db *DB, input json.RawMessage) ToolResult { var in struct { Query string `json:"query"` ColumnID string `json:"column_id"` Requester string `json:"requester"` } if err := json.Unmarshal(input, &in); err != nil { return errResult(err) } cards, err := db.ListCardsWithTime() if err != nil { return errResult(err) } q := strings.ToLower(strings.TrimSpace(in.Query)) col := in.ColumnID req := strings.ToLower(strings.TrimSpace(in.Requester)) out := make([]Card, 0, len(cards)) for _, c := range cards { if col != "" && c.ColumnID != col { continue } if req != "" && !strings.Contains(strings.ToLower(c.Requester), req) { continue } if q != "" { hay := strings.ToLower(c.Title + " " + c.Description + " " + c.Requester) if !strings.Contains(hay, q) { continue } } out = append(out, c) } return okResult(out) } // validateToolName fails fast with clearer error than the dispatch's default. func validateToolName(name string) error { known := map[string]bool{ "list_board": true, "create_column": true, "update_column": true, "rename_column": true, "delete_column": true, "reorder_columns": true, "create_card": true, "update_card": true, "delete_card": true, "move_card": true, "card_history": true, "find_cards": true, "list_users": true, "assign_card": true, } if !known[name] { return fmt.Errorf("unknown tool: %s", name) } return nil }