pythonclaw 0.2.0__py3-none-any.whl
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.
- pythonclaw/__init__.py +17 -0
- pythonclaw/__main__.py +6 -0
- pythonclaw/channels/discord_bot.py +231 -0
- pythonclaw/channels/telegram_bot.py +236 -0
- pythonclaw/config.py +190 -0
- pythonclaw/core/__init__.py +25 -0
- pythonclaw/core/agent.py +773 -0
- pythonclaw/core/compaction.py +220 -0
- pythonclaw/core/knowledge/rag.py +93 -0
- pythonclaw/core/llm/anthropic_client.py +107 -0
- pythonclaw/core/llm/base.py +26 -0
- pythonclaw/core/llm/gemini_client.py +139 -0
- pythonclaw/core/llm/openai_compatible.py +39 -0
- pythonclaw/core/llm/response.py +57 -0
- pythonclaw/core/memory/manager.py +120 -0
- pythonclaw/core/memory/storage.py +164 -0
- pythonclaw/core/persistent_agent.py +103 -0
- pythonclaw/core/retrieval/__init__.py +6 -0
- pythonclaw/core/retrieval/chunker.py +78 -0
- pythonclaw/core/retrieval/dense.py +152 -0
- pythonclaw/core/retrieval/fusion.py +51 -0
- pythonclaw/core/retrieval/reranker.py +112 -0
- pythonclaw/core/retrieval/retriever.py +166 -0
- pythonclaw/core/retrieval/sparse.py +69 -0
- pythonclaw/core/session_store.py +269 -0
- pythonclaw/core/skill_loader.py +322 -0
- pythonclaw/core/skillhub.py +290 -0
- pythonclaw/core/tools.py +622 -0
- pythonclaw/core/utils.py +64 -0
- pythonclaw/daemon.py +221 -0
- pythonclaw/init.py +61 -0
- pythonclaw/main.py +489 -0
- pythonclaw/onboard.py +290 -0
- pythonclaw/scheduler/cron.py +310 -0
- pythonclaw/scheduler/heartbeat.py +178 -0
- pythonclaw/server.py +145 -0
- pythonclaw/session_manager.py +104 -0
- pythonclaw/templates/persona/demo_persona.md +2 -0
- pythonclaw/templates/skills/communication/CATEGORY.md +4 -0
- pythonclaw/templates/skills/communication/email/SKILL.md +54 -0
- pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/communication/email/send_email.py +88 -0
- pythonclaw/templates/skills/data/CATEGORY.md +4 -0
- pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +51 -0
- pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/csv_analyzer/analyze.py +138 -0
- pythonclaw/templates/skills/data/finance/SKILL.md +41 -0
- pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/finance/fetch_quote.py +118 -0
- pythonclaw/templates/skills/data/news/SKILL.md +39 -0
- pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/news/search_news.py +57 -0
- pythonclaw/templates/skills/data/pdf_reader/SKILL.md +40 -0
- pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +113 -0
- pythonclaw/templates/skills/data/scraper/SKILL.md +39 -0
- pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/scraper/scrape.py +92 -0
- pythonclaw/templates/skills/data/weather/SKILL.md +42 -0
- pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/weather/weather.py +142 -0
- pythonclaw/templates/skills/data/youtube/SKILL.md +43 -0
- pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/youtube/youtube_info.py +167 -0
- pythonclaw/templates/skills/dev/CATEGORY.md +4 -0
- pythonclaw/templates/skills/dev/code_runner/SKILL.md +46 -0
- pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/code_runner/run_code.py +117 -0
- pythonclaw/templates/skills/dev/github/SKILL.md +52 -0
- pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/github/gh.py +165 -0
- pythonclaw/templates/skills/dev/http_request/SKILL.md +40 -0
- pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/http_request/request.py +90 -0
- pythonclaw/templates/skills/google/CATEGORY.md +4 -0
- pythonclaw/templates/skills/google/workspace/SKILL.md +98 -0
- pythonclaw/templates/skills/google/workspace/check_setup.sh +52 -0
- pythonclaw/templates/skills/meta/CATEGORY.md +4 -0
- pythonclaw/templates/skills/meta/skill_creator/SKILL.md +151 -0
- pythonclaw/templates/skills/system/CATEGORY.md +4 -0
- pythonclaw/templates/skills/system/change_persona/SKILL.md +41 -0
- pythonclaw/templates/skills/system/change_setting/SKILL.md +65 -0
- pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/change_setting/update_config.py +129 -0
- pythonclaw/templates/skills/system/change_soul/SKILL.md +41 -0
- pythonclaw/templates/skills/system/onboarding/SKILL.md +63 -0
- pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/onboarding/write_identity.py +218 -0
- pythonclaw/templates/skills/system/random/SKILL.md +33 -0
- pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/random/random_util.py +45 -0
- pythonclaw/templates/skills/system/time/SKILL.md +33 -0
- pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/time/time_util.py +81 -0
- pythonclaw/templates/skills/text/CATEGORY.md +4 -0
- pythonclaw/templates/skills/text/translator/SKILL.md +47 -0
- pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/text/translator/translate.py +66 -0
- pythonclaw/templates/skills/web/CATEGORY.md +4 -0
- pythonclaw/templates/skills/web/tavily/SKILL.md +61 -0
- pythonclaw/templates/soul/SOUL.md +54 -0
- pythonclaw/web/__init__.py +1 -0
- pythonclaw/web/app.py +585 -0
- pythonclaw/web/static/favicon.png +0 -0
- pythonclaw/web/static/index.html +1318 -0
- pythonclaw/web/static/logo.png +0 -0
- pythonclaw-0.2.0.dist-info/METADATA +410 -0
- pythonclaw-0.2.0.dist-info/RECORD +112 -0
- pythonclaw-0.2.0.dist-info/WHEEL +5 -0
- pythonclaw-0.2.0.dist-info/entry_points.txt +2 -0
- pythonclaw-0.2.0.dist-info/licenses/LICENSE +21 -0
- pythonclaw-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: weather
|
|
3
|
+
description: >
|
|
4
|
+
Get current weather and forecasts for any city or location worldwide.
|
|
5
|
+
Use when the user asks about weather, temperature, rain, wind, or
|
|
6
|
+
forecasts for any place.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
Fetch weather data using the **Open-Meteo API** — completely free,
|
|
12
|
+
no API key required, and supports any location on Earth.
|
|
13
|
+
|
|
14
|
+
### Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python {skill_path}/weather.py "City Name" [options]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
- `--forecast 3` — include N-day forecast (default: current only)
|
|
22
|
+
- `--format json` — output as JSON (default: human-readable text)
|
|
23
|
+
- `--units imperial` — use Fahrenheit/mph (default: metric)
|
|
24
|
+
|
|
25
|
+
### Examples
|
|
26
|
+
|
|
27
|
+
- "What's the weather in Tokyo?" → `python {skill_path}/weather.py "Tokyo"`
|
|
28
|
+
- "5-day forecast for New York" → `python {skill_path}/weather.py "New York" --forecast 5`
|
|
29
|
+
- "Is it raining in London?" → `python {skill_path}/weather.py "London"`
|
|
30
|
+
- "Weather in Paris in Fahrenheit" → `python {skill_path}/weather.py "Paris" --units imperial`
|
|
31
|
+
|
|
32
|
+
### How It Works
|
|
33
|
+
|
|
34
|
+
1. Geocodes the city name to latitude/longitude via Open-Meteo's geocoding API
|
|
35
|
+
2. Fetches current weather + optional forecast from Open-Meteo's weather API
|
|
36
|
+
3. Returns temperature, humidity, wind speed, weather condition, and precipitation
|
|
37
|
+
|
|
38
|
+
## Resources
|
|
39
|
+
|
|
40
|
+
| File | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `weather.py` | Weather data fetcher via Open-Meteo |
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch weather data from Open-Meteo (free, no API key)."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import urllib.request
|
|
8
|
+
import urllib.parse
|
|
9
|
+
|
|
10
|
+
GEOCODE_URL = "https://geocoding-api.open-meteo.com/v1/search"
|
|
11
|
+
WEATHER_URL = "https://api.open-meteo.com/v1/forecast"
|
|
12
|
+
|
|
13
|
+
WMO_CODES = {
|
|
14
|
+
0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
|
|
15
|
+
45: "Fog", 48: "Depositing rime fog",
|
|
16
|
+
51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
|
|
17
|
+
61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
|
|
18
|
+
71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow",
|
|
19
|
+
77: "Snow grains", 80: "Slight showers", 81: "Moderate showers",
|
|
20
|
+
82: "Violent showers", 85: "Slight snow showers", 86: "Heavy snow showers",
|
|
21
|
+
95: "Thunderstorm", 96: "Thunderstorm with slight hail",
|
|
22
|
+
99: "Thunderstorm with heavy hail",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fetch_json(url: str) -> dict:
|
|
27
|
+
req = urllib.request.Request(url, headers={"User-Agent": "PythonClaw/1.0"})
|
|
28
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
29
|
+
return json.loads(resp.read().decode())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def geocode(city: str) -> dict:
|
|
33
|
+
params = urllib.parse.urlencode({"name": city, "count": 1, "language": "en"})
|
|
34
|
+
data = _fetch_json(f"{GEOCODE_URL}?{params}")
|
|
35
|
+
results = data.get("results", [])
|
|
36
|
+
if not results:
|
|
37
|
+
raise ValueError(f"Location not found: {city}")
|
|
38
|
+
r = results[0]
|
|
39
|
+
return {
|
|
40
|
+
"name": r.get("name", city),
|
|
41
|
+
"country": r.get("country", ""),
|
|
42
|
+
"lat": r["latitude"],
|
|
43
|
+
"lon": r["longitude"],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fetch_weather(lat: float, lon: float, forecast_days: int = 1,
|
|
48
|
+
imperial: bool = False) -> dict:
|
|
49
|
+
temp_unit = "fahrenheit" if imperial else "celsius"
|
|
50
|
+
wind_unit = "mph" if imperial else "kmh"
|
|
51
|
+
params = {
|
|
52
|
+
"latitude": lat,
|
|
53
|
+
"longitude": lon,
|
|
54
|
+
"current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,precipitation",
|
|
55
|
+
"temperature_unit": temp_unit,
|
|
56
|
+
"wind_speed_unit": wind_unit,
|
|
57
|
+
}
|
|
58
|
+
if forecast_days > 1:
|
|
59
|
+
params["daily"] = "temperature_2m_max,temperature_2m_min,weather_code,precipitation_sum"
|
|
60
|
+
params["forecast_days"] = forecast_days
|
|
61
|
+
params["timezone"] = "auto"
|
|
62
|
+
|
|
63
|
+
qs = urllib.parse.urlencode(params)
|
|
64
|
+
return _fetch_json(f"{WEATHER_URL}?{qs}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def format_current(location: dict, data: dict, imperial: bool) -> str:
|
|
68
|
+
c = data.get("current", {})
|
|
69
|
+
temp = c.get("temperature_2m", "?")
|
|
70
|
+
humidity = c.get("relative_humidity_2m", "?")
|
|
71
|
+
wind = c.get("wind_speed_10m", "?")
|
|
72
|
+
precip = c.get("precipitation", 0)
|
|
73
|
+
code = c.get("weather_code", 0)
|
|
74
|
+
condition = WMO_CODES.get(code, f"Code {code}")
|
|
75
|
+
|
|
76
|
+
t_unit = "F" if imperial else "C"
|
|
77
|
+
w_unit = "mph" if imperial else "km/h"
|
|
78
|
+
|
|
79
|
+
lines = [
|
|
80
|
+
f"Weather in {location['name']}, {location['country']}",
|
|
81
|
+
f" Condition: {condition}",
|
|
82
|
+
f" Temperature: {temp}°{t_unit}",
|
|
83
|
+
f" Humidity: {humidity}%",
|
|
84
|
+
f" Wind: {wind} {w_unit}",
|
|
85
|
+
]
|
|
86
|
+
if precip > 0:
|
|
87
|
+
lines.append(f" Precipitation: {precip} mm")
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def format_forecast(data: dict, imperial: bool) -> str:
|
|
92
|
+
daily = data.get("daily", {})
|
|
93
|
+
dates = daily.get("time", [])
|
|
94
|
+
if not dates:
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
t_unit = "F" if imperial else "C"
|
|
98
|
+
lines = ["\nForecast:"]
|
|
99
|
+
for i, date in enumerate(dates):
|
|
100
|
+
hi = daily["temperature_2m_max"][i]
|
|
101
|
+
lo = daily["temperature_2m_min"][i]
|
|
102
|
+
code = daily["weather_code"][i]
|
|
103
|
+
precip = daily["precipitation_sum"][i]
|
|
104
|
+
condition = WMO_CODES.get(code, f"Code {code}")
|
|
105
|
+
line = f" {date}: {lo}–{hi}°{t_unit} {condition}"
|
|
106
|
+
if precip > 0:
|
|
107
|
+
line += f" (precip: {precip}mm)"
|
|
108
|
+
lines.append(line)
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
parser = argparse.ArgumentParser(description="Get weather for any location.")
|
|
114
|
+
parser.add_argument("city", help="City name (e.g. 'Tokyo', 'New York')")
|
|
115
|
+
parser.add_argument("--forecast", type=int, default=1,
|
|
116
|
+
help="Number of forecast days (default: current only)")
|
|
117
|
+
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
118
|
+
parser.add_argument("--units", choices=["metric", "imperial"], default="metric")
|
|
119
|
+
args = parser.parse_args()
|
|
120
|
+
|
|
121
|
+
imperial = args.units == "imperial"
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
loc = geocode(args.city)
|
|
125
|
+
data = fetch_weather(loc["lat"], loc["lon"],
|
|
126
|
+
forecast_days=max(args.forecast, 1),
|
|
127
|
+
imperial=imperial)
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
if args.format == "json":
|
|
133
|
+
data["location"] = loc
|
|
134
|
+
print(json.dumps(data, indent=2))
|
|
135
|
+
else:
|
|
136
|
+
print(format_current(loc, data, imperial))
|
|
137
|
+
if args.forecast > 1:
|
|
138
|
+
print(format_forecast(data, imperial))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: youtube
|
|
3
|
+
description: >
|
|
4
|
+
Get YouTube video information, metadata, and transcripts/subtitles.
|
|
5
|
+
Use when the user asks about a YouTube video, wants a video summary,
|
|
6
|
+
needs a transcript, or asks to extract info from a YouTube URL.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
Extract information and transcripts from YouTube videos.
|
|
12
|
+
|
|
13
|
+
### Prerequisites
|
|
14
|
+
|
|
15
|
+
Install dependencies: `pip install yt-dlp youtube-transcript-api`
|
|
16
|
+
|
|
17
|
+
### Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python {skill_path}/youtube_info.py URL [command] [options]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
- `info` (default) — video title, duration, views, description
|
|
25
|
+
- `transcript` — full transcript/subtitles
|
|
26
|
+
- `search` — search YouTube for videos (uses URL arg as search query)
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
- `--lang en` — transcript language (default: en)
|
|
30
|
+
- `--format json` — output as JSON
|
|
31
|
+
- `--max 5` — max results for search (default: 5)
|
|
32
|
+
|
|
33
|
+
### Examples
|
|
34
|
+
|
|
35
|
+
- "What's this video about? https://youtube.com/watch?v=xyz" → `youtube_info.py URL info`
|
|
36
|
+
- "Get the transcript of this video" → `youtube_info.py URL transcript`
|
|
37
|
+
- "Search YouTube for Python tutorials" → `youtube_info.py "Python tutorials" search`
|
|
38
|
+
|
|
39
|
+
## Resources
|
|
40
|
+
|
|
41
|
+
| File | Description |
|
|
42
|
+
|------|-------------|
|
|
43
|
+
| `youtube_info.py` | YouTube info and transcript extractor |
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract YouTube video info and transcripts."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _try_import(pkg, install_name):
|
|
10
|
+
try:
|
|
11
|
+
return __import__(pkg)
|
|
12
|
+
except ImportError:
|
|
13
|
+
print(f"Error: {install_name} not installed. Run: pip install {install_name}",
|
|
14
|
+
file=sys.stderr)
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_info(url: str) -> dict:
|
|
19
|
+
yt_dlp = _try_import("yt_dlp", "yt-dlp")
|
|
20
|
+
opts = {
|
|
21
|
+
"quiet": True,
|
|
22
|
+
"no_warnings": True,
|
|
23
|
+
"skip_download": True,
|
|
24
|
+
}
|
|
25
|
+
with yt_dlp.YoutubeDL(opts) as ydl:
|
|
26
|
+
info = ydl.extract_info(url, download=False)
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
"title": info.get("title", ""),
|
|
30
|
+
"channel": info.get("uploader", ""),
|
|
31
|
+
"duration": info.get("duration", 0),
|
|
32
|
+
"durationFormatted": _format_duration(info.get("duration", 0)),
|
|
33
|
+
"views": info.get("view_count", 0),
|
|
34
|
+
"likes": info.get("like_count"),
|
|
35
|
+
"uploadDate": info.get("upload_date", ""),
|
|
36
|
+
"description": (info.get("description") or "")[:1000],
|
|
37
|
+
"url": info.get("webpage_url", url),
|
|
38
|
+
"thumbnail": info.get("thumbnail", ""),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_transcript(url: str, lang: str = "en") -> list[dict]:
|
|
43
|
+
_try_import("yt_dlp", "yt-dlp")
|
|
44
|
+
ytt = _try_import("youtube_transcript_api", "youtube-transcript-api")
|
|
45
|
+
|
|
46
|
+
video_id = _extract_video_id(url)
|
|
47
|
+
if not video_id:
|
|
48
|
+
raise ValueError(f"Could not extract video ID from: {url}")
|
|
49
|
+
|
|
50
|
+
api = ytt.YouTubeTranscriptApi()
|
|
51
|
+
try:
|
|
52
|
+
transcript = api.fetch(video_id, languages=[lang])
|
|
53
|
+
except Exception:
|
|
54
|
+
transcript = api.fetch(video_id)
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
"start": round(entry.start, 1),
|
|
59
|
+
"duration": round(entry.duration, 1),
|
|
60
|
+
"text": entry.text,
|
|
61
|
+
}
|
|
62
|
+
for entry in transcript
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def search_youtube(query: str, max_results: int = 5) -> list[dict]:
|
|
67
|
+
yt_dlp = _try_import("yt_dlp", "yt-dlp")
|
|
68
|
+
opts = {
|
|
69
|
+
"quiet": True,
|
|
70
|
+
"no_warnings": True,
|
|
71
|
+
"skip_download": True,
|
|
72
|
+
"extract_flat": True,
|
|
73
|
+
"playlist_items": f"1:{max_results}",
|
|
74
|
+
}
|
|
75
|
+
with yt_dlp.YoutubeDL(opts) as ydl:
|
|
76
|
+
result = ydl.extract_info(f"ytsearch{max_results}:{query}", download=False)
|
|
77
|
+
|
|
78
|
+
entries = result.get("entries", [])
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
"title": e.get("title", ""),
|
|
82
|
+
"url": e.get("url", ""),
|
|
83
|
+
"channel": e.get("uploader", e.get("channel", "")),
|
|
84
|
+
"duration": e.get("duration"),
|
|
85
|
+
}
|
|
86
|
+
for e in entries
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _extract_video_id(url: str) -> str | None:
|
|
91
|
+
import re
|
|
92
|
+
patterns = [
|
|
93
|
+
r"(?:v=|/v/|youtu\.be/)([a-zA-Z0-9_-]{11})",
|
|
94
|
+
r"^([a-zA-Z0-9_-]{11})$",
|
|
95
|
+
]
|
|
96
|
+
for p in patterns:
|
|
97
|
+
m = re.search(p, url)
|
|
98
|
+
if m:
|
|
99
|
+
return m.group(1)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _format_duration(seconds: int) -> str:
|
|
104
|
+
if not seconds:
|
|
105
|
+
return "N/A"
|
|
106
|
+
h, remainder = divmod(seconds, 3600)
|
|
107
|
+
m, s = divmod(remainder, 60)
|
|
108
|
+
if h:
|
|
109
|
+
return f"{h}:{m:02d}:{s:02d}"
|
|
110
|
+
return f"{m}:{s:02d}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main():
|
|
114
|
+
parser = argparse.ArgumentParser(description="YouTube video info & transcripts.")
|
|
115
|
+
parser.add_argument("url", help="YouTube URL or search query")
|
|
116
|
+
parser.add_argument("command", nargs="?", default="info",
|
|
117
|
+
choices=["info", "transcript", "search"])
|
|
118
|
+
parser.add_argument("--lang", default="en", help="Transcript language")
|
|
119
|
+
parser.add_argument("--max", type=int, default=5, help="Max search results")
|
|
120
|
+
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
121
|
+
args = parser.parse_args()
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
if args.command == "info":
|
|
125
|
+
data = get_info(args.url)
|
|
126
|
+
if args.format == "json":
|
|
127
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
128
|
+
else:
|
|
129
|
+
print(f"Title: {data['title']}")
|
|
130
|
+
print(f"Channel: {data['channel']}")
|
|
131
|
+
print(f"Duration: {data['durationFormatted']}")
|
|
132
|
+
print(f"Views: {data['views']:,}")
|
|
133
|
+
if data["likes"]:
|
|
134
|
+
print(f"Likes: {data['likes']:,}")
|
|
135
|
+
print(f"Uploaded: {data['uploadDate']}")
|
|
136
|
+
print(f"URL: {data['url']}")
|
|
137
|
+
if data["description"]:
|
|
138
|
+
print(f"\nDescription:\n{data['description']}")
|
|
139
|
+
|
|
140
|
+
elif args.command == "transcript":
|
|
141
|
+
entries = get_transcript(args.url, lang=args.lang)
|
|
142
|
+
if args.format == "json":
|
|
143
|
+
print(json.dumps(entries, indent=2, ensure_ascii=False))
|
|
144
|
+
else:
|
|
145
|
+
for e in entries:
|
|
146
|
+
ts = _format_duration(int(e["start"]))
|
|
147
|
+
print(f"[{ts}] {e['text']}")
|
|
148
|
+
|
|
149
|
+
elif args.command == "search":
|
|
150
|
+
results = search_youtube(args.url, max_results=args.max)
|
|
151
|
+
if args.format == "json":
|
|
152
|
+
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
153
|
+
else:
|
|
154
|
+
for i, r in enumerate(results, 1):
|
|
155
|
+
dur = _format_duration(r["duration"]) if r["duration"] else "?"
|
|
156
|
+
print(f"{i}. {r['title']} [{dur}]")
|
|
157
|
+
print(f" {r['channel']}")
|
|
158
|
+
print(f" {r['url']}")
|
|
159
|
+
print()
|
|
160
|
+
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code_runner
|
|
3
|
+
description: >
|
|
4
|
+
Execute Python code snippets safely in an isolated subprocess with
|
|
5
|
+
timeout protection. Use when the user asks to run, test, or evaluate
|
|
6
|
+
Python code, calculate expressions, or prototype logic.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
Run Python code in an isolated subprocess with stdout/stderr capture
|
|
12
|
+
and timeout protection. No extra dependencies needed.
|
|
13
|
+
|
|
14
|
+
### Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python {skill_path}/run_code.py [options]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Input methods:
|
|
21
|
+
- `--code "print(2+2)"` — inline code string
|
|
22
|
+
- `--file script.py` — run a Python file
|
|
23
|
+
- `stdin` — pipe code via stdin
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
- `--timeout 30` — max execution time in seconds (default: 30)
|
|
27
|
+
- `--format json` — output as JSON with stdout, stderr, exit code
|
|
28
|
+
- `--no-capture` — stream output directly (no capture)
|
|
29
|
+
|
|
30
|
+
### Examples
|
|
31
|
+
|
|
32
|
+
- "Calculate 2^100" → `run_code.py --code "print(2**100)"`
|
|
33
|
+
- "Run this Python snippet" → `run_code.py --code "import math; print(math.pi)"`
|
|
34
|
+
- "Test this function" → `run_code.py --file test.py`
|
|
35
|
+
|
|
36
|
+
### Security
|
|
37
|
+
|
|
38
|
+
- Code runs in a **subprocess** (not eval/exec in the agent process)
|
|
39
|
+
- Timeout prevents infinite loops
|
|
40
|
+
- Working directory is the project root
|
|
41
|
+
|
|
42
|
+
## Resources
|
|
43
|
+
|
|
44
|
+
| File | Description |
|
|
45
|
+
|------|-------------|
|
|
46
|
+
| `run_code.py` | Safe Python code executor |
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Execute Python code in an isolated subprocess with timeout."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_code(code: str, timeout: int = 30, cwd: str | None = None) -> dict:
|
|
13
|
+
"""Run Python code in a subprocess and capture output."""
|
|
14
|
+
with tempfile.NamedTemporaryFile(
|
|
15
|
+
mode="w", suffix=".py", delete=False, encoding="utf-8"
|
|
16
|
+
) as f:
|
|
17
|
+
f.write(code)
|
|
18
|
+
tmp_path = f.name
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
python = sys.executable
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
[python, tmp_path],
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
timeout=timeout,
|
|
27
|
+
cwd=cwd,
|
|
28
|
+
)
|
|
29
|
+
return {
|
|
30
|
+
"exitCode": result.returncode,
|
|
31
|
+
"stdout": result.stdout,
|
|
32
|
+
"stderr": result.stderr,
|
|
33
|
+
"timedOut": False,
|
|
34
|
+
}
|
|
35
|
+
except subprocess.TimeoutExpired:
|
|
36
|
+
return {
|
|
37
|
+
"exitCode": -1,
|
|
38
|
+
"stdout": "",
|
|
39
|
+
"stderr": f"Execution timed out after {timeout}s",
|
|
40
|
+
"timedOut": True,
|
|
41
|
+
}
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
return {
|
|
44
|
+
"exitCode": -1,
|
|
45
|
+
"stdout": "",
|
|
46
|
+
"stderr": str(exc),
|
|
47
|
+
"timedOut": False,
|
|
48
|
+
}
|
|
49
|
+
finally:
|
|
50
|
+
os.unlink(tmp_path)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_file(path: str, timeout: int = 30) -> dict:
|
|
54
|
+
"""Run a Python file in a subprocess."""
|
|
55
|
+
try:
|
|
56
|
+
python = sys.executable
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
[python, path],
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
timeout=timeout,
|
|
62
|
+
)
|
|
63
|
+
return {
|
|
64
|
+
"exitCode": result.returncode,
|
|
65
|
+
"stdout": result.stdout,
|
|
66
|
+
"stderr": result.stderr,
|
|
67
|
+
"timedOut": False,
|
|
68
|
+
}
|
|
69
|
+
except subprocess.TimeoutExpired:
|
|
70
|
+
return {
|
|
71
|
+
"exitCode": -1,
|
|
72
|
+
"stdout": "",
|
|
73
|
+
"stderr": f"Execution timed out after {timeout}s",
|
|
74
|
+
"timedOut": True,
|
|
75
|
+
}
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
return {
|
|
78
|
+
"exitCode": -1,
|
|
79
|
+
"stdout": "",
|
|
80
|
+
"stderr": str(exc),
|
|
81
|
+
"timedOut": False,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
parser = argparse.ArgumentParser(description="Execute Python code safely.")
|
|
87
|
+
parser.add_argument("--code", default=None, help="Python code to execute")
|
|
88
|
+
parser.add_argument("--file", default=None, help="Python file to execute")
|
|
89
|
+
parser.add_argument("--timeout", type=int, default=30, help="Timeout in seconds")
|
|
90
|
+
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
91
|
+
args = parser.parse_args()
|
|
92
|
+
|
|
93
|
+
if args.file:
|
|
94
|
+
result = run_file(args.file, timeout=args.timeout)
|
|
95
|
+
elif args.code:
|
|
96
|
+
result = run_code(args.code, timeout=args.timeout)
|
|
97
|
+
elif not sys.stdin.isatty():
|
|
98
|
+
code = sys.stdin.read()
|
|
99
|
+
result = run_code(code, timeout=args.timeout)
|
|
100
|
+
else:
|
|
101
|
+
parser.error("Provide --code, --file, or pipe code via stdin.")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if args.format == "json":
|
|
105
|
+
print(json.dumps(result, indent=2))
|
|
106
|
+
else:
|
|
107
|
+
if result["stdout"]:
|
|
108
|
+
print(result["stdout"], end="")
|
|
109
|
+
if result["stderr"]:
|
|
110
|
+
print(result["stderr"], end="", file=sys.stderr)
|
|
111
|
+
if result["timedOut"]:
|
|
112
|
+
print(f"\n[Timed out after {args.timeout}s]", file=sys.stderr)
|
|
113
|
+
sys.exit(result["exitCode"])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: github
|
|
3
|
+
description: >
|
|
4
|
+
Interact with the GitHub API — list repos, create issues, view PRs,
|
|
5
|
+
manage releases. Use when the user asks about GitHub repositories,
|
|
6
|
+
issues, pull requests, or wants to perform any GitHub operation.
|
|
7
|
+
Requires a GitHub Personal Access Token in pythonclaw.json.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Instructions
|
|
11
|
+
|
|
12
|
+
Interact with the GitHub REST API. A Personal Access Token (PAT) is
|
|
13
|
+
read from `skills.github.token` in `pythonclaw.json`.
|
|
14
|
+
|
|
15
|
+
### Prerequisites
|
|
16
|
+
|
|
17
|
+
1. Create a PAT at https://github.com/settings/tokens
|
|
18
|
+
2. Configure in `pythonclaw.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
"skills": {
|
|
22
|
+
"github": {
|
|
23
|
+
"token": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Usage
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python {skill_path}/gh.py <command> [options]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Commands:
|
|
35
|
+
- `repos <user>` — list user's public repositories
|
|
36
|
+
- `repo <owner/repo>` — get repository details
|
|
37
|
+
- `issues <owner/repo>` — list open issues
|
|
38
|
+
- `create-issue <owner/repo> --title "..." --body "..."` — create an issue
|
|
39
|
+
- `prs <owner/repo>` — list open pull requests
|
|
40
|
+
- `pr <owner/repo> <number>` — get PR details
|
|
41
|
+
|
|
42
|
+
### Examples
|
|
43
|
+
|
|
44
|
+
- "List my GitHub repositories" → `gh.py repos <username>`
|
|
45
|
+
- "Show open issues on pythonclaw" → `gh.py issues user/pythonclaw`
|
|
46
|
+
- "Create an issue for the login bug" → `gh.py create-issue user/repo --title "Login bug" --body "..."`
|
|
47
|
+
|
|
48
|
+
## Resources
|
|
49
|
+
|
|
50
|
+
| File | Description |
|
|
51
|
+
|------|-------------|
|
|
52
|
+
| `gh.py` | GitHub API client |
|
|
Binary file
|