Improve first-run and channel setup for non-technical users: detect newer Gemini key formats, pin Node 22.22.3, add Control Panel Telegram approve flow, and keep channels/models when config is rewritten. Persist uclaw wizard state via uclaw-meta.json so restarts skip language/persona prompts. Co-authored-by: Cursor <cursoragent@cursor.com>
3.0 KiB
3.0 KiB
name, description, metadata
| name | description | metadata | ||||
|---|---|---|---|---|---|---|
| sg-transport | Singapore transport - bus arrival times, MRT service alerts, carpark availability, traffic. Uses LTA DataMall. |
|
Singapore Transport
Live bus, MRT and traffic data from LTA DataMall.
Before anything works: a key
Unlike the weather API, LTA needs one. It is free.
- Register at https://datamall.lta.gov.sg/content/datamall/en/request-for-api.html
- The AccountKey arrives by email
- Store it as
LTA_ACCOUNT_KEY— never paste it into a message or a filename
Every request sends it as the AccountKey header.
What it does
- Bus arrivals — next three buses at a stop, with crowding
- Train alerts — live disruptions and advisories
- Carpark availability — free lots, useful before driving to a mall
- Travel times — expressway estimates
How to run it
# Next buses at a stop. Bus stop codes are the 5 digits on the pole.
curl -s -H "AccountKey: $LTA_ACCOUNT_KEY" \
"https://datamall2.mytransport.sg/ltaodataservice/v3/BusArrival?BusStopCode=83139" \
| python3 -c "
import json,sys,datetime
d=json.load(sys.stdin)
now=datetime.datetime.now(datetime.timezone.utc)
for svc in d.get('Services',[]):
mins=[]
for k in ('NextBus','NextBus2','NextBus3'):
t=svc.get(k,{}).get('EstimatedArrival')
if not t: continue
eta=datetime.datetime.fromisoformat(t)
mins.append(max(0,round((eta-now).total_seconds()/60)))
load={'SEA':'seats','SDA':'standing','LSD':'packed'}.get(svc['NextBus'].get('Load'),'')
print(f\"Bus {svc['ServiceNo']:5} {', '.join(str(m)+' min' for m in mins) or 'no data':30} {load}\")
"
# Train disruptions right now
curl -s -H "AccountKey: $LTA_ACCOUNT_KEY" \
"https://datamall2.mytransport.sg/ltaodataservice/TrainServiceAlerts" \
| python3 -c "
import json,sys
d=json.load(sys.stdin)['value']
print('Normal service' if d['Status']==1 else 'DISRUPTION')
for m in d.get('Message',[]): print('-', m['Content'])
"
# Carpark availability, filtered by name
curl -s -H "AccountKey: $LTA_ACCOUNT_KEY" \
"https://datamall2.mytransport.sg/ltaodataservice/CarParkAvailabilityv2" \
| python3 -c "
import json,sys
want='VIVOCITY' # change this
for c in json.load(sys.stdin)['value']:
if want.lower() in c['Development'].lower():
print(f\"{c['Development']:40} {c['AvailableLots']:5} lots\")
"
Working notes
- Ask for the bus stop code rather than guessing from a road name. It is printed on the pole and on the app; guessing sends someone to the wrong stop.
- Arrival times come as absolute timestamps; convert to "in N minutes", which is what the person actually wants to know.
- Crowding codes:
SEAseats available ·SDAstanding only ·LSDlimited standing. Translate them — the codes mean nothing to a rider. - Responses cap at 500 records; the carpark and bus-stop lists are paged with
?$skip=500. - If the key is missing or rejected, say which and point at the registration page. Do not silently return nothing.