ModsCode / Claude Code Mods / Ranking / nba-scores-mod
nba-scores-mod
Today's NBA scoreboard above the Claude Code prompt. Pick your team and it gets its own totem and team color; scores update live while games are on. Live data from ESPN's public endpoints, no key and no account. Zero model tokens.
What it does
- Draws a band above the prompt
- Hooks session.start
- Keeps its own state between turns
- Calls the network
Good points
- Starts no processes
- Written in TypeScript
- Has a desktop-specific branch
- Handles errors
Code to start from
A starter ModsCode writes from the drawing it read, and the mod's own lines that draw, where there are any.
Starter (register.tsx)
import type { Register } from 'claude-code'
// Drawn like nba-scores-mod by Pu-Sung Chang (band), as ModsCode read it from the mod's code:
// https://modscode.com/claude-mods/mods/miami1124-nba-scores-mod/
// Written by ModsCode from that reading, not copied from the mod: use it as you like.
// Each value below stands for one the mod works out at run time; put in your own.
export const register: Register = on => {
// the band above the prompt; what else goes there is drawn under it
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const { Box, Text, Svg, Button, Select } = $.ui.resolve(e)
const svg = '…'
const alt = '…'
const value = '…'
const pinnedDate = '…'
const noGames = '…'
const stale = '…'
const configProblem = '…'
const revealAll = '…'
const pickTeam = '…'
const settings = '…'
const collapse = '…'
const label = '…'
const abbr = '…'
const abbr2 = '…'
const i = '…'
const periods = '…'
const periods2 = '…'
const total = '…'
const score = '…'
const score2 = '…'
const abbr3 = '…'
const shortName = '…'
const shortName2 = '…'
const shortName3 = '…'
const pts = '…'
const pick = '…'
const pick2 = '…'
const pick3 = '…'
const reb = '…'
const pick4 = '…'
const pick5 = '…'
const pick6 = '…'
const ast = '…'
const pick7 = '…'
const pick8 = '…'
const pick9 = '…'
const abbr4 = '…'
const shortName4 = '…'
const shortName5 = '…'
const shortName6 = '…'
const pts2 = '…'
const pick10 = '…'
const pick11 = '…'
const pick12 = '…'
const reb2 = '…'
const pick13 = '…'
const pick14 = '…'
const pick15 = '…'
const ast2 = '…'
const pick16 = '…'
const pick17 = '…'
const pick18 = '…'
return (
<Box flexDirection="column">
<Box flexDirection="row" width="100%" overflow="hidden">
<Box flexShrink={0}>
<Text dimColor>
────────────────────────
</Text>
</Box>
</Box>
<Box flexDirection="row" alignItems="center" gap={1} width="100%">
<Box flexDirection="row" alignItems="center" gap={1} flexShrink={0}>
<Box flexDirection="row" alignItems="center" gap={1}>
<Svg source={`${svg}`} alt={`${alt}`} width={16} height={16} />
<Box backgroundColor="#A32D2D" paddingX={1}>
<Text bold color="#FCEBEB">
NBA
</Text>
</Box>
</Box>
<Text dimColor>
{value}
</Text>
<Text color="#d97706">
{pinnedDate}
</Text>
<Button plain label={`‹`} />
</Box>
<Box flexDirection="row" alignItems="center" gap={1} flexGrow={1} flexShrink={1} overflow="hidden">
<Text dimColor>
{noGames}
</Text>
</Box>
<Box flexDirection="row" alignItems="center" gap={1} flexShrink={0}>
<Button plain label="›" />
<Text color="#d97706">
{stale}
</Text>
<Text color="#d97706">
{configProblem}
</Text>
<Button plain label={`${revealAll}`} />
<Select label={`${pickTeam}`} value="None" />
<Button plain label={`${settings}`} />
<Box>
<Button plain label={`${collapse}`} />
</Box>
</Box>
</Box>
<Box flexDirection="row" alignItems="center" gap={2} marginTop={1}>
<Select label={`${label}`} value={`${value}`} />
<Select label={`${label}`} value={`${value}`} />
<Select label={`${label}`} value={`${value}`} />
</Box>
<Box flexDirection="row">
<Box flexDirection="row">
<Box flexDirection="column" marginRight={2}>
<Text dimColor>
{" "}
</Text>
<Text>
{abbr}
</Text>
<Text>
{abbr2}
</Text>
</Box>
<Box flexDirection="column" alignItems="flex-end" marginRight={2}>
<Text dimColor>
{`Q${i}1`}
</Text>
<Text>
{periods}
</Text>
<Text>
{periods2}
</Text>
</Box>
<Box flexDirection="column" alignItems="flex-end" marginRight={2}>
<Text dimColor>
{total}
</Text>
<Text>
{score}
</Text>
<Text>
{score2}
</Text>
</Box>
</Box>
<Box flexDirection="row">
<Box flexDirection="row" marginLeft={3} flexShrink={0}>
<Box flexDirection="column" marginRight={2} flexShrink={0}>
<Text bold>
{abbr3}
</Text>
<Text dimColor>
{shortName}
</Text>
<Text dimColor>
{shortName2}
</Text>
<Text dimColor>
{shortName3}
</Text>
</Box>
<Box flexDirection="column" alignItems="flex-end" marginRight={2} flexShrink={0}>
<Text dimColor>
{pts}
</Text>
<Text>
{pick}
</Text>
<Text>
{pick2}
</Text>
<Text>
{pick3}
</Text>
</Box>
<Box flexDirection="column" alignItems="flex-end" marginRight={2} flexShrink={0}>
<Text dimColor>
{reb}
</Text>
<Text>
{pick4}
</Text>
<Text>
{pick5}
</Text>
<Text>
{pick6}
</Text>
</Box>
<Box flexDirection="column" alignItems="flex-end" marginRight={2} flexShrink={0}>
<Text dimColor>
{ast}
</Text>
<Text>
{pick7}
</Text>
<Text>
{pick8}
</Text>
<Text>
{pick9}
</Text>
</Box>
</Box>
<Box flexDirection="row" marginLeft={3} flexShrink={0}>
<Box flexDirection="column" marginRight={2} flexShrink={0}>
<Text bold>
{abbr4}
</Text>
<Text dimColor>
{shortName4}
</Text>
<Text dimColor>
{shortName5}
</Text>
<Text dimColor>
{shortName6}
</Text>
</Box>
<Box flexDirection="column" alignItems="flex-end" marginRight={2} flexShrink={0}>
<Text dimColor>
{pts2}
</Text>
<Text>
{pick10}
</Text>
<Text>
{pick11}
</Text>
<Text>
{pick12}
</Text>
</Box>
<Box flexDirection="column" alignItems="flex-end" marginRight={2} flexShrink={0}>
<Text dimColor>
{reb2}
</Text>
<Text>
{pick13}
</Text>
<Text>
{pick14}
</Text>
<Text>
{pick15}
</Text>
</Box>
<Box flexDirection="column" alignItems="flex-end" marginRight={2} flexShrink={0}>
<Text dimColor>
{ast2}
</Text>
<Text>
{pick16}
</Text>
<Text>
{pick17}
</Text>
<Text>
{pick18}
</Text>
</Box>
</Box>
</Box>
</Box>
{await next(e)}
</Box>
)
})
}
The mod's own code
Shown under the mod's licence (MIT); its text is below. Keep the notice if you copy these lines.
mods/nba-scores-mod/hooks/register.tsx lines 481–640 · Band
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
// 只有終端機和桌面版有這條區域。手機、VS Code、claude -p 都沒有
if (e.surface !== 'terminal' && e.surface !== 'desktop') return next(e)
// 問卷佔用同一塊位置時讓位,不要疊在一起
if (e.props.hasSurvey) return next(e)
const setCollapsed = (value: boolean) => {
collapsed = value
void $.store.set(COLLAPSED_KEY, value)
$.ui.invalidate('ui.render')
}
// ---- 桌面版:橫排小卡 ----
if (e.surface === 'desktop') {
const { Box, Button, Select, Svg, Text } = $.ui.resolve(e)
const nowMs = await $.clock.now()
// 別的 mod(例如用量帶)畫的東西一律放在自己上面。兩個 mod 誰包誰是看安裝
// 順序,不這樣固定的話,裝的順序不同上下就會顛倒。
const below = await next(e)
// 有畫圖騰的球隊:圖騰+隊色的徽章。其他球隊維持原本的紅徽章
const totem = totemFor(config.team)
const badge = (
<Box flexDirection="row" alignItems="center" gap={1}>
{totem ? <Svg source={totem.svg} alt={totem.alt} width={TOTEM_SIZE} height={TOTEM_SIZE} /> : null}
<Box backgroundColor={totem?.badgeBg ?? BADGE_BG} paddingX={1}>
<Text bold color={totem?.badgeFg ?? BADGE_FG}>
NBA
</Text>
</Box>
</Box>
)
const rule = (
// truncate 會在尾巴加「…」、wrap 會折成兩行。所以包一層不准縮的框:
// 字維持一整行、撐出去的部分由外框裁掉
<Box flexDirection="row" width="100%" overflow="hidden">
<Box flexShrink={0}>
<Text dimColor>{RULE}</Text>
</Box>
</Box>
)
if (!slate) {
return (
<Box flexDirection="column">
{below}
<Text dimColor>{lastError ? t.fetchFailed(lastError) : t.loading}</Text>
</Box>
)
}
// 收起來時只留「有沒有比賽正在打」這一件事,其他都不講
const live = slateMode(slate) === 'live'
const all = sortLiveFirst(slate.games, config.team)
if (collapsed) {
return (
<Box flexDirection="column">
{below}
{rule}
<Box flexDirection="row" alignItems="center" gap={1}>
{badge}
{live ? <Text color={ACCENT}>● LIVE</Text> : null}
<Box key="nba-scores-mod:expand-area">
<Button
key="nba-scores-mod:expand"
plain
label={t.expand}
hover={{ color: ACCENT }}
onPress={() => setCollapsed(false)}
/>
</Box>
</Box>
</Box>
)
}
// 場次變少時(隔天換一批)原本翻到的位置可能已經不存在
const start = Math.min(offset, Math.max(0, all.length - 1))
const visible = all.slice(start)
const hidden = all.filter(g => isMasked(g, spoilerFree(), revealed)).length
const age = Math.floor((nowMs - slate.fetchedAt) / 60_000)
const chip = (g: Game) => {
const mine = involves(g, config.team)
const masked = isMasked(g, spoilerFree(), revealed)
const started = g.state !== 'pre'
// 打完的比賽:贏的那邊亮、輸的那邊暗,一眼看得出誰贏。遮住時不能洩漏
const decided = g.state === 'post' && !masked && g.away.score !== g.home.score
const side = (which: Which) => {
const team = g[which]
const other = which === 'away' ? g.home : g.away
const lost = decided && team.score < other.score
const flash = flashOf(g.id, which, nowMs)
return (
<Box flexDirection="row" alignItems="center" gap={1}>
{mine ? <Text color={teamColor(team)}>▍</Text> : null}
<Text bold={mine} dimColor={lost}>
{team.abbr}
</Text>
{!started ? null : masked ? (
<Svg source={MASK_SVG} alt={t.maskedAlt} width={22} height={12} />
) : (
// 剛得分的前幾秒顯示「舊比分 +N」,之後才換成新比分。直接寫「新比分 +N」
// 會被讀成還要再加 N 分(100 +2 看起來像 102)。
<Text bold={!lost} dimColor={lost} color={flash && !flash.strong ? FLASH_COLOR : undefined}>
{String(flash?.strong ? team.score - flash.delta : team.score)}
</Text>
)}
{flash?.strong && !masked ? <Text color={FLASH_COLOR}>+{String(flash.delta)}</Text> : null}
</Box>
)
}
return (
<Box
key={g.id}
flexDirection="row"
alignItems="center"
gap={1}
paddingX={1}
flexShrink={0}
borderStyle="round"
borderDimColor={!mine && g.id !== openId}
>
{g.state === 'in' ? <Text color={ACCENT}>●</Text> : null}
{side('away')}
<Text dimColor>{started ? '–' : '@'}</Text>
{side('home')}
{g.state === 'in' ? (
<Text color={ACCENT}>{chipDetail(g, showingNext)}</Text>
) : g.state === 'pre' ? (
<Text color={UPCOMING}>{chipDetail(g, showingNext)}</Text>
) : (
<Text dimColor>{chipDetail(g, showingNext)}</Text>
)}
{started && !masked ? (
<Button
key={`nba-scores-mod:open:${g.id}`}
plain
label={g.id === openId ? '▴' : '▾'}
onPress={() => {
openId = g.id === openId ? undefined : g.id
$.ui.invalidate('ui.render')
if (openId !== undefined && (boxState.id !== openId || boxState.error !== undefined)) {
const picked = openId
void refreshBox($, boxState, picked, () => openId === picked, backoff)
}
}}
/>
) : null}
{masked ? (
<Button
key={`nba-scores-mod:reveal:${g.id}`}
plain
label={t.reveal}
onPress={() => {
revealed.add(g.id)
$.ui.invalidate('ui.render')
}}
/>
) : null}mods/nba-scores-mod/hooks/register.tsx lines 196–285 · Band
const renderDetail = (
ui: { Box: any; Text: any },
g: Game,
boxState: BoxState,
opts: { wide: boolean; gapTop: number; t: Strings },
) => {
const { Box, Text } = ui
const count = Math.max(g.away.periods.length, g.home.periods.length)
const awayLost = g.state === 'post' && g.away.score < g.home.score
const homeLost = g.state === 'post' && g.home.score < g.away.score
const column = (id: string, title: string, away: string, home: string, strong: boolean) => (
<Box key={id} flexDirection="column" alignItems="flex-end" marginRight={2}>
<Text dimColor>{title}</Text>
<Text bold={strong && !awayLost} dimColor={awayLost}>
{away}
</Text>
<Text bold={strong && !homeLost} dimColor={homeLost}>
{home}
</Text>
</Box>
)
// 右邊:兩隊各自得分前幾名。資料屬於別場(剛換場、還在抓)就先說讀取中
const mineBox = boxState.id === g.id
const teamBlock = (abbr: string) => {
const team = boxState.teams?.find(t => t.abbr === abbr)
if (!team) return null
const stat = (id: string, title: string, pick: (p: TeamBox['players'][number]) => number) => (
<Box key={id} flexDirection="column" alignItems="flex-end" marginRight={2} flexShrink={0}>
<Text dimColor>{title}</Text>
{team.players.map((p, i) => (
<Text key={`${id}${i}`}>{String(pick(p))}</Text>
))}
</Box>
)
return (
// 每一欄都不准縮:空間不夠時寧可右邊被裁掉,也不能讓名字折行害數字對不上
<Box key={abbr} flexDirection="row" marginLeft={3} flexShrink={0}>
<Box key={`names:${abbr}`} flexDirection="column" marginRight={2} flexShrink={0}>
<Text bold>{abbr}</Text>
{team.players.map((p, i) => (
<Text key={`n${i}`} dimColor>
{shortName(p.name)}
</Text>
))}
</Box>
{stat('pts', opts.t.pts, p => p.pts)}
{stat('reb', opts.t.reb, p => p.reb)}
{stat('ast', opts.t.ast, p => p.ast)}
</Box>
)
}
return (
<Box flexDirection={opts.wide ? 'row' : 'column'} marginTop={opts.gapTop}>
<Box flexDirection="row">
<Box flexDirection="column" marginRight={2}>
<Text dimColor> </Text>
<Text bold={!awayLost} dimColor={awayLost}>
{g.away.abbr}
</Text>
<Text bold={!homeLost} dimColor={homeLost}>
{g.home.abbr}
</Text>
</Box>
{Array.from({ length: count }, (_, i) =>
column(
`p${i}`,
i < 4 ? `Q${i + 1}` : `OT${i - 3}`,
String(g.away.periods[i] ?? '-'),
String(g.home.periods[i] ?? '-'),
false,
),
)}
{column('total', opts.t.total, String(g.away.score), String(g.home.score), true)}
</Box>
<Box flexDirection="row">
{!mineBox ? (
<Box marginLeft={3}>
<Text dimColor>{opts.t.boxLoading}</Text>
</Box>
) : boxState.error !== undefined ? (
<Box marginLeft={3}>
<Text color="#d97706">{opts.t.boxFailed(boxState.error)}</Text>
</Box>
) : (
[teamBlock(g.away.abbr), teamBlock(g.home.abbr)]
)}
</Box>
</Box>
)
}Licence text (MIT)
MIT License Copyright (c) 2026 Pu-Sung Chang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
How to build this: Band above the prompt, Keep state with $.store
What its code calls
networkwrites filesenv varsdrives Clauderemembers
What its code shows
Read from the source at commit dbfdbf5041bd on 2026-10-11. The first group decides whether a mod is listed; the rest is for you to weigh. A later commit is not read until the next crawl.
| Decides the listing: code you can check | |
|---|---|
| Entry read | Every module hooks/hooks.json names was read.mods/nba-scores-mod/hooks/register.tsx |
| Parses | Every file parses as TypeScript or JavaScript.8 files parsed |
| All its code is in the mod | Every import is a file of the mod or the engine's own module; no packages, no require.all its code is in the mod |
| Readable | No minified lines, no eval or new Function, no import of a computed path, no encoded blobs.no minified code, no eval, no encoded blobs |
| Calls in the open | Network and programs are reached as $.http.fetch and $.process.run in place, never passed around.every call outside goes through $ in the open |
| For you to weigh before installing | |
| Where it draws | The terminal, the desktop app, both, or nothing (hooks only: a guard or a prompt rewriter), as its code says. Shown, never a reason to leave a mod out.draws in the terminal and the desktop app, with its own branch for the desktop |
| Install line | Whether its repository's own .claude-plugin/marketplace.json lists it, so it installs from that marketplace with the lines on its page.listed in its repository's .claude-plugin/marketplace.json: installs from there |
| Programs on Windows | Whether every program it starts through $.process exists on Windows (tail, date, open, osascript, /usr/… do not).runs no program a Windows PC lacks |
| Network | Whether every fetch names its https host in the code.mods/nba-scores-mod/hooks/register.tsx:164 fetch(summaryUrl(gameId)); mods/nba-scores-mod/hooks/register.tsx:372 fetch(scoreboardUrl(day)); mods/nba-scores-mod/hooks/register.tsx:379 fetch(SCOREBOARD_URL) |
| Programs | Whether every program it starts is named in the code and is not a shell, an interpreter or a network tool.starts no programs |
| Reads and sends | Whether it both reads something of yours (files, environment variables, the transcript, settings) and reaches outside (the network, an MCP server, another session, telemetry).reads environment variables, files and reaches the network |
| Keys in the source | Whether the source holds what looks like an API key or a private key.no keys in the source |
| Licence | An open licence (MIT, Apache-2.0, BSD, ISC, Unlicense, 0BSD, CC0) with its text lets ModsCode show its code; otherwise the code is linked.MIT |
| Settings hooks | Hooks declared in hooks/hooks.json or plugin.json beside the module: a command (a shell when it has no args), an HTTP request, an MCP tool or a model prompt at an event. They run outside $.no settings hooks: its function hooks alone |
| Servers it ships | MCP servers (a program, or a URL; a headersHelper is a shell command; a .mcpb bundle is unpacked at run time), LSP servers, monitors (a shell command for the whole session) and bin/ (on the PATH of the Bash tool).ships no MCP or LSP server, no monitor, no bin/ |
| Skills, commands, agents | What its Markdown declares beyond text: allowed-tools granted for the turn, hooks added when a skill is invoked, a command run before Claude reads the skill.no skills, commands, agents or workflows |
| Tools and MCP | Whether every tool it calls through $.tool.call and $.mcp.call is named in the code, and none is a shell (Bash through the tool runs a shell without $.process).calls no tool, MCP server or command |
| Asks of the model | Completions it runs ($.model.complete, fork, classify), subagents it spawns, prompts it submits as you, and the tools, commands and agents it adds.asks nothing of the model |
| Events it hooks | By what a hook there can do: gates (refuse a tool call, a prompt, a setting, another plugin), shapes (what the model reads: the system prompt, tool descriptions, each request), reads (your input, the events of the settings hooks, every event with *), and the rest.also: session.start, ui.render (AbovePrompt) · timers: clock.every |
| Reads, by name | The environment variables and files it names, and whether it reads the transcript, settings.json, the prompt box or the mouse selection.env: HOME; files: a path computed at run time |
| Writes, by name | The files and environment variables it names, and whether it changes settings, the prompt box, the clipboard, the conversation (a row the model reads) or another session.files: a path computed at run time; settings (config.set) |
This is a reading of the code text, not a security review: it does not run the mod, and it cannot see what a program the mod starts does. The check before installing is yours. How mods are chosen
Install it
Check it yourself before you install it. Mods run unsandboxed, with your permissions, and ModsCode does not vouch for what this code does. Read its code (or run claude plugin validate . in its folder), weigh what it reaches, and install the commit you read.
From its marketplace
Its repository is its marketplace. Add that marketplace (in the terminal with these commands, or in the desktop app's plugin settings) and install the mod from it:
/plugin marketplace add miami1124/pusung-mods/plugin install nba-scores-mod@pusung-mods/reload-pluginsThis installs the marketplace's latest commit, which may not be the one read here (dbfdbf5041bd). Compare them before you install. /reload-plugins loads it into a session that is already open.
Try the commit ModsCode read
No marketplace needed. In a terminal, clone the repository, check out the commit read, validate the mod, and load it for one session:
git clone https://github.com/miami1124/pusung-modsgit -C pusung-mods checkout dbfdbf5041bd0e924b67e5750fa69dd052abf678claude plugin validate pusung-mods/mods/nba-scores-modclaude --plugin-dir pusung-mods/mods/nba-scores-modThe validate line lists the events the mod hooks and what it asks Claude Code to do, without running it. The last line loads the mod for this session only and writes nothing to your settings. If the author has rewritten history, the commit may no longer exist.
Ask Claude about it
With the ModsCode connector added, Claude can read this mod's code and what it reaches, and build on them. Ask it like this:
Read the ModsCode mod "miami1124-nba-scores-mod" with its code and make me one like itSearch them from Claude: the MCP connector
Similar mods
- cache-timerbyheaven★ 2
- cockpitSyed Muhammad Hassaan★ 1
- skinshellosverre★ 1
- cache-barTsungYing★ 0
- dashDevOhMyCode★ 0
- git-diff-timelineliawzishen★ 0
Badge for your README
If this is your mod, paste this into your README. It links to this page.
[](https://modscode.com/claude-mods/mods/miami1124-nba-scores-mod/)