Ah, you're right, I completely missed that "if SERVER" check.
Digging into it further, it seems like your best option may be to simply generate a list of chat commands using ULib.sayCmds table on the server, then pass it along to the client soon after they join.
Essentially, the ulx.cmdsByCategory table references ULib.cmds.translatedCmds. You could iterate through that to save a for loop, but even then, the opposite commands listed there are literally a direct reference to the primary command:
> print( ULib.cmds.translatedCmds["ulx gag"] == ULib.cmds.translatedCmds["ulx ungag"] )...
true
On top of that, the opposite data for a command doesn't store its chat command: (This may be something we want to look into)
> PrintTable( ULib.cmds.translatedCmds["ulx gag"] )...
<truncated>
opposite = ulx ungag
oppositeArgs:
3 = true
say_cmd:
1 = !gag
And lastly, some chat commands are registered manually via ULib.addSayCommand, and as far as I'm aware, there are no references to it on the client. (XGUI is one example- !menu and !xgui are valid commands that I was too lazy or didn't need to create a full ULX command for).
.. So yeah, I'd definitely recommend sending your own table over from the server using ULib.sayCmds. The table also stores the access required for each chat command (nil for everyone), so you can easily check if the player can run it or not.