Here's a little script I threw together for this.  Run it with:  !show-hands  Output looks like:      Code:  on('ready',()=>{
  const HE = (() => {
    const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g,'\\$1');
    const e = (s) => `&${s};`;
    const entities = {
      '<' : e('lt'),
      '>' : e('gt'),
      "'" : e('#39'),
      '@' : e('#64'),
      '{' : e('#123'),
      '|' : e('#124'),
      '}' : e('#125'),
      '[' : e('#91'),
      ']' : e('#93'),
      '"' : e('quot')
    };
    const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`,'g');
    return (s) => s.replace(re, (c) => (entities[c] || c) );
  })();
  const s = {
    num: `position:absolute;top:3px;right:3px;z-index:100;font-weight:bold;color:white;font-size:2em;text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;`,
    card: `max-width: 5em;max-height:7em;position:relative;display:inline-block;margin:.5em;box-shadow:.2em .2em .4em .4em;`,
    title: `font-weight:bold;color:black;font-size:1.3em;`,
    title2: `bottom:3px;left:3px;z-index:100;font-weight:bold;color:white;font-size:1em;text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;`,
    player: `padding:.5em;margin-bottom: 1em;`,
    deck: `border:1px solid #333;background-color: #999; margine-left:1em;padding:.5em;`
  };
  const f = {
    t      : (c)   => `<div style="${s.title}">${c}</div>`,
    t2     : (c)   => `<div style="${s.title2}">${c}</div>`,
    player : (p,b) => `<div style="${s.player}">${f.t(p)}${b}</div>`,
    deck   : (d,b) => `<div style="${s.deck}">${f.t2(d)}${b}</div>`,
    card   : (t,i) => `<div style="${s.card}">${f.hover(t,f.image(i))}</div>`,
    hover  : (t,b) => `<span class="tipsy showtip" title="${HE(HE(t))}">${b}</span>`,
    image  : (i)   => `<img src="${i}">`,
  };
  const showCard = (card) => f.card(card.get('name'),card.get('avatar'));
  const getPlayerByID= (()=>{
    let cache = {};
    return (pid) => {
      if(!cache.hasOwnProperty(pid)){
        cache[pid] = getObj('player',pid);
      }
      return cache[pid];
    };
  })();
  const getDeckByID = (()=>{
    let cache = {};
    on('destroy:deck',(did)=>{
      delete cache[did];
    });
    return (did) => {
      if(!cache.hasOwnProperty(did)){
        cache[did] = getObj('deck',did);
      }
      return cache[did];
    };
  })();
  on('chat:message',msg=>{
    if('api'===msg.type && /^!show-hands(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){
      let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
      let hands=findObjs({type:'hand'})
        .reduce((m,h)=>{
          let p = (getPlayerByID(h.get('parentid'))||{get:()=>`Unknown Player: ${h.get('parentid')}`}).get('displayname');
          let ds = h.get('currentHand')
            .split(/\s*,\s*/)
            .filter(s=>s.length)
            .map(cid=>getObj('card',cid))
            .filter(o=>o!==undefined)
            .reduce((m,c)=>({...m,[c.get('deckid')]:[...(m[c.get('deckid')]||[]),c]}), {})
            ;
          let deckOutput = Object.keys(ds).map(id=>{
            let d = (getDeckByID(id)||{get:()=>`Unknown Deck: ${id}`}).get('name');
            let cards = ds[id]
              .map(showCard)
              .join('')
              ;
            return f.deck(d,cards);
          }).join('');
          return [...m,f.player(p,deckOutput)];
        },[]);
        sendChat('',`/w "${who}" ${hands.join('')}`);
    }
  });
});