Weather from wttr.in in the status line (refreshed at most once an hour); /weather shows the one-line forecast

Current weather in the status line, from wttr.in.
curl against wttr.in (metric units, 5 s timeout) and shows e.g. ☀️ Paris +21°C in the status line.location, wttr.in guesses from your IP and the city is left out of the display (coordinates are filtered).| Command | Effect |
|---|---|
/weather | Fetches and shows the one-line forecast for the configured location |
| Field | Type | Default | Meaning |
|---|---|---|---|
location | string | "" | City or place name for wttr.in; empty = guess from IP |
claude --plugin-dir /path/to/ModsTools/mods/ascii-weather
location is empty); the only mod of the fun set that does.curl on the PATH.claude plugin validate mods/ascii-weather
claude plugin test mods/ascii-weather # 11 testshooks/register.ts 41 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { REFRESH_MS, parseWeather, weatherUrl } from './weather'
4
5async function fetchWeather($: EngineInterface, location: string, format: string): Promise<string | null> {
6 try {
7 const out = await $.process.run(['curl', '-s', '-m', '5', weatherUrl(location, format)])
8
9 return out.exitCode === 0 ? parseWeather(out.stdout) : null
10 } catch {
11 return null
12 }
13}
14
15async function refresh($: EngineInterface, location: string, state: { at: number }): Promise<void> {
16 state.at = await $.clock.now()
17 $.ui.status((await fetchWeather($, location, '%c+%l+%t')) ?? undefined)
18}
19
20export const register: Register = (on, options) => {
21 const location = typeof options?.location === 'string' ? options.location : ''
22 const state = { at: Number.NEGATIVE_INFINITY }
23
24 on('session.start', async ($, e, next) => {
25 await $.command.register({ name: 'weather', description: 'ascii-weather: the one-line forecast for the configured location' })
26 await refresh($, location, state)
27
28 return next(e)
29 })
30
31 on('turn.complete', async ($, e, next) => {
32 if (e.agentId === undefined && (await $.clock.now()) - state.at >= REFRESH_MS) await refresh($, location, state)
33
34 return next(e)
35 })
36
37 on('command.run', { command: 'weather' }, async $ => ({
38 text: (await fetchWeather($, location, '3')) ?? 'Weather unavailable (wttr.in did not answer or the location is unknown).',
39 }))
40}
41hooks/weather.ts 18 lines1export const REFRESH_MS = 3_600_000
2
3const COORDS = /^-?\d+(\.\d+)?,-?\d+(\.\d+)?:?$/
4const REJECT = /<|unknown location|not found|upstream error|sorry|error/i
5
6export function weatherUrl(location: string, format: string): string {
7 return `https://wttr.in/${encodeURIComponent(location.trim())}?format=${encodeURIComponent(format)}&m`
8}
9
10export function parseWeather(raw: string): string | null {
11 const text = raw.replace(/\s+/g, ' ').trim()
12 if (text === '' || text.length > 100 || REJECT.test(text)) return null
13 const words = text.split(' ').filter(word => !COORDS.test(word))
14 const out = words.join(' ')
15
16 return out === '' ? null : out
17}
18