SLOPSHOPPER

ascii-weather

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

newcommandstatusprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ascii-weather
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /weather ⎿ ascii-weather: Weather unavailable (wttr.in did not answer or the location is unknown). ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

ascii-weather

Current weather in the status line, from wttr.in.

What it does

  • At session start and after a main turn at least one hour after the last fetch, runs curl against wttr.in (metric units, 5 s timeout) and shows e.g. ☀️ Paris +21°C in the status line.
  • With an empty location, wttr.in guesses from your IP and the city is left out of the display (coordinates are filtered).
  • On any failure the status line entry is cleared; no retry before the next hour.

Commands

CommandEffect
/weatherFetches and shows the one-line forecast for the configured location

Options

FieldTypeDefaultMeaning
locationstring""City or place name for wttr.in; empty = guess from IP

Install

claude --plugin-dir /path/to/ModsTools/mods/ascii-weather

Limits

  • Makes a network call to wttr.in (with your IP when location is empty); the only mod of the fun set that does.
  • Answers that look like errors or are longer than 100 characters are discarded.
  • Needs curl on the PATH.

Develop

claude plugin validate mods/ascii-weather
claude plugin test mods/ascii-weather   # 11 tests
Source 2 files
hooks/register.ts 41 lines
1import 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}
41
hooks/weather.ts 18 lines
1export 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