From fe96231308551efeddac6d7e4695454dcc016864 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 20 Jul 2026 19:13:01 -0400 Subject: [PATCH] fix(skills): weather skill uses keyless wttr.in instead of key-gated APIs (#349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled weather skill allowed egress only to api.openweathermap.org and api.weatherapi.com — both require an API key — yet declared no key env at all, so it was dead on arrival. Its tool bodies were also prose only, with no runnable command. Repoint it at wttr.in (free, keyless, accepts a city name directly so it maps 1:1 to the location input) and ship two runnable scripts: - scripts/weather-current.sh '{"location":"Tokyo"}' - scripts/weather-forecast.sh '{"location":"Tokyo","days":3}' Both use curl + jq, need no API key, validate input, check the HTTP status, and emit shaped JSON. egress_domains is now [wttr.in]. Kept in sync: the egress-wizard domain hints (egress_step.go), the scanner test fixture, and the weather example in .claude/skills/forge.md. Verified live against wttr.in (current + forecast); scripts are shellcheck-clean; forge-skills and forge-cli/internal/tui suites green. --- .claude/skills/forge.md | 6 +- forge-cli/internal/tui/steps/egress_step.go | 7 +- forge-skills/local/embedded/weather/SKILL.md | 53 +++++++++++++-- .../weather/scripts/weather-current.sh | 57 ++++++++++++++++ .../weather/scripts/weather-forecast.sh | 66 +++++++++++++++++++ forge-skills/local/scanner_test.go | 2 +- 6 files changed, 176 insertions(+), 15 deletions(-) create mode 100755 forge-skills/local/embedded/weather/scripts/weather-current.sh create mode 100755 forge-skills/local/embedded/weather/scripts/weather-forecast.sh diff --git a/.claude/skills/forge.md b/.claude/skills/forge.md index ff14fe7d..18b65f9e 100644 --- a/.claude/skills/forge.md +++ b/.claude/skills/forge.md @@ -1094,12 +1094,12 @@ description: Weather data skill # required, one-line summary metadata: forge: requires: - bins: [curl] # binaries that must be in PATH + bins: [curl, jq] # binaries that must be in PATH env: - required: [WEATHER_API_KEY] + required: [] # keyless: wttr.in needs no API key one_of: [] optional: [] - egress_domains: [api.openweathermap.org] + egress_domains: [wttr.in] denied_tools: [http_request] # tools this skill must NOT use timeout_hint: 60 # suggested execution timeout in seconds --- diff --git a/forge-cli/internal/tui/steps/egress_step.go b/forge-cli/internal/tui/steps/egress_step.go index 592ac168..7a5849c7 100644 --- a/forge-cli/internal/tui/steps/egress_step.go +++ b/forge-cli/internal/tui/steps/egress_step.go @@ -182,10 +182,9 @@ func inferSource(domain string, ctx *tui.WizardContext) string { // Skill domains skillDomains := map[string]string{ - "api.github.com": "github skill", - "github.com": "github skill", - "api.openweathermap.org": "weather skill", - "api.weatherapi.com": "weather skill", + "api.github.com": "github skill", + "github.com": "github skill", + "wttr.in": "weather skill", } if src, ok := skillDomains[domain]; ok { return src diff --git a/forge-skills/local/embedded/weather/SKILL.md b/forge-skills/local/embedded/weather/SKILL.md index d634df24..3b657c77 100644 --- a/forge-skills/local/embedded/weather/SKILL.md +++ b/forge-skills/local/embedded/weather/SKILL.md @@ -12,24 +12,63 @@ metadata: requires: bins: - curl + - jq env: required: [] one_of: [] optional: [] egress_domains: - - api.openweathermap.org - - api.weatherapi.com + - wttr.in --- + +# Weather Skill + +Get current weather and short-range forecasts for any location using +[wttr.in](https://wttr.in). **No API key required** — wttr.in is a free, +keyless service, so this skill works out of the box. + +## Quick Start + +```bash +./scripts/weather-current.sh '{"location": "Tokyo"}' +./scripts/weather-forecast.sh '{"location": "Tokyo", "days": 3}' +``` + +The `location` accepts a city name (`Tokyo`, `New York`), an airport code +(`SFO`), or coordinates (`35.68,139.76`). + ## Tool: weather_current Get current weather for a location. -**Input:** location (string) - City name or coordinates -**Output:** Current temperature, conditions, humidity, and wind speed +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| location | string | yes | City name, airport code, or `lat,lon` coordinates | + +**Output:** JSON object with `location`, `temperature_c`, `temperature_f`, +`feels_like_c`, `conditions`, `humidity`, `wind_kmph`, `wind_dir`, and +`observation_time_utc`. + +```bash +./scripts/weather-current.sh '{"location": "Tokyo"}' +``` ## Tool: weather_forecast -Get weather forecast for a location. +Get a daily weather forecast for a location. + +**Input:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| location | string | yes | City name, airport code, or `lat,lon` coordinates | +| days | integer | no | Number of days, 1-3. Default: 3 (wttr.in caps the free forecast at 3 days) | + +**Output:** JSON object with `location` and a `forecast` array of +`{date, high_c, low_c, high_f, low_f, conditions, chance_of_rain_pct}`. -**Input:** location (string), days (integer: 1-7) -**Output:** Daily forecast with high/low temperatures and conditions +```bash +./scripts/weather-forecast.sh '{"location": "Tokyo", "days": 3}' +``` diff --git a/forge-skills/local/embedded/weather/scripts/weather-current.sh b/forge-skills/local/embedded/weather/scripts/weather-current.sh new file mode 100755 index 00000000..0bc7991c --- /dev/null +++ b/forge-skills/local/embedded/weather/scripts/weather-current.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# weather-current.sh — Current weather for a location via wttr.in. +# Usage: ./weather-current.sh '{"location": "Tokyo"}' +# +# Requires: curl, jq. No API key or environment variable needed — wttr.in +# is a free, keyless service. +set -euo pipefail + +# --- Read input --- +INPUT="${1:-}" +if [ -z "$INPUT" ]; then + echo '{"error": "usage: weather-current.sh {\"location\": \"...\"}"}' >&2 + exit 1 +fi + +# Validate JSON +if ! echo "$INPUT" | jq empty 2>/dev/null; then + echo '{"error": "invalid JSON input"}' >&2 + exit 1 +fi + +# --- Check required fields --- +LOCATION=$(echo "$INPUT" | jq -r '.location // empty') +if [ -z "$LOCATION" ]; then + echo '{"error": "location field is required"}' >&2 + exit 1 +fi + +# URL-encode the location for the wttr.in path segment. +LOCATION_ENC=$(jq -rn --arg s "$LOCATION" '$s|@uri') + +# --- Call wttr.in (j1 = JSON format) --- +RESPONSE=$(curl -s -w "\n%{http_code}" \ + -H "User-Agent: curl" \ + "https://wttr.in/${LOCATION_ENC}?format=j1") + +# Split response body and status code +HTTP_CODE=$(echo "$RESPONSE" | tail -1) +BODY=$(echo "$RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" -ne 200 ]; then + echo "{\"error\": \"wttr.in returned status $HTTP_CODE for location '$LOCATION'\"}" >&2 + exit 1 +fi + +# --- Shape the current conditions --- +echo "$BODY" | jq '{ + location: (.nearest_area[0] | "\(.areaName[0].value), \(.country[0].value)"), + temperature_c: (.current_condition[0].temp_C | tonumber), + temperature_f: (.current_condition[0].temp_F | tonumber), + feels_like_c: (.current_condition[0].FeelsLikeC | tonumber), + conditions: .current_condition[0].weatherDesc[0].value, + humidity: (.current_condition[0].humidity | tonumber), + wind_kmph: (.current_condition[0].windspeedKmph | tonumber), + wind_dir: .current_condition[0].winddir16Point, + observation_time_utc: .current_condition[0].observation_time +}' diff --git a/forge-skills/local/embedded/weather/scripts/weather-forecast.sh b/forge-skills/local/embedded/weather/scripts/weather-forecast.sh new file mode 100755 index 00000000..286fc521 --- /dev/null +++ b/forge-skills/local/embedded/weather/scripts/weather-forecast.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# weather-forecast.sh — Daily forecast for a location via wttr.in. +# Usage: ./weather-forecast.sh '{"location": "Tokyo", "days": 3}' +# +# Requires: curl, jq. No API key or environment variable needed — wttr.in +# is a free, keyless service. wttr.in caps the free forecast at 3 days. +set -euo pipefail + +# --- Read input --- +INPUT="${1:-}" +if [ -z "$INPUT" ]; then + echo '{"error": "usage: weather-forecast.sh {\"location\": \"...\", \"days\": 3}"}' >&2 + exit 1 +fi + +# Validate JSON +if ! echo "$INPUT" | jq empty 2>/dev/null; then + echo '{"error": "invalid JSON input"}' >&2 + exit 1 +fi + +# --- Check required fields --- +LOCATION=$(echo "$INPUT" | jq -r '.location // empty') +if [ -z "$LOCATION" ]; then + echo '{"error": "location field is required"}' >&2 + exit 1 +fi + +# Clamp days to the 1-3 range wttr.in serves for free. +DAYS=$(echo "$INPUT" | jq -r '.days // 3') +case "$DAYS" in + '' | *[!0-9]*) DAYS=3 ;; +esac +if [ "$DAYS" -lt 1 ]; then DAYS=1; fi +if [ "$DAYS" -gt 3 ]; then DAYS=3; fi + +# URL-encode the location for the wttr.in path segment. +LOCATION_ENC=$(jq -rn --arg s "$LOCATION" '$s|@uri') + +# --- Call wttr.in (j1 = JSON format) --- +RESPONSE=$(curl -s -w "\n%{http_code}" \ + -H "User-Agent: curl" \ + "https://wttr.in/${LOCATION_ENC}?format=j1") + +# Split response body and status code +HTTP_CODE=$(echo "$RESPONSE" | tail -1) +BODY=$(echo "$RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" -ne 200 ]; then + echo "{\"error\": \"wttr.in returned status $HTTP_CODE for location '$LOCATION'\"}" >&2 + exit 1 +fi + +# --- Shape the forecast (hourly index 4 ~ midday) --- +echo "$BODY" | jq --argjson days "$DAYS" '{ + location: (.nearest_area[0] | "\(.areaName[0].value), \(.country[0].value)"), + forecast: [ .weather[0:$days][] | { + date: .date, + high_c: (.maxtempC | tonumber), + low_c: (.mintempC | tonumber), + high_f: (.maxtempF | tonumber), + low_f: (.mintempF | tonumber), + conditions: .hourly[4].weatherDesc[0].value, + chance_of_rain_pct: (.hourly[4].chanceofrain | tonumber) + } ] +}' diff --git a/forge-skills/local/scanner_test.go b/forge-skills/local/scanner_test.go index ce68ba8c..f3bde3d3 100644 --- a/forge-skills/local/scanner_test.go +++ b/forge-skills/local/scanner_test.go @@ -38,7 +38,7 @@ metadata: bins: - curl egress_domains: - - api.openweathermap.org + - wttr.in --- ## Tool: weather_current Get current weather.