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.
- package/README.md +230 -142
- package/configs/hn-top.config.json +60 -27
- package/launchd/rss-update.error.log +3 -27
- package/launchd/rss-update.log +308 -0
- package/launchd/task-health.json +54 -0
- package/out/dwyl-quotes.json +1621 -0
- package/out/javascript-tips.json +107 -0
- package/out/league-loading-screen-tips.json +107 -0
- package/out/ruby-tips.json +115 -0
- package/out/settings-linux.json +87 -0
- package/out/settings-mac.json +87 -0
- package/out/settings-windows.json +87 -0
- package/out/typescript-tips.json +131 -0
- package/out/vscode-tips.json +87 -0
- package/out/wow-loading-screen-tips.json +116 -0
- package/package.json +19 -12
- package/scripts/build.ts +3 -3
- package/scripts/debug-hn-hydration.ts +33 -0
- package/scripts/run-rss-update.zsh +25 -3
- package/scripts/show-thinking-phrases-health.ts +74 -0
- package/scripts/trigger-thinking-phrases-scheduler.zsh +50 -0
- package/src/core/config.ts +65 -3
- package/src/core/githubModels.ts +200 -112
- package/src/core/interactive.ts +49 -67
- package/src/core/phraseCache.ts +242 -0
- package/src/core/phraseFormats.ts +243 -0
- package/src/core/presets.ts +1 -1
- package/src/core/runner.ts +246 -113
- package/src/core/scheduler.ts +1 -1
- package/src/core/taskHealth.ts +213 -0
- package/src/core/types.ts +32 -8
- package/src/core/utils.ts +27 -2
- package/src/sources/customJson.ts +28 -18
- package/src/sources/earthquakes.ts +4 -4
- package/src/sources/githubActivity.ts +120 -48
- package/src/sources/hackerNews.ts +19 -7
- package/src/sources/rss.ts +25 -11
- package/src/sources/stocks.ts +31 -10
- package/src/sources/weatherAlerts.ts +173 -7
- package/tsconfig.json +1 -1
- package/scripts/update-rss-settings.ts +0 -7
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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