SLOPSHOPPER

prayer-times

The current prayer and how long is left, the next prayer, and zawal, above the prompt. Computed on your computer from your location; nothing is sent anywhere.

newbandcommandtoasttimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prayer-times
› 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 › /prayers ⎿ prayer-times: Set your latitude and longitude in /config (prayer-times). They stay on this computer. 🕌 prayer-times: set your latitude and longitude in /config. They stay on this computer ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
🕌 prayer-times: set your latitude and longitude in /config. They stay on this computer ⟨Claude Code's own drawing⟩
README

claude-code-mods-collection

A private reference collection of public Claude Code mods, with a security-oriented analysis.

  • ANALYSIS.md: what each mod can reach, which to install, which to avoid
  • SOURCES.md: upstream repo and pinned commit for every folder in mods/

Nothing here is installed or executed. All code is by its upstream authors under their own licences (MIT, one Apache-2.0); each folder keeps its original LICENSE.

Source 3 files
hooks/register.tsx 355 lines
1// Prayer Times: which prayer is it, and how long is left?
2//   One line above the prompt: the current prayer and the time left to pray it, then the
3//   next one; zawal and the makruh minutes after sunrise and before sunset in red. The times
4//   are computed on this computer from the latitude and longitude in /config (the sun's
5//   position, as prayer apps do it), so the location is never sent anywhere.
6//   /prayers lists today's times. A toast says when a prayer begins, and again when its time
7//   is nearly over.
8import { atom, read, update } from 'claude-code'
9import type { EngineInterface, Register } from 'claude-code'
10
11import { bandsFile, parseOrder, stackBands } from './bands'
12
13const MIN = 60_000
14const SOON_MIN = 20 // the time left turns yellow under this many minutes
15const LAST_MIN = 5 // and red under this many
16
17// The sun's angle below the horizon for Fajr and Isha; Makkah's Isha is a fixed 90 minutes after Maghrib.
18const METHODS = {
19  karachi: { fajr: 18, isha: 18 },
20  mwl: { fajr: 18, isha: 17 },
21  isna: { fajr: 15, isha: 15 },
22  egypt: { fajr: 19.5, isha: 17.5 },
23  makkah: { fajr: 18.5, isha: 90 / 60, ishaIsMinutes: true },
24} as const
25
26export type Config = {
27  latitude: number
28  longitude: number
29  method: keyof typeof METHODS
30  asr: 'hanafi' | 'standard'
31  highLatitude: 'angle' | 'seventh' | 'middle'
32  adjust?: Partial<Record<Adjustable, number>> // minutes, to match a mosque's timetable
33}
34
35type Adjustable = 'fajr' | 'sunrise' | 'dhuhr' | 'asr' | 'sunset' | 'isha'
36
37// A day's times in hours after midnight UTC of that date (some may fall before 0 or past 24).
38export type Times = { fajr: number; sunrise: number; noon: number; dhuhr: number; asr: number; sunset: number; isha: number; estimated?: boolean }
39
40// Held by the host, so the line keeps ticking across a hot reload.
41const now = atom({ plugin: 'prayer-times', key: 'now' } as const, 0)
42
43let bandOrder: Promise<string[]> | undefined // read once per session: restart to apply
44
45export const register: Register = (on, options) => {
46  const cfg = configFrom(options)
47  const makruh = num(options.makruhMinutes, 15)
48  const zawal = num(options.zawalMinutes, 5)
49  const toasts = options.toasts !== false
50  const warnMinutes = Math.min(120, num(options.warnMinutes, 15))
51  let toasted = ''
52  let warned = ''
53
54  on('session.start', async ($, e, next) => {
55    const r = await next(e)
56    await $.command.register({ name: 'prayers', description: "Today's prayer times" }).catch(() => {})
57    const first = await $.clock.now()
58    await update($, now, () => first)
59    // The line moves each minute; a toast marks the start of each prayer, and another its last minutes.
60    $.clock.every(30_000, () => {
61      void (async () => {
62        const t = await $.clock.now()
63        await update($, now, () => t)
64        if (!cfg || !toasts) return
65        const s = status(cfg, new Date(t), makruh, zawal)
66        const key = `${s.current?.name}@${s.current?.start.toDateString()}`
67        if (s.current && t - s.current.start.getTime() < 60_000 && key !== toasted) {
68          toasted = key
69          $.ui.toast(`🕌 ${s.current.name} has begun · until ${hhmm(s.current.end)}`)
70        }
71        const due = deadlineDue(s, new Date(t), warnMinutes, warned, makruh)
72        if (due) {
73          warned = due.key
74          $.ui.toast(due.text)
75        }
76      })().catch(() => {})
77    })
78    return r
79  })
80
81  on('command.run', { command: 'prayers' }, async () => {
82    if (!cfg) return { text: 'Set your latitude and longitude in /config (prayer-times). They stay on this computer.' }
83    const d = new Date()
84    const t = timesFor(cfg, d)
85    const at = (h: number) => hhmm(atHours(d, h))
86    return {
87      text:
88        `Fajr ${at(t.fajr)} · Sunrise ${at(t.sunrise)} · Zawal ${at(t.noon - zawal / 60)} · Dhuhr ${at(t.dhuhr)} · ` +
89        `Asr ${at(t.asr)} · Maghrib ${at(t.sunset)} · Isha ${at(t.isha)} (${cfg.method}, ${cfg.asr} Asr` +
90        `${t.estimated ? ', no sunrise or sunset today: estimated from latitude 65°' : ''})`,
91    }
92  })
93
94  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
95    const rest = await next(e) // what other mods and Claude Code draw here stays
96    if (e.props.hasSurvey) return rest
97    const t = (await read($, now)) || Date.now() // subscribes the line to the clock
98    const { Box, Text } = $.ui.resolve(e)
99    if (!cfg) {
100      const ask = (
101        <Box paddingX={1}>
102          <Text dimColor wrap="truncate-end">{'🕌 prayer-times: set your latitude and longitude in /config. They stay on this computer'}</Text>
103        </Box>
104      )
105      return stackBands(rest, 'prayer-times', ask, 'above', await (bandOrder ??= readBandOrder($).catch(() => []))) // above what other mods draw, unless the band order says otherwise
106    }
107    const s = status(cfg, new Date(t), makruh, zawal)
108    const mine = (
109      <Box paddingX={1}>
110        <Text wrap="truncate-end">
111          {s.parts.map((p, i) => {
112            // A leading emoji is drawn plain: some terminals shift or clip a bold one.
113            const icon = /^(\p{Extended_Pictographic}\uFE0F?) /u.exec(p.text)?.[1]
114            const label = icon ? p.text.slice(icon.length) : p.text
115            return (
116              <Text>
117                {i > 0 && <Text dimColor>{' · '}</Text>}
118                {icon && <Text>{icon}</Text>}
119                <Text color={p.color} bold={p.bold} dimColor={p.dim}>{label}</Text>
120              </Text>
121            )
122          })}
123        </Text>
124      </Box>
125    )
126    return stackBands(rest, 'prayer-times', mine, 'above', await (bandOrder ??= readBandOrder($).catch(() => []))) // above what other mods draw, unless the band order says otherwise
127  })
128}
129
130// The band order file, as the claude-code-mods checklist saves it; [] on any error.
131async function readBandOrder($: EngineInterface): Promise<string[]> {
132  const file = bandsFile(await $.env.get('CLAUDE_CONFIG_DIR'), await $.env.get('HOME'))
133  return file && (await $.fs.exists(file)) ? parseOrder(await $.fs.read(file)) : []
134}
135
136// Forgets the cached band order, for tests.
137export function resetBandOrder() {
138  bandOrder = undefined
139}
140
141const DHUHR_AFTER = 2 / 60 // Dhuhr starts this long after the sun's highest point
142
143type Part = { text: string; color?: string; bold?: boolean; dim?: boolean }
144type Window = { name: string; start: Date; end: Date }
145
146// What the line says at `at`: the prayer whose time it is and what is left, or the forbidden window, then the next prayer.
147export function status(cfg: Config, at: Date, makruh = 15, zawal = 5) {
148  // Times are UTC hours of each local date, so far from UTC (e.g. UTC+13) a day's prayers can sit two dates back.
149  const windows = [-2, -1, 0, 1].flatMap(offset => dayWindows(cfg, addDays(at, offset), makruh, zawal))
150  const inside = (w: Window) => w.start <= at && at < w.end
151  const prayers = windows.filter(w => !w.name.startsWith('!'))
152  const current = prayers.find(inside)
153  const blocked = windows.find(w => w.name.startsWith('!') && inside(w))
154  const next = prayers.find(w => w.start > at)!
155  const parts: Part[] = []
156
157  if (blocked) {
158    const label = blocked.name === '!zawal' ? 'Zawal' : blocked.name === '!sunrise' ? 'Sunrise' : 'Sunset near'
159    parts.push({ text: `⛔ ${label}`, color: 'red', bold: true })
160    if (blocked.name === '!sunset' && current) parts.push({ text: `${current.name} ${until(current.end, at)} left, makruh now`, color: 'red' })
161    else parts.push({ text: `no prayer for ${until(blocked.end, at)}`, color: 'red' })
162  } else if (current) {
163    const left = (current.end.getTime() - at.getTime()) / MIN
164    parts.push({ text: `🕌 ${current.name}`, bold: true })
165    parts.push({ text: `${until(current.end, at)} left`, color: left < LAST_MIN ? 'red' : left < SOON_MIN ? 'yellow' : 'green', bold: left < SOON_MIN })
166  } else {
167    parts.push({ text: `🕌 ${next.name} in ${until(next.start, at)}`, bold: true })
168  }
169  if (current || blocked) parts.push({ text: `next ${next.name} ${hhmm(next.start)}`, dim: true })
170  // Before Dhuhr, say when zawal comes.
171  const z = windows.find(w => w.name === '!zawal' && w.start > at)
172  if (z && next.name === 'Dhuhr' && !blocked) parts.push({ text: `zawal ${hhmm(z.start)}`, dim: true })
173  if (timesFor(cfg, at).estimated) parts.push({ text: 'estimated (no sunrise or sunset today)', dim: true })
174  return { parts, current, next, blocked }
175}
176
177// The warning that the current prayer's time is nearly over, once per window (`warned` is the last key warned).
178// It also comes when the session starts inside the last minutes, but not for a window no longer than the warning,
179// whose start toast already says when it ends. Asr warns too: its time runs to sunset, through the makruh minutes.
180// Asr's real deadline is the start of the makruh minutes before sunset, so it warns before those.
181export function deadlineDue(s: Pick<ReturnType<typeof status>, 'current'>, at: Date, warnMinutes: number, warned: string, makruh = 0) {
182  const w = s.current
183  if (!w || !(warnMinutes > 0)) return null
184  const key = `${w.name}@${w.start.getTime()}`
185  const end = w.name === 'Asr' && makruh > 0 ? new Date(w.end.getTime() - makruh * MIN) : w.end
186  const left = (end.getTime() - at.getTime()) / MIN
187  const length = (w.end.getTime() - w.start.getTime()) / MIN
188  if (key === warned || left <= 0 || left > warnMinutes || length <= warnMinutes) return null
189  if (end !== w.end) return { key, text: `⏳ Asr: makruh in ${Math.ceil(left)} min (${hhmm(end)}), sunset ${hhmm(w.end)}` }
190  return { key, text: `⏳ ${w.name} ends in ${Math.ceil(left)} min (${hhmm(w.end)})` }
191}
192
193// One day's windows: each prayer from its start to its end (Hanafi), and the times not to pray, marked with `!`.
194function dayWindows(cfg: Config, day: Date, makruh: number, zawal: number): Window[] {
195  const t = timesFor(cfg, day)
196  const tomorrow = timesFor(cfg, addDays(day, 1))
197  const at = (h: number) => atHours(day, h)
198  return [
199    { name: 'Fajr', start: at(t.fajr), end: at(t.sunrise) },
200    { name: '!sunrise', start: at(t.sunrise), end: at(t.sunrise + makruh / 60) },
201    { name: '!zawal', start: at(t.noon - zawal / 60), end: at(t.noon + DHUHR_AFTER) },
202    { name: 'Dhuhr', start: at(t.dhuhr), end: at(t.asr) },
203    { name: 'Asr', start: at(t.asr), end: at(t.sunset) },
204    { name: '!sunset', start: at(t.sunset - makruh / 60), end: at(t.sunset) },
205    { name: 'Maghrib', start: at(t.sunset), end: at(t.isha) },
206    { name: 'Isha', start: at(t.isha), end: at(24 + tomorrow.fajr) },
207  ]
208}
209
210// The day's times at the computer's own time zone for that date.
211// The times for the computer's local date, as UTC hours, so a daylight-saving change that day can't shift them.
212export function timesFor(cfg: Config, day: Date): Times {
213  const t = computeTimes(cfg, day.getFullYear(), day.getMonth() + 1, day.getDate(), 0)
214  for (const [k, m] of Object.entries(cfg.adjust ?? {})) t[k as Adjustable] += m / 60
215  return t
216}
217
218// `asr+2 isha-5` as minutes per time. Dhuhr moves Dhuhr's start only (zawal stays on the sun's highest point); Maghrib moves sunset.
219export function adjustFrom(text: string): Config['adjust'] {
220  const names: Record<string, Adjustable> = { fajr: 'fajr', sunrise: 'sunrise', dhuhr: 'dhuhr', asr: 'asr', maghrib: 'sunset', isha: 'isha' }
221  const out: Partial<Record<Adjustable, number>> = {}
222  for (const m of text.toLowerCase().matchAll(/(fajr|sunrise|dhuhr|asr|maghrib|isha)\s*([+-]?)\s*(\d+(?:\.\d+)?)/g)) out[names[m[1]!]!] = (m[2] === '-' ? -1 : 1) * Number(m[3])
223  return out
224}
225
226// The sun's position for the day, then the times it reaches each prayer's angle (the PrayTimes method).
227export function computeTimes(cfg: Config, year: number, month: number, day: number, zone: number): Times {
228  const t = solve(cfg, year, month, day, zone)
229  if (Number.isFinite(t.sunrise) && Number.isFinite(t.sunset)) return t
230  // Polar day or night: no sunrise or sunset at all. Use the nearest latitude that has them (65°), as many scholars advise.
231  return { ...solve({ ...cfg, latitude: Math.sign(cfg.latitude) * 65 }, year, month, day, zone), estimated: true }
232}
233
234function solve(cfg: Config, year: number, month: number, day: number, zone: number): Times {
235  const { latitude: lat, longitude: lng } = cfg
236  const m = METHODS[cfg.method]
237  const jdate = julian(year, month, day) - lng / (15 * 24)
238  const sun = (t: number) => sunPosition(jdate + t)
239  const midDay = (t: number) => fix(12 - sun(t).equation, 24)
240  const angleTime = (angle: number, t: number, before: boolean) => {
241    const decl = sun(t).declination
242    const noon = midDay(t)
243    const cos = (-sin(angle) - sin(decl) * sin(lat)) / (cos_(decl) * cos_(lat))
244    const span = acos(cos) / 15 // NaN when the sun never reaches the angle
245    return noon + (before ? -span : span)
246  }
247  const asrTime = (factor: number, t: number) => {
248    const decl = sun(t).declination
249    return angleTime(-acot(factor + tan(Math.abs(lat - decl))), t, false)
250  }
251  const factor = cfg.asr === 'hanafi' ? 2 : 1
252
253  // Two passes: the first from rough guesses, the second from the first's answers.
254  let h = { fajr: 5, sunrise: 6, noon: 12, asr: 13, sunset: 18, isha: 18 }
255  for (let pass = 0; pass < 2; pass++) {
256    const d = (x: number) => x / 24
257    h = {
258      fajr: angleTime(m.fajr, d(h.fajr), true),
259      sunrise: angleTime(0.833, d(h.sunrise), true),
260      noon: midDay(d(h.noon)),
261      asr: asrTime(factor, d(h.asr)),
262      sunset: angleTime(0.833, d(h.sunset), false),
263      isha: 'ishaIsMinutes' in m ? NaN : angleTime(m.isha, d(h.isha), false),
264    }
265  }
266  const shift = zone - lng / 15
267  const out: Times = { fajr: h.fajr + shift, sunrise: h.sunrise + shift, noon: h.noon + shift, dhuhr: h.noon + shift + DHUHR_AFTER, asr: h.asr + shift, sunset: h.sunset + shift, isha: h.isha + shift }
268  if ('ishaIsMinutes' in m) out.isha = out.sunset + m.isha
269
270  // Far from the equator the sun may never get low enough: Fajr and Isha take a share of the night instead.
271  const night = 24 - (out.sunset - out.sunrise)
272  const share = (angle: number) => (cfg.highLatitude === 'seventh' ? 1 / 7 : cfg.highLatitude === 'middle' ? 1 / 2 : angle / 60)
273  const fajrMax = share(m.fajr) * night
274  if (!Number.isFinite(out.fajr) || out.sunrise - out.fajr > fajrMax) out.fajr = out.sunrise - fajrMax
275  if (!('ishaIsMinutes' in m)) {
276    const ishaMax = share(m.isha) * night
277    if (!Number.isFinite(out.isha) || out.isha - out.sunset > ishaMax) out.isha = out.sunset + ishaMax
278  }
279  return out
280}
281
282function sunPosition(jd: number) {
283  const D = jd - 2451545
284  const g = fix(357.529 + 0.98560028 * D, 360)
285  const q = fix(280.459 + 0.98564736 * D, 360)
286  const L = fix(q + 1.915 * sin(g) + 0.02 * sin(2 * g), 360)
287  const e = 23.439 - 0.00000036 * D
288  const ra = atan2(cos_(e) * sin(L), cos_(L)) / 15
289  return { declination: asin(sin(e) * sin(L)), equation: q / 15 - fix(ra, 24) }
290}
291
292function julian(year: number, month: number, day: number) {
293  if (month <= 2) {
294    year -= 1
295    month += 12
296  }
297  const a = Math.floor(year / 100)
298  const b = 2 - a + Math.floor(a / 4)
299  return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + b - 1524.5
300}
301
302const rad = (d: number) => (d * Math.PI) / 180
303const deg = (r: number) => (r * 180) / Math.PI
304const sin = (d: number) => Math.sin(rad(d))
305const cos_ = (d: number) => Math.cos(rad(d))
306const tan = (d: number) => Math.tan(rad(d))
307const asin = (x: number) => deg(Math.asin(x))
308const acos = (x: number) => deg(Math.acos(x))
309const atan2 = (y: number, x: number) => deg(Math.atan2(y, x))
310const acot = (x: number) => deg(Math.atan(1 / x))
311const fix = (a: number, b: number) => {
312  const r = a - b * Math.floor(a / b)
313  return r < 0 ? r + b : r
314}
315
316function configFrom(options: Record<string, unknown>): Config | null {
317  const latitude = Number(options.latitude)
318  const longitude = Number(options.longitude)
319  const isSet = (v: unknown) => v !== undefined && v !== null && v !== ''
320  if (!isSet(options.latitude) || !isSet(options.longitude) || !(Math.abs(latitude) <= 90) || !(Math.abs(longitude) <= 180)) return null
321  const method = String(options.method ?? 'karachi') as Config['method']
322  return {
323    latitude,
324    longitude,
325    method: method in METHODS ? method : 'karachi',
326    asr: options.asr === 'standard' ? 'standard' : 'hanafi',
327    highLatitude: options.highLatitude === 'seventh' || options.highLatitude === 'middle' ? options.highLatitude : 'angle',
328    adjust: adjustFrom(String(options.adjust ?? '')),
329  }
330}
331
332function num(value: unknown, fallback: number) {
333  const n = Number(value)
334  return value === undefined || value === null || value === '' || !Number.isFinite(n) ? fallback : Math.max(0, n)
335}
336
337function addDays(d: Date, n: number) {
338  return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n, 12)
339}
340
341// The instant `hours` after midnight UTC of the day's local date.
342export function atHours(day: Date, hours: number) {
343  return new Date(Date.UTC(day.getFullYear(), day.getMonth(), day.getDate()) + Math.round(hours * 60) * MIN)
344}
345
346export function hhmm(d: Date) {
347  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
348}
349
350// Time left until `end`, rounded up to the minute: `42m`, `1h 05m`.
351export function until(end: Date, at: Date) {
352  const m = Math.max(0, Math.ceil((end.getTime() - at.getTime()) / MIN))
353  return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, '0')}m`
354}
355
hooks/bands.ts 73 lines
1// Band order: every band mod ships this same file (canonical copy: shared/bands.ts in the repo).
2//   The order file, <configDir>/claude-code-mods/bands.json, lists mod names top to bottom
3//   (the last sits right above the prompt). Each band mod wraps its band in a Box keyed
4//   `band:<name>` and returns one column keyed `bands`; the next band mod unpacks that
5//   column, adds its own band and sorts again, so the last one to draw sorts them all.
6//   Edit shared/bands.ts, then copy it over every mod's hooks/bands.ts (a repo test checks).
7//   Reading the file needs $, which Claude Code follows only within one file, so each mod
8//   reads it in its own register file with bandsFile and parseOrder, once per session.
9import type { RenderElement, RenderNode } from 'claude-code'
10
11const COLUMN = 'bands'
12const PREFIX = 'band:'
13
14// Where the checklist saves the order: CLAUDE_CONFIG_DIR, or ~/.claude. Undefined with neither.
15export function bandsFile(configDir: string | undefined, home: string | undefined): string | undefined {
16  const dir = configDir ? configDir.replace(/\/+$/, '') : home ? `${home}/.claude` : ''
17  return dir ? `${dir}/claude-code-mods/bands.json` : undefined
18}
19
20// The names in an order file; [] when it is no JSON or not { order: string[] }.
21export function parseOrder(text: string): string[] {
22  try {
23    const order = (JSON.parse(text) as { order?: unknown } | null)?.order
24    return Array.isArray(order) && order.every(n => typeof n === 'string') ? order : []
25  } catch {
26    return []
27  }
28}
29
30/**
31 * One column of bands: `mine` wrapped in a Box keyed `band:<name>`, placed
32 * `at` (above or below) what `next(e)` returned, then sorted by `order`.
33 * With an order, items that are no band come first, then bands not in the
34 * order (in their current order), then the listed ones in file order. With
35 * no order nothing moves, so the screen looks as it did before.
36 */
37export function stackBands(rest: RenderElement | null | undefined, name: string, mine: RenderNode, at: 'above' | 'below', order: string[]): RenderElement {
38  const own: RenderElement = { type: 'Box', props: { key: PREFIX + name, flexDirection: 'column' }, children: [mine] }
39  const others = unpack(rest)
40  const items = at === 'above' ? [own, ...others] : [...others, own]
41  return { type: 'Box', props: { key: COLUMN, flexDirection: 'column' }, children: order.length ? sorted(items, order) : items }
42}
43
44// Another band mod's column gives up its items; anything else is one item.
45function unpack(rest: RenderElement | null | undefined): RenderNode[] {
46  if (!rest) return []
47  if (!isColumn(rest)) return [rest]
48  const kids = rest.children ?? []
49  const owner = rest.part?.plugin
50  // The engine stamped the column with its owner: that stamp belongs on the owner's own band.
51  return kids.map(k => (owner && typeof k !== 'string' && bandName(k) === owner && !('part' in k && k.part) ? { ...k, part: rest.part } : k))
52}
53
54function isColumn(el: RenderElement): el is RenderElement & { type: 'Box' } {
55  return el.type === 'Box' && el.props?.key === COLUMN
56}
57
58function bandName(node: RenderNode): string | undefined {
59  if (typeof node === 'string' || node.type !== 'Box') return undefined
60  const key = node.props?.key
61  return typeof key === 'string' && key.startsWith(PREFIX) ? key.slice(PREFIX.length) : undefined
62}
63
64function sorted(items: RenderNode[], order: string[]): RenderNode[] {
65  const rank = (n: RenderNode) => {
66    const name = bandName(n)
67    if (name === undefined) return -2 // not a band: first
68    const i = order.indexOf(name)
69    return i < 0 ? -1 : i // a band not in the order: on top of the listed ones
70  }
71  return items.map((n, i) => ({ n, i, r: rank(n) })).sort((a, b) => a.r - b.r || a.i - b.i).map(x => x.n)
72}
73
types/index.d.ts 8 lines
1export type Tick = number // the last clock tick, so the line redraws each minute
2
3declare module 'claude-code' {
4  interface PluginState {
5    'prayer-times': { now: Tick }
6  }
7}
8