sportsing 0.1.1 → 0.2.0
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.
- package/README.md +173 -16
- package/package.json +12 -2
- package/src/alerts.ts +47 -0
- package/src/api.ts +1 -1
- package/src/click-to-watch.ts +39 -0
- package/src/commands/_lib.ts +28 -12
- package/src/commands/analyze.ts +2 -2
- package/src/commands/ask.ts +1 -1
- package/src/commands/cross.ts +354 -0
- package/src/commands/fav.ts +19 -16
- package/src/commands/league-ai.ts +201 -0
- package/src/commands/league-bracket.ts +150 -0
- package/src/commands/league.ts +839 -0
- package/src/commands/live.ts +57 -42
- package/src/commands/me.ts +1 -1
- package/src/commands/predict.ts +2 -2
- package/src/commands/scorers.ts +1 -1
- package/src/commands/setup.ts +3 -3
- package/src/commands/subscriptions.ts +66 -0
- package/src/commands/table.ts +1 -1
- package/src/commands/watch.ts +19 -11
- package/src/config.ts +166 -28
- package/src/espn.ts +606 -33
- package/src/format.ts +87 -0
- package/src/game-events.ts +239 -0
- package/src/game.ts +51 -0
- package/src/index.ts +35 -16
- package/src/league-ai.ts +359 -0
- package/src/league-alerts.ts +139 -0
- package/src/league-detect.ts +104 -0
- package/src/league-overlay.ts +351 -0
- package/src/league-panels.ts +159 -0
- package/src/overlay.ts +96 -51
- package/src/playoff-bracket.ts +384 -0
- package/src/recap.ts +37 -8
- package/src/season.ts +182 -0
- package/src/sports/fifa.ts +9 -4
- package/src/sports/nba.ts +20 -0
- package/src/sports/nhl.ts +20 -0
- package/src/standings.ts +130 -0
- package/src/stream.ts +106 -40
- package/src/watch-route.ts +95 -0
- package/src/watchability.ts +183 -0
package/README.md
CHANGED
|
@@ -1,25 +1,49 @@
|
|
|
1
1
|
# ⚽ sportsing
|
|
2
2
|
|
|
3
|
-
Sports in your terminal — the FIFA World Cup 2026
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,
|
|
7
|
+
[ui-leaf](https://www.npmjs.com/package/@openthink/ui-leaf), powers the
|
|
8
|
+
browser-streaming window and is installed (with its native binary) automatically.
|
|
7
9
|
|
|
8
10
|
```
|
|
9
11
|
sportsing fifa today
|
|
10
|
-
sportsing
|
|
11
|
-
sportsing
|
|
12
|
+
sportsing nba schedule --team UTAH
|
|
13
|
+
sportsing nhl next --team UTAH
|
|
14
|
+
sportsing today # your favorite teams across every sport (fifa, nba, nhl)
|
|
12
15
|
```
|
|
13
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
|
+
|
|
14
21
|
## Install
|
|
15
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
|
+
From source:
|
|
31
|
+
|
|
16
32
|
```sh
|
|
17
|
-
bun install #
|
|
33
|
+
bun install # deps: ui-leaf (+ its browser binary) and dev types
|
|
18
34
|
bun run build # compiles a standalone binary → dist/sportsing
|
|
19
35
|
```
|
|
20
36
|
|
|
21
|
-
Then run `./dist/sportsing …`, or put it on your `PATH`.
|
|
22
|
-
|
|
37
|
+
Then run `./dist/sportsing …`, or put it on your `PATH`.
|
|
38
|
+
|
|
39
|
+
> **Streaming from the compiled binary.** `bun run build` produces a single
|
|
40
|
+
> file, but it can't embed ui-leaf's ~64MB native browser binary. Running from
|
|
41
|
+
> source (`bun run src/index.ts …`) or installing from npm resolves that binary
|
|
42
|
+
> automatically; for the standalone `dist/sportsing`, point streaming at an
|
|
43
|
+
> installed copy with `UI_LEAF_BINARY_PATH=/path/to/ui-leaf-bin`. Everything
|
|
44
|
+
> except the `watch` browser window works with no binary present.
|
|
45
|
+
|
|
46
|
+
For live data, add a free [football-data.org](https://www.football-data.org) API key:
|
|
23
47
|
|
|
24
48
|
```sh
|
|
25
49
|
sportsing fifa setup # paste your key (or set FOOTBALL_DATA_API_KEY)
|
|
@@ -30,7 +54,8 @@ scores or tables).
|
|
|
30
54
|
|
|
31
55
|
## Commands
|
|
32
56
|
|
|
33
|
-
`sportsing fifa <command>` (
|
|
57
|
+
`sportsing fifa <command>` (bare `sportsing today|next|me` cover your favorites
|
|
58
|
+
across every sport — see **Your teams, every sport** below):
|
|
34
59
|
|
|
35
60
|
| Command | What it does |
|
|
36
61
|
|---|---|
|
|
@@ -49,7 +74,121 @@ scores or tables).
|
|
|
49
74
|
Run `sportsing fifa help` for the full list (`serve` and `ask` are the AI-bus
|
|
50
75
|
commands; `ask` is low-level plumbing that `serve` wraps).
|
|
51
76
|
|
|
52
|
-
##
|
|
77
|
+
## NBA
|
|
78
|
+
|
|
79
|
+
`sportsing nba <command>` — the NBA via ESPN's free API (no key), preseason
|
|
80
|
+
through playoffs. Times are local; preseason games are tagged `PRE`, postseason
|
|
81
|
+
(incl. play-in) `POST`.
|
|
82
|
+
|
|
83
|
+
| Command | What it does |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `today [--tomorrow\|--yesterday\|--offset N]` | One day's games |
|
|
86
|
+
| `next` | Next game + countdown |
|
|
87
|
+
| `schedule` | A team's whole season by day (league-wide: next 7 days) |
|
|
88
|
+
| `results` | Finished games, newest first (league-wide: last 7 days) |
|
|
89
|
+
| `standings [--conference X] [--division X]` | Regular-season standings, favorites marked ★ |
|
|
90
|
+
| `season [team]` | Your favorites' season: record, home/away splits, playoff race |
|
|
91
|
+
| `bracket [--season YYYY]` | Playoff bracket (projected before the postseason) |
|
|
92
|
+
| `fav [add\|rm\|list]` | Manage NBA favorites (`fav add UTAH`, `fav add Jazz`) |
|
|
93
|
+
| `watch [team] [--wait] [--provider X] [--url L] [--smoke]` | Open the game where you can watch it — see **Where to watch** below |
|
|
94
|
+
| `live [--notify [--quiet]]` | Auto-refreshing live board; `--notify` alerts for favorites — see **Live fav-alerts** below |
|
|
95
|
+
| `analyze <team> [team]` | AI read of the live or latest game: shooting, boards, turnovers, leaders (answered by `serve`; `--prompt` prints it) |
|
|
96
|
+
| `predict <team> [team]` | AI prediction for the next game from both teams' recent form |
|
|
97
|
+
| `recap <team> [team]` | AI "here's what you missed": lead changes, runs, close-late baskets, quarter scores |
|
|
98
|
+
|
|
99
|
+
`--team X` (abbreviation or name; NBA.com codes like `UTA` work too) picks one
|
|
100
|
+
team for `today` / `next` / `schedule` / `results`; `--mine` limits them to your
|
|
101
|
+
NBA favorites. NBA favorites are separate from FIFA ones.
|
|
102
|
+
|
|
103
|
+
`today` / `next` / `schedule` show a **WATCH** column once you've set your
|
|
104
|
+
subscriptions (see **Where to watch**). `sportsing nba watch` opens Jazz games on
|
|
105
|
+
Fubo by default.
|
|
106
|
+
|
|
107
|
+
`sportsing nba watch [team] --overlay` opens Fubo's NBA hub with a live-stats
|
|
108
|
+
overlay (same gear / settings / spoiler-delay as the FIFA one) that follows the
|
|
109
|
+
game you open, recognized from the page title's English team names. NBA panels:
|
|
110
|
+
score & clock, FG% / 3P%, leaders (PTS / REB / AST), fouls, timeouts. On an NHL
|
|
111
|
+
game page (`sportsing nhl watch --provider fubo --overlay`): score & clock,
|
|
112
|
+
shots on goal, power plays, faceoff %, goalie saves. Both have **Get caught up**
|
|
113
|
+
(a recap up to your delayed stream's moment, via `/loop sportsing serve`). Panel choices are saved per
|
|
114
|
+
provider and sport (`overlayPanels["fubo:nba"]`). `--overlay --smoke` checks the
|
|
115
|
+
overlay renders, then tears the window down.
|
|
116
|
+
|
|
117
|
+
## NHL
|
|
118
|
+
|
|
119
|
+
`sportsing nhl <command>` — the same commands as `nba`, for the NHL (ESPN, no
|
|
120
|
+
key), preseason through playoffs, tagged `PRE` / `POST` the same way. Live and
|
|
121
|
+
final status uses hockey periods: `12:34 - 2nd`, `End of 3rd`, `OT`, `SO`,
|
|
122
|
+
`Final/OT`, `Final/SO`.
|
|
123
|
+
|
|
124
|
+
`--team X` takes an abbreviation or name — `UTAH`, `UTA`, `Mammoth`, and
|
|
125
|
+
NHL.com codes like `TBL` / `LAK` all work. NHL favorites (`fav add UTAH`) are
|
|
126
|
+
separate from NBA and FIFA ones, so the Jazz and the Mammoth can both be `UTAH`.
|
|
127
|
+
|
|
128
|
+
`standings`, `season`, `bracket`, `watch`, and `analyze` / `predict` / `recap`
|
|
129
|
+
work exactly as for the NBA — the AI prompts speak hockey (goaltending, special
|
|
130
|
+
teams, every goal and penalty). There's
|
|
131
|
+
no default NHL streaming provider: `nhl watch` follows **Where to watch** — an
|
|
132
|
+
over-the-air game prints its channel (e.g. Utah 16) and opens nothing.
|
|
133
|
+
|
|
134
|
+
## Where to watch (NBA / NHL)
|
|
135
|
+
|
|
136
|
+
Tell sportsing what you can watch with and where you live, once — it applies to
|
|
137
|
+
every sport:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
sportsing subscriptions set fubo nba-league-pass local-ota # whichever you have (or `none`)
|
|
141
|
+
sportsing subscriptions market utah # home market (default: utah)
|
|
142
|
+
sportsing subscriptions # show them (alias: subs)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
From each game's broadcasts (ESPN), sportsing then works out how *you* can watch
|
|
146
|
+
it:
|
|
147
|
+
|
|
148
|
+
- **WATCH column** in `today` / `next` / `schedule`: the service (`Fubo`,
|
|
149
|
+
`NBA League Pass`) or over-the-air channel, `✗` if you can't watch it, `?` if it
|
|
150
|
+
can't tell. With no subscriptions set, a one-line hint replaces the column.
|
|
151
|
+
- **`<sport> watch`** acts on the same answer: opens Fubo or League Pass, prints
|
|
152
|
+
the channel for an over-the-air-only game (nothing to open; exits 0), or says
|
|
153
|
+
why a game isn't watchable (exits 1). `--provider` / `--url` override it.
|
|
154
|
+
- **League Pass** covers out-of-market NBA games only — blacked out when a
|
|
155
|
+
home-market team plays or the game has a national broadcast. National
|
|
156
|
+
exclusives you don't carry are flagged.
|
|
157
|
+
|
|
158
|
+
## Your teams, every sport
|
|
159
|
+
|
|
160
|
+
Bare `today`, `next`, and `me` (no sport prefix) aggregate your favorite teams
|
|
161
|
+
across every sport, each row tagged with its sport:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
sportsing nba fav add UTAH && sportsing nhl fav add UTAH
|
|
165
|
+
sportsing today # the Jazz and the Mammoth today (--tomorrow, --yesterday, --offset N)
|
|
166
|
+
sportsing next # each favorite's next game + countdown, soonest first
|
|
167
|
+
sportsing me # last result + next game per favorite team
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Bare `live --notify` alerts across every sport too (see **Live fav-alerts**).
|
|
171
|
+
Every other command needs a sport — a bare one (e.g. `sportsing standings`)
|
|
172
|
+
prints the sports that have it and exits non-zero.
|
|
173
|
+
|
|
174
|
+
Favorites are stored per sport as `<sport>:<team>` (`nba:UTAH`, `nhl:UTAH`,
|
|
175
|
+
`fifa:USA`) in `~/.config/sportsing/config.json`; add and remove them with each
|
|
176
|
+
sport's `fav` command.
|
|
177
|
+
|
|
178
|
+
## Upgrading to 0.2.0
|
|
179
|
+
|
|
180
|
+
- **Breaking — bare commands need a sport.** In 0.1.x every bare command ran as a
|
|
181
|
+
FIFA command (`sportsing serve` meant `sportsing fifa serve`). Now only `today`,
|
|
182
|
+
`next`, and `me` work bare (across every sport); everything else needs its
|
|
183
|
+
prefix: `sportsing fifa serve`, `sportsing fifa live --notify`,
|
|
184
|
+
`sportsing fifa watch USA`, `sportsing fifa fav add USA`, and so on. Update any
|
|
185
|
+
scripts, aliases, or launchd jobs that call a bare command.
|
|
186
|
+
- **Favorites are scoped by sport.** Existing unprefixed favorites (`"USA"`) are
|
|
187
|
+
read as FIFA favorites (`fifa:USA`) — nothing is lost. The file is rewritten in
|
|
188
|
+
the prefixed form the next time you add or remove a favorite. Downgrading to
|
|
189
|
+
0.1.x after that would see `fifa:USA` as a team name, so don't mix versions.
|
|
190
|
+
|
|
191
|
+
## Watch (FIFA)
|
|
53
192
|
|
|
54
193
|
`sportsing fifa watch [team] [team]` opens the broadcast in your own browser
|
|
55
194
|
(your real Chrome, via [ui-leaf](https://www.npmjs.com/package/@openthink/ui-leaf)):
|
|
@@ -95,6 +234,8 @@ Turn `live` into an ambient alerter that pings you when your favorite teams play
|
|
|
95
234
|
sportsing fifa fav add USA # set up favorites first
|
|
96
235
|
sportsing fifa live --notify # live board + OS notifications
|
|
97
236
|
sportsing fifa live --notify --quiet & # headless: alerts only, backgroundable
|
|
237
|
+
sportsing nba live --notify # same for the NBA (and `nhl live`)
|
|
238
|
+
sportsing live --notify --quiet & # one alerter for every sport with a favorite
|
|
98
239
|
```
|
|
99
240
|
|
|
100
241
|
Each refresh diffs the latest scores against the previous tick and raises an OS
|
|
@@ -106,6 +247,20 @@ full-time alerts exactly once:
|
|
|
106
247
|
- **Goal** — the scorer and the resulting scoreline (with a sound).
|
|
107
248
|
- **Full time** — the final scoreline.
|
|
108
249
|
|
|
250
|
+
NBA and NHL alert on what matters per sport, not every basket:
|
|
251
|
+
|
|
252
|
+
- **NHL** — puck drop, every goal (with a sound), end of each period, overtime /
|
|
253
|
+
shootout, final.
|
|
254
|
+
- **NBA** — tip-off, lead changes, a close finish (≤ 5 points with ≤ 5:00 left in
|
|
255
|
+
the 4th or OT, once per game), final.
|
|
256
|
+
- **Tip-off / puck drop** are click-to-watch, routed like `<sport> watch` (see
|
|
257
|
+
**Where to watch**): a game on Fubo or League Pass opens on click; an
|
|
258
|
+
over-the-air-only game names its channel instead (nothing to click into).
|
|
259
|
+
|
|
260
|
+
Bare `sportsing live --notify` polls every sport you have a favorite in from one
|
|
261
|
+
process; a sport that can't load (e.g. FIFA without an API key) is reported and
|
|
262
|
+
the rest keep alerting. Without `--quiet` it logs each alert as a line.
|
|
263
|
+
|
|
109
264
|
Flags:
|
|
110
265
|
|
|
111
266
|
- **`--notify`** — fire the alerts. With no favorites set, it warns and runs the
|
|
@@ -117,7 +272,7 @@ Flags:
|
|
|
117
272
|
|
|
118
273
|
### Click-to-watch requires `terminal-notifier`
|
|
119
274
|
|
|
120
|
-
Clickable kickoff notifications use
|
|
275
|
+
Clickable kickoff / tip-off / puck-drop notifications use
|
|
121
276
|
[`terminal-notifier`](https://github.com/julienXX/terminal-notifier) (macOS):
|
|
122
277
|
|
|
123
278
|
```sh
|
|
@@ -127,7 +282,8 @@ brew install terminal-notifier
|
|
|
127
282
|
Notifications **degrade gracefully** when it's absent: on macOS they fall back to
|
|
128
283
|
`osascript` (plain banner, no click action); on Linux to `notify-send`; otherwise
|
|
129
284
|
to a terminal bell. Nothing errors — you just don't get the one-click-to-watch
|
|
130
|
-
behavior without `terminal-notifier`.
|
|
285
|
+
behavior without `terminal-notifier`. A click opens a Terminal window running
|
|
286
|
+
`watch` (Ctrl-C or close the stream window to stop).
|
|
131
287
|
|
|
132
288
|
## AI (analyze / predict / overlay "Ask Claude" + "Get caught up")
|
|
133
289
|
|
|
@@ -157,18 +313,19 @@ prints this recipe; `sportsing fifa` and the watch nag point at it.
|
|
|
157
313
|
### Low-level primitive — `serve`
|
|
158
314
|
|
|
159
315
|
`sportsing fifa serve` is the bare answerer loop (it powers `analyze` / `predict`
|
|
160
|
-
|
|
316
|
+
/ `recap` too — for every sport: the bus is shared, so one `fifa serve` answers
|
|
317
|
+
NBA and NHL questions as well). `agent-setup` supersedes the old manual two-step for the agent-driven flow,
|
|
161
318
|
but `serve` remains the primitive if you want to compose it yourself:
|
|
162
319
|
|
|
163
320
|
```sh
|
|
164
321
|
sportsing fifa watch --wait # (backgrounded) opens the game when it's live
|
|
165
|
-
/loop sportsing serve
|
|
322
|
+
/loop sportsing fifa serve # answer-only loop — no watch supervision
|
|
166
323
|
```
|
|
167
324
|
|
|
168
325
|
> **Run the answerer in a minimal-tool session.** Whether via `agent-setup` or
|
|
169
326
|
> `serve`, the loop reads **untrusted** text (viewer questions + raw API fields)
|
|
170
327
|
> into a tool-capable Claude session. Give that session no MCP/file tools and only
|
|
171
|
-
> the `sportsing ask --reply` Bash capability, so a prompt-injection in a question
|
|
328
|
+
> the `sportsing fifa ask --reply` Bash capability, so a prompt-injection in a question
|
|
172
329
|
> can't reach anything dangerous. `serve` prints this reminder each tick.
|
|
173
330
|
|
|
174
331
|
## Development
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sportsing",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sportsing — the FIFA World Cup 2026
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -45,6 +49,12 @@
|
|
|
45
49
|
"publishConfig": {
|
|
46
50
|
"access": "public"
|
|
47
51
|
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@openthink/ui-leaf": "^1.5.1"
|
|
54
|
+
},
|
|
55
|
+
"trustedDependencies": [
|
|
56
|
+
"@openthink/ui-leaf"
|
|
57
|
+
],
|
|
48
58
|
"devDependencies": {
|
|
49
59
|
"@types/node": "^25.9.2",
|
|
50
60
|
"bun-types": "^1.3.14",
|
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
|
+
}
|
package/src/commands/_lib.ts
CHANGED
|
@@ -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
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
if (favs
|
|
82
|
-
|
|
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
|
-
/**
|
|
86
|
-
export
|
|
87
|
-
|
|
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
|
}
|
package/src/commands/analyze.ts
CHANGED
|
@@ -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;
|
package/src/commands/ask.ts
CHANGED
|
@@ -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;
|