sportsing 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +167 -12
  2. package/package.json +6 -2
  3. package/src/alerts.ts +47 -0
  4. package/src/api.ts +1 -1
  5. package/src/click-to-watch.ts +39 -0
  6. package/src/commands/_lib.ts +28 -12
  7. package/src/commands/analyze.ts +2 -2
  8. package/src/commands/ask.ts +1 -1
  9. package/src/commands/cross.ts +354 -0
  10. package/src/commands/fav.ts +19 -16
  11. package/src/commands/league-ai.ts +201 -0
  12. package/src/commands/league-bracket.ts +150 -0
  13. package/src/commands/league.ts +839 -0
  14. package/src/commands/live.ts +57 -42
  15. package/src/commands/me.ts +1 -1
  16. package/src/commands/predict.ts +2 -2
  17. package/src/commands/scorers.ts +1 -1
  18. package/src/commands/setup.ts +3 -3
  19. package/src/commands/subscriptions.ts +66 -0
  20. package/src/commands/table.ts +1 -1
  21. package/src/commands/upgrade.ts +137 -0
  22. package/src/commands/watch.ts +19 -11
  23. package/src/config.ts +166 -28
  24. package/src/espn.ts +606 -33
  25. package/src/format.ts +87 -0
  26. package/src/game-events.ts +239 -0
  27. package/src/game.ts +51 -0
  28. package/src/index.ts +41 -16
  29. package/src/league-ai.ts +359 -0
  30. package/src/league-alerts.ts +139 -0
  31. package/src/league-detect.ts +104 -0
  32. package/src/league-overlay.ts +351 -0
  33. package/src/league-panels.ts +159 -0
  34. package/src/overlay.ts +96 -51
  35. package/src/playoff-bracket.ts +384 -0
  36. package/src/recap.ts +37 -8
  37. package/src/season.ts +182 -0
  38. package/src/sports/fifa.ts +9 -4
  39. package/src/sports/nba.ts +20 -0
  40. package/src/sports/nhl.ts +20 -0
  41. package/src/standings.ts +130 -0
  42. package/src/stream.ts +49 -8
  43. package/src/upgrade.ts +182 -0
  44. package/src/watch-route.ts +95 -0
  45. package/src/watchability.ts +183 -0
package/README.md CHANGED
@@ -1,19 +1,41 @@
1
1
  # ⚽ sportsing
2
2
 
3
- Sports in your terminal — the FIFA World Cup 2026 schedule, favorites, live
4
- scores, **ambient fav-alerts**, browser streaming, highlights, stats, and AI
5
- analysis. Runs on [Bun](https://bun.sh); its one runtime dependency,
3
+ Sports in your terminal — the FIFA World Cup 2026, the **NBA**, and the
4
+ **NHL**: schedules, favorites across every sport, live scores, standings, playoff
5
+ brackets, **where to watch**, **ambient fav-alerts**, browser streaming,
6
+ highlights, stats, and AI analysis. Runs on [Bun](https://bun.sh); its one runtime dependency,
6
7
  [ui-leaf](https://www.npmjs.com/package/@openthink/ui-leaf), powers the
7
8
  browser-streaming window and is installed (with its native binary) automatically.
8
9
 
9
10
  ```
10
11
  sportsing fifa today
11
- sportsing fifa next --team USA
12
- sportsing today # the `fifa` prefix is optional during the World Cup
12
+ sportsing nba schedule --team UTAH
13
+ sportsing nhl next --team UTAH
14
+ sportsing today # your favorite teams across every sport (fifa, nba, nhl)
13
15
  ```
14
16
 
17
+ > **Upgrading from 0.1.x?** Bare commands other than `today` / `next` / `me` now
18
+ > need a sport prefix (`sportsing serve` → `sportsing fifa serve`), and favorites
19
+ > are now stored per sport. See **Upgrading to 0.2.0** below.
20
+
15
21
  ## Install
16
22
 
23
+ From npm (needs [Bun](https://bun.sh) on your `PATH` — the CLI runs on it):
24
+
25
+ ```sh
26
+ bun install -g sportsing # or: npm install -g sportsing
27
+ sportsing --version
28
+ ```
29
+
30
+ To update later, run `sportsing upgrade`. It checks npm for the latest release
31
+ and reinstalls it with whichever of npm or bun installed your copy.
32
+ `sportsing upgrade --check` only compares your version with the latest and
33
+ installs nothing. A source checkout, a compiled binary or an `npx`/`bunx`
34
+ run isn't updated in place. For those, the command prints how to update
35
+ instead.
36
+
37
+ From source:
38
+
17
39
  ```sh
18
40
  bun install # deps: ui-leaf (+ its browser binary) and dev types
19
41
  bun run build # compiles a standalone binary → dist/sportsing
@@ -39,7 +61,8 @@ scores or tables).
39
61
 
40
62
  ## Commands
41
63
 
42
- `sportsing fifa <command>` (or just `sportsing <command>` during the Cup):
64
+ `sportsing fifa <command>` (bare `sportsing today|next|me` cover your favorites
65
+ across every sport — see **Your teams, every sport** below):
43
66
 
44
67
  | Command | What it does |
45
68
  |---|---|
@@ -58,7 +81,121 @@ scores or tables).
58
81
  Run `sportsing fifa help` for the full list (`serve` and `ask` are the AI-bus
59
82
  commands; `ask` is low-level plumbing that `serve` wraps).
60
83
 
61
- ## Watch
84
+ ## NBA
85
+
86
+ `sportsing nba <command>` — the NBA via ESPN's free API (no key), preseason
87
+ through playoffs. Times are local; preseason games are tagged `PRE`, postseason
88
+ (incl. play-in) `POST`.
89
+
90
+ | Command | What it does |
91
+ |---|---|
92
+ | `today [--tomorrow\|--yesterday\|--offset N]` | One day's games |
93
+ | `next` | Next game + countdown |
94
+ | `schedule` | A team's whole season by day (league-wide: next 7 days) |
95
+ | `results` | Finished games, newest first (league-wide: last 7 days) |
96
+ | `standings [--conference X] [--division X]` | Regular-season standings, favorites marked ★ |
97
+ | `season [team]` | Your favorites' season: record, home/away splits, playoff race |
98
+ | `bracket [--season YYYY]` | Playoff bracket (projected before the postseason) |
99
+ | `fav [add\|rm\|list]` | Manage NBA favorites (`fav add UTAH`, `fav add Jazz`) |
100
+ | `watch [team] [--wait] [--provider X] [--url L] [--smoke]` | Open the game where you can watch it — see **Where to watch** below |
101
+ | `live [--notify [--quiet]]` | Auto-refreshing live board; `--notify` alerts for favorites — see **Live fav-alerts** below |
102
+ | `analyze <team> [team]` | AI read of the live or latest game: shooting, boards, turnovers, leaders (answered by `serve`; `--prompt` prints it) |
103
+ | `predict <team> [team]` | AI prediction for the next game from both teams' recent form |
104
+ | `recap <team> [team]` | AI "here's what you missed": lead changes, runs, close-late baskets, quarter scores |
105
+
106
+ `--team X` (abbreviation or name; NBA.com codes like `UTA` work too) picks one
107
+ team for `today` / `next` / `schedule` / `results`; `--mine` limits them to your
108
+ NBA favorites. NBA favorites are separate from FIFA ones.
109
+
110
+ `today` / `next` / `schedule` show a **WATCH** column once you've set your
111
+ subscriptions (see **Where to watch**). `sportsing nba watch` opens Jazz games on
112
+ Fubo by default.
113
+
114
+ `sportsing nba watch [team] --overlay` opens Fubo's NBA hub with a live-stats
115
+ overlay (same gear / settings / spoiler-delay as the FIFA one) that follows the
116
+ game you open, recognized from the page title's English team names. NBA panels:
117
+ score & clock, FG% / 3P%, leaders (PTS / REB / AST), fouls, timeouts. On an NHL
118
+ game page (`sportsing nhl watch --provider fubo --overlay`): score & clock,
119
+ shots on goal, power plays, faceoff %, goalie saves. Both have **Get caught up**
120
+ (a recap up to your delayed stream's moment, via `/loop sportsing serve`). Panel choices are saved per
121
+ provider and sport (`overlayPanels["fubo:nba"]`). `--overlay --smoke` checks the
122
+ overlay renders, then tears the window down.
123
+
124
+ ## NHL
125
+
126
+ `sportsing nhl <command>` — the same commands as `nba`, for the NHL (ESPN, no
127
+ key), preseason through playoffs, tagged `PRE` / `POST` the same way. Live and
128
+ final status uses hockey periods: `12:34 - 2nd`, `End of 3rd`, `OT`, `SO`,
129
+ `Final/OT`, `Final/SO`.
130
+
131
+ `--team X` takes an abbreviation or name — `UTAH`, `UTA`, `Mammoth`, and
132
+ NHL.com codes like `TBL` / `LAK` all work. NHL favorites (`fav add UTAH`) are
133
+ separate from NBA and FIFA ones, so the Jazz and the Mammoth can both be `UTAH`.
134
+
135
+ `standings`, `season`, `bracket`, `watch`, and `analyze` / `predict` / `recap`
136
+ work exactly as for the NBA — the AI prompts speak hockey (goaltending, special
137
+ teams, every goal and penalty). There's
138
+ no default NHL streaming provider: `nhl watch` follows **Where to watch** — an
139
+ over-the-air game prints its channel (e.g. Utah 16) and opens nothing.
140
+
141
+ ## Where to watch (NBA / NHL)
142
+
143
+ Tell sportsing what you can watch with and where you live, once — it applies to
144
+ every sport:
145
+
146
+ ```sh
147
+ sportsing subscriptions set fubo nba-league-pass local-ota # whichever you have (or `none`)
148
+ sportsing subscriptions market utah # home market (default: utah)
149
+ sportsing subscriptions # show them (alias: subs)
150
+ ```
151
+
152
+ From each game's broadcasts (ESPN), sportsing then works out how *you* can watch
153
+ it:
154
+
155
+ - **WATCH column** in `today` / `next` / `schedule`: the service (`Fubo`,
156
+ `NBA League Pass`) or over-the-air channel, `✗` if you can't watch it, `?` if it
157
+ can't tell. With no subscriptions set, a one-line hint replaces the column.
158
+ - **`<sport> watch`** acts on the same answer: opens Fubo or League Pass, prints
159
+ the channel for an over-the-air-only game (nothing to open; exits 0), or says
160
+ why a game isn't watchable (exits 1). `--provider` / `--url` override it.
161
+ - **League Pass** covers out-of-market NBA games only — blacked out when a
162
+ home-market team plays or the game has a national broadcast. National
163
+ exclusives you don't carry are flagged.
164
+
165
+ ## Your teams, every sport
166
+
167
+ Bare `today`, `next`, and `me` (no sport prefix) aggregate your favorite teams
168
+ across every sport, each row tagged with its sport:
169
+
170
+ ```sh
171
+ sportsing nba fav add UTAH && sportsing nhl fav add UTAH
172
+ sportsing today # the Jazz and the Mammoth today (--tomorrow, --yesterday, --offset N)
173
+ sportsing next # each favorite's next game + countdown, soonest first
174
+ sportsing me # last result + next game per favorite team
175
+ ```
176
+
177
+ Bare `live --notify` alerts across every sport too (see **Live fav-alerts**).
178
+ Every other command needs a sport — a bare one (e.g. `sportsing standings`)
179
+ prints the sports that have it and exits non-zero.
180
+
181
+ Favorites are stored per sport as `<sport>:<team>` (`nba:UTAH`, `nhl:UTAH`,
182
+ `fifa:USA`) in `~/.config/sportsing/config.json`; add and remove them with each
183
+ sport's `fav` command.
184
+
185
+ ## Upgrading to 0.2.0
186
+
187
+ - **Breaking — bare commands need a sport.** In 0.1.x every bare command ran as a
188
+ FIFA command (`sportsing serve` meant `sportsing fifa serve`). Now only `today`,
189
+ `next`, and `me` work bare (across every sport); everything else needs its
190
+ prefix: `sportsing fifa serve`, `sportsing fifa live --notify`,
191
+ `sportsing fifa watch USA`, `sportsing fifa fav add USA`, and so on. Update any
192
+ scripts, aliases, or launchd jobs that call a bare command.
193
+ - **Favorites are scoped by sport.** Existing unprefixed favorites (`"USA"`) are
194
+ read as FIFA favorites (`fifa:USA`) — nothing is lost. The file is rewritten in
195
+ the prefixed form the next time you add or remove a favorite. Downgrading to
196
+ 0.1.x after that would see `fifa:USA` as a team name, so don't mix versions.
197
+
198
+ ## Watch (FIFA)
62
199
 
63
200
  `sportsing fifa watch [team] [team]` opens the broadcast in your own browser
64
201
  (your real Chrome, via [ui-leaf](https://www.npmjs.com/package/@openthink/ui-leaf)):
@@ -104,6 +241,8 @@ Turn `live` into an ambient alerter that pings you when your favorite teams play
104
241
  sportsing fifa fav add USA # set up favorites first
105
242
  sportsing fifa live --notify # live board + OS notifications
106
243
  sportsing fifa live --notify --quiet & # headless: alerts only, backgroundable
244
+ sportsing nba live --notify # same for the NBA (and `nhl live`)
245
+ sportsing live --notify --quiet & # one alerter for every sport with a favorite
107
246
  ```
108
247
 
109
248
  Each refresh diffs the latest scores against the previous tick and raises an OS
@@ -115,6 +254,20 @@ full-time alerts exactly once:
115
254
  - **Goal** — the scorer and the resulting scoreline (with a sound).
116
255
  - **Full time** — the final scoreline.
117
256
 
257
+ NBA and NHL alert on what matters per sport, not every basket:
258
+
259
+ - **NHL** — puck drop, every goal (with a sound), end of each period, overtime /
260
+ shootout, final.
261
+ - **NBA** — tip-off, lead changes, a close finish (≤ 5 points with ≤ 5:00 left in
262
+ the 4th or OT, once per game), final.
263
+ - **Tip-off / puck drop** are click-to-watch, routed like `<sport> watch` (see
264
+ **Where to watch**): a game on Fubo or League Pass opens on click; an
265
+ over-the-air-only game names its channel instead (nothing to click into).
266
+
267
+ Bare `sportsing live --notify` polls every sport you have a favorite in from one
268
+ process; a sport that can't load (e.g. FIFA without an API key) is reported and
269
+ the rest keep alerting. Without `--quiet` it logs each alert as a line.
270
+
118
271
  Flags:
119
272
 
120
273
  - **`--notify`** — fire the alerts. With no favorites set, it warns and runs the
@@ -126,7 +279,7 @@ Flags:
126
279
 
127
280
  ### Click-to-watch requires `terminal-notifier`
128
281
 
129
- Clickable kickoff notifications use
282
+ Clickable kickoff / tip-off / puck-drop notifications use
130
283
  [`terminal-notifier`](https://github.com/julienXX/terminal-notifier) (macOS):
131
284
 
132
285
  ```sh
@@ -136,7 +289,8 @@ brew install terminal-notifier
136
289
  Notifications **degrade gracefully** when it's absent: on macOS they fall back to
137
290
  `osascript` (plain banner, no click action); on Linux to `notify-send`; otherwise
138
291
  to a terminal bell. Nothing errors — you just don't get the one-click-to-watch
139
- behavior without `terminal-notifier`.
292
+ behavior without `terminal-notifier`. A click opens a Terminal window running
293
+ `watch` (Ctrl-C or close the stream window to stop).
140
294
 
141
295
  ## AI (analyze / predict / overlay "Ask Claude" + "Get caught up")
142
296
 
@@ -166,18 +320,19 @@ prints this recipe; `sportsing fifa` and the watch nag point at it.
166
320
  ### Low-level primitive — `serve`
167
321
 
168
322
  `sportsing fifa serve` is the bare answerer loop (it powers `analyze` / `predict`
169
- too). `agent-setup` supersedes the old manual two-step for the agent-driven flow,
323
+ / `recap` too — for every sport: the bus is shared, so one `fifa serve` answers
324
+ NBA and NHL questions as well). `agent-setup` supersedes the old manual two-step for the agent-driven flow,
170
325
  but `serve` remains the primitive if you want to compose it yourself:
171
326
 
172
327
  ```sh
173
328
  sportsing fifa watch --wait # (backgrounded) opens the game when it's live
174
- /loop sportsing serve # answer-only loop — no watch supervision
329
+ /loop sportsing fifa serve # answer-only loop — no watch supervision
175
330
  ```
176
331
 
177
332
  > **Run the answerer in a minimal-tool session.** Whether via `agent-setup` or
178
333
  > `serve`, the loop reads **untrusted** text (viewer questions + raw API fields)
179
334
  > into a tool-capable Claude session. Give that session no MCP/file tools and only
180
- > the `sportsing ask --reply` Bash capability, so a prompt-injection in a question
335
+ > the `sportsing fifa ask --reply` Bash capability, so a prompt-injection in a question
181
336
  > can't reach anything dangerous. `serve` prints this reminder each tick.
182
337
 
183
338
  ## Development
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sportsing",
3
- "version": "0.1.2",
4
- "description": "Sportsing — the FIFA World Cup 2026 in your terminal: schedule, favorites, live scores, ambient fav-alerts, browser streaming, highlights, stats, and AI analysis.",
3
+ "version": "0.2.1",
4
+ "description": "Sportsing — sports in your terminal: the FIFA World Cup 2026, the NBA, and the NHL. Schedules, favorites across every sport, live scores, standings, where-to-watch, browser streaming, highlights, stats, and AI analysis.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "sportsing": "src/index.ts"
@@ -34,6 +34,10 @@
34
34
  "world-cup",
35
35
  "wc2026",
36
36
  "soccer",
37
+ "nba",
38
+ "nhl",
39
+ "basketball",
40
+ "hockey",
37
41
  "football",
38
42
  "cli",
39
43
  "terminal",
package/src/alerts.ts ADDED
@@ -0,0 +1,47 @@
1
+ // Ambient favorite-team alerts, sport-neutral. Each sport turns successive
2
+ // score polls into `Alert`s (FIFA: events.ts; NBA/NHL: league-alerts.ts); an
3
+ // `Alerter` wraps that with its own fetch so bare `sportsing live --notify`
4
+ // can poll every sport with a favorite from one process.
5
+
6
+ import { notify, type NotifyOptions } from "./notify.ts";
7
+
8
+ /** How often `live` refreshes and alerters poll. */
9
+ export const LIVE_REFRESH_MS = 60_000;
10
+
11
+ /** One OS notification to raise. */
12
+ export interface Alert {
13
+ title: string;
14
+ body: string;
15
+ options: NotifyOptions;
16
+ }
17
+
18
+ /** One sport's favorite-team alerter. */
19
+ export interface Alerter {
20
+ /** Sport label, e.g. "NBA". */
21
+ label: string;
22
+ /** The favorite teams it alerts for, for the startup line. */
23
+ teams: string[];
24
+ /** Fetch the latest scores and return the new alerts since the previous
25
+ * poll. The first poll only sets the baseline (a game already under way
26
+ * when you start raises no start alert). */
27
+ poll: () => Promise<Alert[]>;
28
+ }
29
+
30
+ /** Raise an alert as a desktop notification. */
31
+ export function raise(a: Alert): void {
32
+ notify(a.title, a.body, a.options);
33
+ }
34
+
35
+ /**
36
+ * Poll every alerter once, concurrently. A sport whose poll fails is handed to
37
+ * `onError` and contributes nothing this round; the others' alerts still come
38
+ * back, in alerter order.
39
+ */
40
+ export async function pollAlerters(alerters: Alerter[], onError: (a: Alerter, e: unknown) => void): Promise<Alert[]> {
41
+ const settled = await Promise.allSettled(alerters.map((a) => a.poll()));
42
+ return settled.flatMap((r, i) => {
43
+ if (r.status === "fulfilled") return r.value;
44
+ onError(alerters[i]!, r.reason);
45
+ return [];
46
+ });
47
+ }
package/src/api.ts CHANGED
@@ -131,7 +131,7 @@ export async function getOpenFootballMatches(): Promise<Match[]> {
131
131
  if (!res.ok)
132
132
  throw new ApiError(
133
133
  res.status,
134
- "Could not fetch the offline fixture schedule — network unavailable. Run `sportsing setup` to add a free API key for live data.",
134
+ "Could not fetch the offline fixture schedule — network unavailable. Run `sportsing fifa setup` to add a free API key for live data.",
135
135
  );
136
136
  const data = (await res.json()) as any;
137
137
  const out: Match[] = [];
@@ -0,0 +1,39 @@
1
+ // Click-to-watch for start-of-game notifications: the shell command a
2
+ // notification runs when clicked (terminal-notifier `-execute`).
3
+ //
4
+ // `watch` is interactive — it blocks until its stream window is closed, and
5
+ // refuses to run without a controlling TTY. terminal-notifier runs `-execute`
6
+ // through /bin/sh with no TTY, so the click opens a Terminal window that runs
7
+ // `watch` there (a TTY, and a Ctrl-C to stop it).
8
+
9
+ /** POSIX single-quote a string so it's safe as one shell argument. */
10
+ export function shQuote(s: string): string {
11
+ return `'${s.replace(/'/g, `'\\''`)}'`;
12
+ }
13
+
14
+ /**
15
+ * argv that re-runs this sportsing: the binary itself when compiled
16
+ * (`bun build --compile`; the entry lives in Bun's virtual /$bunfs), else the
17
+ * Bun runtime plus the entry script (the npm install runs src/index.ts).
18
+ */
19
+ export function selfInvocation(main = Bun.main, execPath = process.execPath): string[] {
20
+ const compiled = main.startsWith("/$bunfs/") || main.includes("~BUN");
21
+ return compiled ? [execPath] : [execPath, main];
22
+ }
23
+
24
+ /** `<exe…> <sport> watch <team>`, shell-quoted. */
25
+ export function watchCommand(exe: string[], sport: string, team: string): string {
26
+ return `${exe.map(shQuote).join(" ")} ${sport} watch ${shQuote(team)}`;
27
+ }
28
+
29
+ /** Escape a string for an AppleScript double-quoted literal. */
30
+ function osaString(s: string): string {
31
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
32
+ }
33
+
34
+ /** A shell command that runs `cmd` in a new macOS Terminal window and brings
35
+ * Terminal to the front. */
36
+ export function inTerminal(cmd: string): string {
37
+ const script = (line: string) => `-e ${shQuote(`tell application "Terminal" to ${line}`)}`;
38
+ return `osascript ${script(`do script ${osaString(cmd)}`)} ${script("activate")}`;
39
+ }
@@ -43,7 +43,7 @@ export async function withFallback<T>(
43
43
  console.error(
44
44
  c.yellow(
45
45
  "No API key set — showing the offline schedule (no live scores/tables).\n" +
46
- "Run `sportsing setup` for live data.",
46
+ "Run `sportsing fifa setup` for live data.",
47
47
  ) + "\n",
48
48
  );
49
49
  const matches = await getOpenFootballMatches();
@@ -70,19 +70,35 @@ export function getFlag(args: string[], flag: string): string | null {
70
70
  }
71
71
 
72
72
  /**
73
- * If `--mine` is present in args, narrow `matches` to games involving a favorite
74
- * team. Returns the literal `"no-favorites"` when `--mine` was asked for but no
75
- * favorites are set, so the caller can show a helpful hint. Without `--mine`,
76
- * returns the list unchanged.
73
+ * The favorites `--mine` should filter by for `sport`: null when `--mine` is
74
+ * absent, the literal `"no-favorites"` when it was asked for but that sport has
75
+ * none set (so the caller can show a hint), else that sport's favorite names.
76
+ */
77
+ export async function mineFavorites(args: string[], sport: string): Promise<string[] | null | "no-favorites"> {
78
+ if (!args.includes("--mine")) return null;
79
+ const favs = await getFavorites(sport);
80
+ return favs.length === 0 ? "no-favorites" : favs;
81
+ }
82
+
83
+ /**
84
+ * If `--mine` is present in args, narrow football-data `matches` (the FIFA
85
+ * shape) to games involving a FIFA favorite. Returns `"no-favorites"` when
86
+ * `--mine` was asked for but none are set. Without `--mine`, returns the list
87
+ * unchanged. ESPN-league commands filter `Game`s by team id instead (league.ts).
77
88
  */
78
89
  export async function applyMine(matches: Match[], args: string[]): Promise<Match[] | "no-favorites"> {
79
- if (!args.includes("--mine")) return matches;
80
- const favs = (await getFavorites()).map((f) => f.toLowerCase());
81
- if (favs.length === 0) return "no-favorites";
82
- return matches.filter((m) => favs.some((n) => matchHasTeam(m, n)));
90
+ const favs = await mineFavorites(args, "fifa");
91
+ if (favs === null) return matches;
92
+ if (favs === "no-favorites") return favs;
93
+ const needles = favs.map((f) => f.toLowerCase());
94
+ return matches.filter((m) => needles.some((n) => matchHasTeam(m, n)));
83
95
  }
84
96
 
85
- /** Shared message for `--mine` with no favorites configured. */
86
- export function noFavoritesHint(): void {
87
- console.log(c.dim("No favorite teams yet — add one with ") + c.bold("sportsing fifa fav add USA"));
97
+ /** Example team for "add a favorite" hints, per sport. */
98
+ export const EXAMPLE_TEAM: Record<string, string> = { fifa: "USA", nba: "UTAH", nhl: "UTAH" };
99
+
100
+ /** Shared message for `--mine` with no favorites configured for `sport`. */
101
+ export function noFavoritesHint(sport = "fifa"): void {
102
+ const example = EXAMPLE_TEAM[sport] ?? "<team>";
103
+ console.log(c.dim("No favorite teams yet — add one with ") + c.bold(`sportsing ${sport} fav add ${example}`));
88
104
  }
@@ -36,7 +36,7 @@ export async function analyze(args: string[]) {
36
36
  }
37
37
 
38
38
  process.stderr.write(c.dim("Posted to the ask bus — waiting for your Claude agent to answer…\n"));
39
- process.stderr.write(c.dim("(keep one serving: /loop sportsing serve)\n"));
39
+ process.stderr.write(c.dim("(keep one serving: /loop sportsing fifa serve)\n"));
40
40
  const id = await postQuestion({
41
41
  source: "analyze",
42
42
  question: prompt,
@@ -47,7 +47,7 @@ export async function analyze(args: string[]) {
47
47
  const analysis = await waitForAnswer(id, 180_000);
48
48
  if (analysis === null) {
49
49
  console.error(c.yellow("No Claude agent answered within 3 minutes."));
50
- console.error(c.dim("Start a serving agent in another Claude session, then retry: /loop sportsing serve"));
50
+ console.error(c.dim("Start a serving agent in another Claude session, then retry: /loop sportsing fifa serve"));
51
51
  console.error(c.dim("Or run with --prompt to get the prompt and analyze elsewhere."));
52
52
  process.exitCode = 1;
53
53
  return;
@@ -26,7 +26,7 @@ export async function ask(args: string[]): Promise<void> {
26
26
  // self-contained PROMPT (not data): it blocks for the next pending question and
27
27
  // emits instructions telling the reading agent to answer it and run the exact
28
28
  // `ask --reply` command. Designed to be dropped into a Claude session as
29
- // `/loop sportsing serve` — each tick serves one question; the model IS the
29
+ // `/loop sportsing fifa serve` — each tick serves one question; the model IS the
30
30
  // answerer, so no `claude -p` is ever spawned.
31
31
  export async function serve(args: string[]): Promise<void> {
32
32
  const waitSec = numFlag(args, "--wait") ?? 60;