npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ttt-lan

v1.1.0

Published

Zero-config LAN tic-tac-toe for your terminal: discover colleagues, invite, play, and share a gossiped leaderboard.

Readme

ttt-lan

Play tic-tac-toe with the people sitting around you. No server, no signup, no IP addresses to type.

npx ttt-lan

It asks your name once, then shows everyone else on your office network who is also running it — with their records, ratings, and ping. Type /invite arjun, they type /accept, and you are playing. Results are gossiped across the network, so the leaderboard stays in sync on every machine, even ones that were not involved in the game.

╭─ TTT-LAN ────────────────────────────────── you: Riya (1042 Elo) ─╮
│                                                                    │
│   #  Player          Status      Elo    W-L-D     Ping             │
│   1  Arjun#7f3a      idle        1088   14-6-2     3ms             │
│   2  Meera           in-game     1031    8-9-1    11ms             │
│   3  Dev             idle         996    4-4-0     7ms             │
│   4  Sana            away         974    2-7-1      —              │
│                                                                    │
│   4 players online · 214 matches recorded                          │
╰────────────────────────────────────────────────────────────────────╯
  /invite <name|#>   /who   /scores   /name <new>   /help   /quit

Read this first — the three things that actually go wrong

1. Your firewall will ask permission on the first run. Click Allow, on private networks. This is by far the most common "it doesn't work" report. macOS and Windows both prompt; if you dismiss it, other people simply never see you. On Windows, make sure the Private box is ticked, not just Public.

2. A corporate VPN will usually break discovery. VPN clients hijack the default route, so broadcasts go down the tunnel instead of onto the office LAN. Symptom: the lobby stays empty even though a colleague two desks away is running it. Fix:

ttt-lan --iface 192.168.1.41      # your real LAN address, from /whoami or `ip addr`

3. Some managed office access points enable "client isolation", which blocks all peer-to-peer traffic. No fallback can rescue this — the network is deliberately preventing your machines from talking to each other. If /connect to a colleague's address also fails, this is almost certainly why. Ask your network admin, or use a different network.

If the lobby is empty for 10 seconds you will get an on-screen hint pointing at these.


Requirements

  • Node.js ≥ 20.11 (22 LTS recommended). Check with node --version.
  • Everyone must be on the same subnet — same office wifi or switch.

Install

npx ttt-lan            # no install, always the current version
npm i -g ttt-lan       # or install it properly

Playing

Once you are in the lobby:

| What you want | What you type | |---|---| | See who is around | /who | | Challenge someone | /invite arjun or /invite #2 | | Accept a challenge | /accept | | Play a square | 5 — just the number, 1–9 like a numpad | | Say something | /chat nice one or > nice one — works in-game and in the lobby | | Quick reactions | /gg, /nice, /oops | | Taunt the loser | /taunt — winners only 😏 | | Give up | /resign | | Play again | /rematch (colors swap each time) | | See the leaderboard | /scores | | Change your name | /name Riya | | Leave | /quit, or Ctrl-C twice |

Squares are numbered like a phone keypad, and you can also use tl, mid, br, or b2:

 1 │ 2 │ 3
───┼───┼───
 4 │ 5 │ 6
───┼───┼───
 7 │ 8 │ 9

When broadcast is blocked

If nobody shows up automatically, one person runs /whoami and reads out their address; everyone else runs:

/connect 192.168.1.42

That is enough to bootstrap the whole office — peers exchange their rosters on connect, so one manual link pulls in everyone that person can see.

Trash talk

Win a game and /taunt flashes a message across the loser's terminal:

╔══════════════════════════════════════════════════════════════════╗
║    Tumse na ho payega                                            ║
║    — Riya                                                        ║
╚══════════════════════════════════════════════════════════════════╝

/taunt alone sends the classic; /taunt anything you like sends your own. Only the winner of the last game can fire one — losing and taunting is just noise. The banner fades after a few seconds.

/chat also works in the lobby now, so you can round people up before starting a game. And when someone gets hot, everyone sees it:

🔥 Arjun is on a 3-win streak!

Ratings

Standard Elo, starting at 1000. Ratings are never stored: they are recomputed from the match log every time, so any two machines holding the same games always agree on the standings, no matter what order the results arrived in. Players with fewer than 3 games show as (provisional) and rank below established players.

Command-line options

ttt-lan                             # normal launch
ttt-lan --name Riya                 # skip/override the name prompt
ttt-lan --port 37025                # pin the TCP port
ttt-lan --discovery-port 37030      # pin the UDP port (multi-instance on macOS)
ttt-lan --iface 192.168.1.41        # pin the broadcast interface (VPN escape hatch)
ttt-lan --connect 192.168.1.42      # bootstrap a manual peer at startup
ttt-lan --hotseat                   # two players, one keyboard, no network
ttt-lan --no-color --ascii          # dumb-terminal mode
ttt-lan --debug                     # verbose file logging
ttt-lan --reset                     # wipe profile + results (asks first)

--ascii swaps the box-drawing characters and emoji for plain ASCII; use it with --no-color on terminals that mangle Unicode (notably older Windows cmd.exe). NO_COLOR is honored too.

Your data

Everything lives in ~/.ttt-lan/:

profile.json      your id and display name
settings.json     turn timeout, theme, sounds
results.jsonl     every match you know about, one JSON object per line
ttt-lan.log       debug log (1MB × 3, rotated)

Set TTT_LAN_HOME to move it elsewhere. Deleting results.jsonl only forgets your local copy of the history — anyone else still holding those games will gossip them back.

Privacy

Anyone on your network can see your name and play you. Don't use a name you wouldn't put on a whiteboard. Names, chat, and match results are broadcast unencrypted on the local network. This is a toy for a trusted office LAN, not a secure messenger.

Strings arriving from the network are stripped of control characters and ANSI escape sequences before rendering, so a colleague cannot name themselves something that scribbles on your terminal.


Development

npm test           # node --test, ~150 tests
npm run typecheck  # tsc --noEmit over JSDoc types; there is no build step
npm start          # run from source

The code is plain ESM JavaScript with JSDoc types — npm run typecheck type-checks it without a compile step, and src/ runs directly as written.

Running several instances on one machine

Each instance needs its own data directory:

TTT_LAN_HOME=/tmp/ttt/a ttt-lan --name Alpha &
TTT_LAN_HOME=/tmp/ttt/b ttt-lan --name Bravo &
TTT_LAN_HOME=/tmp/ttt/c ttt-lan --name Charlie &

On Linux and recent Node this just works — SO_REUSEPORT lets them share the discovery port.

On macOS/BSD with older Node, SO_REUSEADDR alone is not enough for two processes to bind the same UDP port, so give each instance its own discovery port and link them manually:

TTT_LAN_HOME=/tmp/ttt/a ttt-lan --name Alpha --discovery-port 37020 --port 37021 &
TTT_LAN_HOME=/tmp/ttt/b ttt-lan --name Bravo --discovery-port 37030 --port 37031 \
  --connect 127.0.0.1:37021 &

Without this, the second instance appears to start fine and simply never sees the first — which costs an afternoon if you do not know to look for it.

Layout

src/
  cli.js          argument parsing and bootstrap
  app.js          wiring: discovery + sessions + match + store, exposed as a controller
  engine.js       pure tic-tac-toe (no I/O, no async)
  elo.js          pure rating math
  framing.js      NDJSON accumulator for TCP — the module most worth trusting
  protocol.js     message schemas, validation, sanitization
  session.js      TCP server/client, rate limiting, ping/pong liveness
  discovery.js    UDP beacons, roster, TTL
  match.js        match state machine (host is authoritative)
  store.js        profile + append-only match log
  leaderboard.js  standings derived by replaying the log
  ui/             Ink components and the command parser

engine.js and elo.js import nothing from the project and have no side effects, so they are trivial to test and reason about. The host owns the board — guests send move intents and render whatever the host says the position is, which removes the entire class of desync bugs.

License

MIT