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.