thinking-phrases 1.0.1 → 2.0.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.
Files changed (41) hide show
  1. package/README.md +230 -142
  2. package/configs/hn-top.config.json +60 -27
  3. package/launchd/rss-update.error.log +3 -27
  4. package/launchd/rss-update.log +308 -0
  5. package/launchd/task-health.json +54 -0
  6. package/out/dwyl-quotes.json +1621 -0
  7. package/out/javascript-tips.json +107 -0
  8. package/out/league-loading-screen-tips.json +107 -0
  9. package/out/ruby-tips.json +115 -0
  10. package/out/settings-linux.json +87 -0
  11. package/out/settings-mac.json +87 -0
  12. package/out/settings-windows.json +87 -0
  13. package/out/typescript-tips.json +131 -0
  14. package/out/vscode-tips.json +87 -0
  15. package/out/wow-loading-screen-tips.json +116 -0
  16. package/package.json +19 -12
  17. package/scripts/build.ts +3 -3
  18. package/scripts/debug-hn-hydration.ts +33 -0
  19. package/scripts/run-rss-update.zsh +25 -3
  20. package/scripts/show-thinking-phrases-health.ts +74 -0
  21. package/scripts/trigger-thinking-phrases-scheduler.zsh +50 -0
  22. package/src/core/config.ts +65 -3
  23. package/src/core/githubModels.ts +200 -112
  24. package/src/core/interactive.ts +49 -67
  25. package/src/core/phraseCache.ts +242 -0
  26. package/src/core/phraseFormats.ts +243 -0
  27. package/src/core/presets.ts +1 -1
  28. package/src/core/runner.ts +246 -113
  29. package/src/core/scheduler.ts +1 -1
  30. package/src/core/taskHealth.ts +213 -0
  31. package/src/core/types.ts +32 -8
  32. package/src/core/utils.ts +27 -2
  33. package/src/sources/customJson.ts +28 -18
  34. package/src/sources/earthquakes.ts +4 -4
  35. package/src/sources/githubActivity.ts +120 -48
  36. package/src/sources/hackerNews.ts +19 -7
  37. package/src/sources/rss.ts +25 -11
  38. package/src/sources/stocks.ts +31 -10
  39. package/src/sources/weatherAlerts.ts +173 -7
  40. package/tsconfig.json +1 -1
  41. package/scripts/update-rss-settings.ts +0 -7
@@ -1,6 +1,23 @@
1
- import type { ArticleItem, PhraseSource, WeatherSeverity } from '../core/types.js';
1
+ import { formatWeatherNoAlertsPhrase } from '../core/phraseFormats.js';
2
+ import type { ArticleItem, Config, PhraseSource, WeatherSeverity } from '../core/types.js';
2
3
  import { fetchJson, fetchUsZipLocation, logInfo, relativeTime, truncate } from '../core/utils.js';
3
4
 
5
+ interface NwsPointResponse {
6
+ properties?: {
7
+ relativeLocation?: {
8
+ properties?: {
9
+ city?: string;
10
+ state?: string;
11
+ };
12
+ };
13
+ forecast?: string;
14
+ forecastHourly?: string;
15
+ forecastZone?: string;
16
+ county?: string;
17
+ observationStations?: string;
18
+ };
19
+ }
20
+
4
21
  interface NwsAlertFeature {
5
22
  id?: string;
6
23
  properties?: {
@@ -20,6 +37,27 @@ interface NwsAlertsResponse {
20
37
  features?: NwsAlertFeature[];
21
38
  }
22
39
 
40
+ interface WeatherLookupContext {
41
+ locationLabel: string;
42
+ lookupUrl: string;
43
+ stationsUrl?: string;
44
+ }
45
+
46
+ interface NwsStationsResponse {
47
+ features?: { properties?: { stationIdentifier?: string } }[];
48
+ }
49
+
50
+ interface NwsObservationResponse {
51
+ properties?: {
52
+ temperature?: { value?: number | null };
53
+ textDescription?: string;
54
+ windSpeed?: { value?: number | null };
55
+ windDirection?: { value?: number | null };
56
+ relativeHumidity?: { value?: number | null };
57
+ timestamp?: string;
58
+ };
59
+ }
60
+
23
61
  const SEVERITY_RANK: Record<WeatherSeverity, number> = {
24
62
  minor: 1,
25
63
  moderate: 2,
@@ -58,20 +96,131 @@ function buildWeatherContent(feature: NwsAlertFeature, maxLength: number): strin
58
96
  return pieces.length > 0 ? truncate(pieces.join(' • '), maxLength) : undefined;
59
97
  }
60
98
 
61
- export async function fetchWeatherAlertArticles(config: import('../core/types.js').Config): Promise<ArticleItem[]> {
99
+ function trimTrailingPathSegment(value?: string): string | undefined {
100
+ return value?.trim().replace(/\/+$/u, '').split('/').filter(Boolean).at(-1);
101
+ }
102
+
103
+ function buildLookupUrl(latitude: number, longitude: number): string {
104
+ return `https://forecast.weather.gov/MapClick.php?lat=${latitude}&lon=${longitude}`;
105
+ }
106
+
107
+ function windDirectionLabel(degrees?: number | null): string | undefined {
108
+ if (degrees === null || degrees === undefined || !Number.isFinite(degrees)) return undefined;
109
+ const dirs = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
110
+ return dirs[Math.round(degrees / 45) % 8];
111
+ }
112
+
113
+ function celsiusToFahrenheit(c: number): number {
114
+ return Math.round(c * 9 / 5 + 32);
115
+ }
116
+
117
+ function metersPerSecToMph(ms: number): number {
118
+ return Math.round(ms * 2.237);
119
+ }
120
+
121
+ async function fetchCurrentConditions(context: WeatherLookupContext, config: Config): Promise<ArticleItem | null> {
122
+ if (!context.stationsUrl) return null;
123
+
124
+ try {
125
+ const stationsPayload = await fetchJson<NwsStationsResponse>(context.stationsUrl, { accept: 'application/geo+json' });
126
+ const stationId = stationsPayload.features?.[0]?.properties?.stationIdentifier;
127
+ if (!stationId) return null;
128
+
129
+ const obsUrl = `https://api.weather.gov/stations/${stationId}/observations/latest`;
130
+ const obs = await fetchJson<NwsObservationResponse>(obsUrl, { accept: 'application/geo+json' });
131
+ const props = obs.properties;
132
+ if (!props) return null;
133
+
134
+ const tempC = props.temperature?.value;
135
+ const tempF = tempC !== null && tempC !== undefined ? celsiusToFahrenheit(tempC) : undefined;
136
+ const description = props.textDescription?.trim();
137
+ const windMs = props.windSpeed?.value;
138
+ const windMph = windMs !== null && windMs !== undefined ? metersPerSecToMph(windMs) : undefined;
139
+ const windDir = windDirectionLabel(props.windDirection?.value);
140
+ const humidity = props.relativeHumidity?.value;
141
+
142
+ if (tempF === undefined && !description) return null;
143
+
144
+ const conditionParts: string[] = [];
145
+ if (tempF !== undefined) conditionParts.push(`${tempF}°F`);
146
+ if (description) conditionParts.push(description);
147
+ if (windMph !== undefined && windMph > 0) {
148
+ conditionParts.push(`Wind ${windDir ?? ''} ${windMph} mph`.replace(/\s+/gu, ' ').trim());
149
+ }
150
+ if (humidity !== null && humidity !== undefined) {
151
+ conditionParts.push(`Humidity ${Math.round(humidity)}%`);
152
+ }
153
+
154
+ const conditions = conditionParts.join(', ');
155
+ const title = conditions;
156
+ const displayPhrase = `${conditions} — ${context.locationLabel} — Weather.gov`;
157
+ logInfo(config, `Current conditions: ${context.locationLabel}, ${conditions}`);
158
+
159
+ return {
160
+ type: 'article',
161
+ id: `weather-conditions:${stationId}`,
162
+ title,
163
+ displayPhrase,
164
+ link: context.lookupUrl,
165
+ source: 'Weather.gov',
166
+ content: title,
167
+ articleContent: title,
168
+ skipModelRewrite: true,
169
+ };
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ function buildNoAlertsArticle(context: WeatherLookupContext): ArticleItem {
176
+ return {
177
+ type: 'article',
178
+ id: `weather-alert:none:${context.locationLabel.toLowerCase()}`,
179
+ source: 'Weather.gov',
180
+ title: `No active alerts near ${context.locationLabel}`,
181
+ displayPhrase: formatWeatherNoAlertsPhrase({ location: context.locationLabel }),
182
+ link: context.lookupUrl,
183
+ content: `Lookup: ${context.lookupUrl}`,
184
+ articleContent: `No active weather alerts are currently active near ${context.locationLabel}. Lookup: ${context.lookupUrl}`,
185
+ skipModelRewrite: true,
186
+ };
187
+ }
188
+
189
+ export async function fetchWeatherAlertArticles(config: Config): Promise<ArticleItem[]> {
62
190
  if (!config.weatherAlerts.enabled) {
63
191
  return [];
64
192
  }
65
193
 
66
194
  const params = new URLSearchParams();
195
+ let lookupContext: WeatherLookupContext | undefined;
67
196
  if (config.weatherAlerts.zipCode?.trim()) {
68
197
  const zipLocation = await fetchUsZipLocation(config.weatherAlerts.zipCode);
69
- if (zipLocation.stateAbbreviation) {
70
- params.set('area', zipLocation.stateAbbreviation);
71
- logInfo(config, `Resolved weather ZIP ${zipLocation.zipCode} to ${zipLocation.stateAbbreviation}`);
72
- }
198
+ const pointUrl = `https://api.weather.gov/points/${zipLocation.latitude},${zipLocation.longitude}`;
199
+ const pointPayload = await fetchJson<NwsPointResponse>(pointUrl, { accept: 'application/geo+json' });
200
+ const relativeLocation = pointPayload.properties?.relativeLocation?.properties;
201
+ const forecastZoneId = trimTrailingPathSegment(pointPayload.properties?.forecastZone);
202
+ const countyZoneId = trimTrailingPathSegment(pointPayload.properties?.county);
203
+ const locationLabel = [
204
+ zipLocation.placeName || relativeLocation?.city?.trim(),
205
+ zipLocation.stateAbbreviation || relativeLocation?.state?.trim(),
206
+ ].filter(Boolean).join(', ');
207
+
208
+ params.set('point', `${zipLocation.latitude},${zipLocation.longitude}`);
209
+ lookupContext = {
210
+ locationLabel: locationLabel || `${zipLocation.zipCode}`,
211
+ lookupUrl: buildLookupUrl(zipLocation.latitude, zipLocation.longitude),
212
+ stationsUrl: pointPayload.properties?.observationStations,
213
+ };
214
+ logInfo(
215
+ config,
216
+ `Resolved weather ZIP ${zipLocation.zipCode} to ${lookupContext.locationLabel} (${zipLocation.latitude}, ${zipLocation.longitude})${forecastZoneId ? ` • forecast zone ${forecastZoneId}` : ''}${countyZoneId ? ` • county ${countyZoneId}` : ''}`,
217
+ );
73
218
  } else if (config.weatherAlerts.area?.trim()) {
74
219
  params.set('area', config.weatherAlerts.area.trim().toUpperCase());
220
+ lookupContext = {
221
+ locationLabel: config.weatherAlerts.area.trim().toUpperCase(),
222
+ lookupUrl: `https://api.weather.gov/alerts/active?area=${encodeURIComponent(config.weatherAlerts.area.trim().toUpperCase())}`,
223
+ };
75
224
  }
76
225
 
77
226
  const url = `https://api.weather.gov/alerts/active${params.toString() ? `?${params.toString()}` : ''}`;
@@ -79,7 +228,7 @@ export async function fetchWeatherAlertArticles(config: import('../core/types.js
79
228
  const payload = await fetchJson<NwsAlertsResponse>(url, { accept: 'application/geo+json' });
80
229
  const minimumRank = SEVERITY_RANK[config.weatherAlerts.minimumSeverity];
81
230
 
82
- return (payload.features ?? [])
231
+ const items = (payload.features ?? [])
83
232
  .filter(feature => {
84
233
  const severity = normalizeSeverity(feature.properties?.severity);
85
234
  return severity ? SEVERITY_RANK[severity] >= minimumRank : false;
@@ -102,6 +251,23 @@ export async function fetchWeatherAlertArticles(config: import('../core/types.js
102
251
  };
103
252
  })
104
253
  .filter(item => Boolean(item.title));
254
+
255
+ // Always fetch current conditions when we have a ZIP-based location
256
+ const conditionsArticle = lookupContext ? await fetchCurrentConditions(lookupContext, config) : null;
257
+
258
+ if (items.length === 0 && lookupContext) {
259
+ logInfo(config, `No active weather alerts for ${lookupContext.locationLabel}. Lookup: ${lookupContext.lookupUrl}`);
260
+ const results: ArticleItem[] = [];
261
+ if (conditionsArticle) results.push(conditionsArticle);
262
+ results.push(buildNoAlertsArticle(lookupContext));
263
+ return results;
264
+ }
265
+
266
+ if (conditionsArticle) {
267
+ return [conditionsArticle, ...items];
268
+ }
269
+
270
+ return items;
105
271
  }
106
272
 
107
273
  export const weatherAlertsSource: PhraseSource = {
package/tsconfig.json CHANGED
@@ -11,5 +11,5 @@
11
11
  "outDir": "dist",
12
12
  "rootDir": "."
13
13
  },
14
- "include": ["scripts/**/*", "src/**/*"]
14
+ "include": ["bin/**/*", "scripts/**/*", "src/**/*", "tests/**/*", "vitest.config.ts"]
15
15
  }
@@ -1,7 +0,0 @@
1
- import { runDynamicPhrases } from '../src/core/runner.ts';
2
-
3
- runDynamicPhrases().catch((error: unknown) => {
4
- const message = error instanceof Error ? error.message : String(error);
5
- console.error(`thinking-phrases: ${message}`);
6
- process.exit(1);
7
- });