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.
Files changed (112) hide show
  1. pythonclaw/__init__.py +17 -0
  2. pythonclaw/__main__.py +6 -0
  3. pythonclaw/channels/discord_bot.py +231 -0
  4. pythonclaw/channels/telegram_bot.py +236 -0
  5. pythonclaw/config.py +190 -0
  6. pythonclaw/core/__init__.py +25 -0
  7. pythonclaw/core/agent.py +773 -0
  8. pythonclaw/core/compaction.py +220 -0
  9. pythonclaw/core/knowledge/rag.py +93 -0
  10. pythonclaw/core/llm/anthropic_client.py +107 -0
  11. pythonclaw/core/llm/base.py +26 -0
  12. pythonclaw/core/llm/gemini_client.py +139 -0
  13. pythonclaw/core/llm/openai_compatible.py +39 -0
  14. pythonclaw/core/llm/response.py +57 -0
  15. pythonclaw/core/memory/manager.py +120 -0
  16. pythonclaw/core/memory/storage.py +164 -0
  17. pythonclaw/core/persistent_agent.py +103 -0
  18. pythonclaw/core/retrieval/__init__.py +6 -0
  19. pythonclaw/core/retrieval/chunker.py +78 -0
  20. pythonclaw/core/retrieval/dense.py +152 -0
  21. pythonclaw/core/retrieval/fusion.py +51 -0
  22. pythonclaw/core/retrieval/reranker.py +112 -0
  23. pythonclaw/core/retrieval/retriever.py +166 -0
  24. pythonclaw/core/retrieval/sparse.py +69 -0
  25. pythonclaw/core/session_store.py +269 -0
  26. pythonclaw/core/skill_loader.py +322 -0
  27. pythonclaw/core/skillhub.py +290 -0
  28. pythonclaw/core/tools.py +622 -0
  29. pythonclaw/core/utils.py +64 -0
  30. pythonclaw/daemon.py +221 -0
  31. pythonclaw/init.py +61 -0
  32. pythonclaw/main.py +489 -0
  33. pythonclaw/onboard.py +290 -0
  34. pythonclaw/scheduler/cron.py +310 -0
  35. pythonclaw/scheduler/heartbeat.py +178 -0
  36. pythonclaw/server.py +145 -0
  37. pythonclaw/session_manager.py +104 -0
  38. pythonclaw/templates/persona/demo_persona.md +2 -0
  39. pythonclaw/templates/skills/communication/CATEGORY.md +4 -0
  40. pythonclaw/templates/skills/communication/email/SKILL.md +54 -0
  41. pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc +0 -0
  42. pythonclaw/templates/skills/communication/email/send_email.py +88 -0
  43. pythonclaw/templates/skills/data/CATEGORY.md +4 -0
  44. pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +51 -0
  45. pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc +0 -0
  46. pythonclaw/templates/skills/data/csv_analyzer/analyze.py +138 -0
  47. pythonclaw/templates/skills/data/finance/SKILL.md +41 -0
  48. pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc +0 -0
  49. pythonclaw/templates/skills/data/finance/fetch_quote.py +118 -0
  50. pythonclaw/templates/skills/data/news/SKILL.md +39 -0
  51. pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc +0 -0
  52. pythonclaw/templates/skills/data/news/search_news.py +57 -0
  53. pythonclaw/templates/skills/data/pdf_reader/SKILL.md +40 -0
  54. pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc +0 -0
  55. pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +113 -0
  56. pythonclaw/templates/skills/data/scraper/SKILL.md +39 -0
  57. pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc +0 -0
  58. pythonclaw/templates/skills/data/scraper/scrape.py +92 -0
  59. pythonclaw/templates/skills/data/weather/SKILL.md +42 -0
  60. pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc +0 -0
  61. pythonclaw/templates/skills/data/weather/weather.py +142 -0
  62. pythonclaw/templates/skills/data/youtube/SKILL.md +43 -0
  63. pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc +0 -0
  64. pythonclaw/templates/skills/data/youtube/youtube_info.py +167 -0
  65. pythonclaw/templates/skills/dev/CATEGORY.md +4 -0
  66. pythonclaw/templates/skills/dev/code_runner/SKILL.md +46 -0
  67. pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc +0 -0
  68. pythonclaw/templates/skills/dev/code_runner/run_code.py +117 -0
  69. pythonclaw/templates/skills/dev/github/SKILL.md +52 -0
  70. pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc +0 -0
  71. pythonclaw/templates/skills/dev/github/gh.py +165 -0
  72. pythonclaw/templates/skills/dev/http_request/SKILL.md +40 -0
  73. pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc +0 -0
  74. pythonclaw/templates/skills/dev/http_request/request.py +90 -0
  75. pythonclaw/templates/skills/google/CATEGORY.md +4 -0
  76. pythonclaw/templates/skills/google/workspace/SKILL.md +98 -0
  77. pythonclaw/templates/skills/google/workspace/check_setup.sh +52 -0
  78. pythonclaw/templates/skills/meta/CATEGORY.md +4 -0
  79. pythonclaw/templates/skills/meta/skill_creator/SKILL.md +151 -0
  80. pythonclaw/templates/skills/system/CATEGORY.md +4 -0
  81. pythonclaw/templates/skills/system/change_persona/SKILL.md +41 -0
  82. pythonclaw/templates/skills/system/change_setting/SKILL.md +65 -0
  83. pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc +0 -0
  84. pythonclaw/templates/skills/system/change_setting/update_config.py +129 -0
  85. pythonclaw/templates/skills/system/change_soul/SKILL.md +41 -0
  86. pythonclaw/templates/skills/system/onboarding/SKILL.md +63 -0
  87. pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc +0 -0
  88. pythonclaw/templates/skills/system/onboarding/write_identity.py +218 -0
  89. pythonclaw/templates/skills/system/random/SKILL.md +33 -0
  90. pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc +0 -0
  91. pythonclaw/templates/skills/system/random/random_util.py +45 -0
  92. pythonclaw/templates/skills/system/time/SKILL.md +33 -0
  93. pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc +0 -0
  94. pythonclaw/templates/skills/system/time/time_util.py +81 -0
  95. pythonclaw/templates/skills/text/CATEGORY.md +4 -0
  96. pythonclaw/templates/skills/text/translator/SKILL.md +47 -0
  97. pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc +0 -0
  98. pythonclaw/templates/skills/text/translator/translate.py +66 -0
  99. pythonclaw/templates/skills/web/CATEGORY.md +4 -0
  100. pythonclaw/templates/skills/web/tavily/SKILL.md +61 -0
  101. pythonclaw/templates/soul/SOUL.md +54 -0
  102. pythonclaw/web/__init__.py +1 -0
  103. pythonclaw/web/app.py +585 -0
  104. pythonclaw/web/static/favicon.png +0 -0
  105. pythonclaw/web/static/index.html +1318 -0
  106. pythonclaw/web/static/logo.png +0 -0
  107. pythonclaw-0.2.0.dist-info/METADATA +410 -0
  108. pythonclaw-0.2.0.dist-info/RECORD +112 -0
  109. pythonclaw-0.2.0.dist-info/WHEEL +5 -0
  110. pythonclaw-0.2.0.dist-info/entry_points.txt +2 -0
  111. pythonclaw-0.2.0.dist-info/licenses/LICENSE +21 -0
  112. 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,4 @@
1
+ ---
2
+ name: dev
3
+ description: Developer tools — HTTP requests, GitHub integration, and other dev utilities.
4
+ ---
@@ -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 |