From 4c391d4e8173cdb032f1f17d539a60cfa0eeffcc Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 31 Jan 2026 07:28:44 +0000 Subject: [PATCH 1/2] Add Task Manager scripts (Moltbot port) --- SKILL_TASK_MANAGER.md | 18 ++++++++ scripts/clawd_notion.py | 89 ++++++++++++++++++++++++++++++++++++++++ scripts/finish_task.py | 55 +++++++++++++++++++++++++ scripts/list_projects.py | 16 ++++++++ scripts/list_tasks.py | 28 +++++++++++++ scripts/select_task.py | 30 ++++++++++++++ 6 files changed, 236 insertions(+) create mode 100644 SKILL_TASK_MANAGER.md create mode 100644 scripts/clawd_notion.py create mode 100644 scripts/finish_task.py create mode 100644 scripts/list_projects.py create mode 100644 scripts/list_tasks.py create mode 100644 scripts/select_task.py diff --git a/SKILL_TASK_MANAGER.md b/SKILL_TASK_MANAGER.md new file mode 100644 index 00000000..1c2add6e --- /dev/null +++ b/SKILL_TASK_MANAGER.md @@ -0,0 +1,18 @@ +# SKILL: Task Manager + +## Commands +- `#task`: Start a new task session. + 1. Run `python3 scripts/list_projects.py` + 2. Ask user to choose project number. + 3. Run `python3 scripts/list_tasks.py ` + 4. Ask user to choose task number (or 'new' for new task - not impl yet, ask for manual ID if needed). + 5. Run `python3 scripts/select_task.py ` + +- `#fertig`: Finish current task. + 1. Ask user for short summary of work. + 2. Run `python3 scripts/finish_task.py ""` + 3. Ask user if they want to push (`git push`). + +## Notes +- Requires `.env.notion` with `NOTION_API_KEY`. +- Stores state in `.dev_session/SESSION_INFO`. diff --git a/scripts/clawd_notion.py b/scripts/clawd_notion.py new file mode 100644 index 00000000..2d792c5f --- /dev/null +++ b/scripts/clawd_notion.py @@ -0,0 +1,89 @@ +import os +import json +import urllib.request +import urllib.error +from datetime import datetime + +# Load env +def load_env(): + paths = [".env.notion", "../.env.notion", "/app/.env.notion"] + for path in paths: + if os.path.exists(path): + with open(path) as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, val = line.split('=', 1) + os.environ[key] = val.strip('"\'') + break + +load_env() +TOKEN = os.environ.get("NOTION_API_KEY") + +def request(method, url, data=None): + if not TOKEN: + print("ERROR: NOTION_API_KEY not found.") + return None + + headers = { + "Authorization": f"Bearer {TOKEN}", + "Content-Type": "application/json", + "Notion-Version": "2022-06-28" + } + + req = urllib.request.Request(url, headers=headers, method=method) + if data: + req.data = json.dumps(data).encode('utf-8') + + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode('utf-8')) + except urllib.error.HTTPError as e: + print(f"HTTP Error {e.code}: {e.read().decode('utf-8')}") + return None + except Exception as e: + print(f"Request Error: {e}") + return None + +def find_db(title): + res = request("POST", "https://api.notion.com/v1/search", { + "query": title, + "filter": {"value": "database", "property": "object"} + }) + if not res: return None + for item in res.get("results", []): + if item["title"][0]["plain_text"].lower() == title.lower(): + return item["id"] + return None + +def query_db(db_id, filter_payload=None): + payload = {} + if filter_payload: payload["filter"] = filter_payload + res = request("POST", f"https://api.notion.com/v1/databases/{db_id}/query", payload) + return res.get("results", []) if res else [] + +def get_title(page): + props = page.get("properties", {}) + for key, val in props.items(): + if val["type"] == "title" and val["title"]: + return val["title"][0]["plain_text"] + return "Untitled" + +def get_status_options(db_id): + res = request("GET", f"https://api.notion.com/v1/databases/{db_id}") + if not res: return [] + props = res.get("properties", {}) + status = props.get("Status", {}).get("status", {}) + return [opt["name"] for opt in status.get("options", [])] + +def update_page(page_id, props): + return request("PATCH", f"https://api.notion.com/v1/pages/{page_id}", {"properties": props}) + +def append_blocks(page_id, blocks): + return request("PATCH", f"https://api.notion.com/v1/blocks/{page_id}/children", {"children": blocks}) + +def create_page(parent_db, props): + return request("POST", "https://api.notion.com/v1/pages", { + "parent": {"database_id": parent_db}, + "properties": props + }) diff --git a/scripts/finish_task.py b/scripts/finish_task.py new file mode 100644 index 00000000..454b33f8 --- /dev/null +++ b/scripts/finish_task.py @@ -0,0 +1,55 @@ +import clawd_notion as notion +import sys +import json +import os +import subprocess +from datetime import datetime + +SESSION_FILE = ".dev_session/SESSION_INFO" + +def main(): + if not os.path.exists(SESSION_FILE): + print("Keine aktive Session.") + return + + # Parse args manually strictly for summary + # Usage: python finish_task.py "My Summary" + summary = sys.argv[1] if len(sys.argv) > 1 else "Update" + + with open(SESSION_FILE) as f: + session = json.load(f) + + task_id = session["task_id"] + + # 1. Update Notion + # Calculate time + start = datetime.fromisoformat(session["start_time"]) + hours = (datetime.now() - start).total_seconds() / 3600 + + # Get current duration + # (Skipping read for now, just appending blocks) + + blocks = [ + { + "object": "block", + "type": "heading_2", + "heading_2": {"rich_text": [{"text": {"content": f"Update {datetime.now().strftime('%Y-%m-%d %H:%M')}"}}]} + }, + { + "object": "block", + "type": "paragraph", + "paragraph": {"rich_text": [{"text": {"content": f"Time invested: {hours:.2f}h\n\n{summary}"}}]} + } + ] + notion.append_blocks(task_id, blocks) + + # 2. Git Commit + subprocess.run(["git", "add", "."]) + subprocess.run(["git", "commit", "-m", f"[{task_id[:4]}] {summary}"]) + + # Cleanup + os.remove(SESSION_FILE) + print("Session beendet, Notion geupdated, Commited.") + +if __name__ == "__main__": + main() diff --git a/scripts/list_projects.py b/scripts/list_projects.py new file mode 100644 index 00000000..c6338bb9 --- /dev/null +++ b/scripts/list_projects.py @@ -0,0 +1,16 @@ +import clawd_notion as notion +import json + +def main(): + db_id = notion.find_db("Projects [UT]") + if not db_id: + print("Projects DB not found.") + return + + projects = notion.query_db(db_id) + print("Verfügbare Projekte:") + for i, p in enumerate(projects): + print(f"{i+1}. {notion.get_title(p)} (ID: {p['id']})") + +if __name__ == "__main__": + main() diff --git a/scripts/list_tasks.py b/scripts/list_tasks.py new file mode 100644 index 00000000..40a56d0a --- /dev/null +++ b/scripts/list_tasks.py @@ -0,0 +1,28 @@ +import clawd_notion as notion +import sys +import json + +def main(): + if len(sys.argv) < 2: + print("Usage: python list_tasks.py ") + return + + project_id = sys.argv[1] + db_id = notion.find_db("Tasks [UT]") + + tasks = notion.query_db(db_id, { + "property": "Project", + "relation": {"contains": project_id} + }) + + # Filter out done tasks if needed, or sort + # For now, just list all linked + + print(f"Tasks für Projekt {project_id}:") + for i, t in enumerate(tasks): + status = t["properties"].get("Status", {}).get("status", {}).get("name", "No Status") + if status == "Done": continue + print(f"{i+1}. [{status}] {notion.get_title(t)} (ID: {t['id']})") + +if __name__ == "__main__": + main() diff --git a/scripts/select_task.py b/scripts/select_task.py new file mode 100644 index 00000000..eb5234a6 --- /dev/null +++ b/scripts/select_task.py @@ -0,0 +1,30 @@ +import clawd_notion as notion +import sys +import json +import os +from datetime import datetime + +SESSION_FILE = ".dev_session/SESSION_INFO" + +def main(): + if len(sys.argv) < 2: + print("Usage: python select_task.py ") + return + + task_id = sys.argv[1] + + # Set status to Doing + notion.update_page(task_id, {"Status": {"status": {"name": "Doing"}}}) + + # Save Session + os.makedirs(os.path.dirname(SESSION_FILE), exist_ok=True) + with open(SESSION_FILE, "w") as f: + json.dump({ + "task_id": task_id, + "start_time": datetime.now().isoformat() + }, f) + + print(f"Session gestartet für Task {task_id}. Status auf 'Doing' gesetzt.") + +if __name__ == "__main__": + main() From 5ad0389aaaaf805912b19d176beabe9a2e1a106e Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sat, 31 Jan 2026 07:37:29 +0000 Subject: [PATCH 2/2] Enhance finish_task: Update Total Duration prop and format status report like dev_session.py --- scripts/clawd_notion.py | 14 ++++++++++++ scripts/finish_task.py | 47 ++++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/scripts/clawd_notion.py b/scripts/clawd_notion.py index 2d792c5f..b0f1d999 100644 --- a/scripts/clawd_notion.py +++ b/scripts/clawd_notion.py @@ -76,6 +76,20 @@ def get_status_options(db_id): status = props.get("Status", {}).get("status", {}) return [opt["name"] for opt in status.get("options", [])] + res = request("GET", f"https://api.notion.com/v1/pages/{page_id}") + return res.get("properties", {}) if res else {} + +def get_property_value(page_id, prop_name): + props = get_page_properties(page_id) + if not props: return None + prop = props.get(prop_name) + if not prop: return None + + if prop["type"] == "number": + return prop["number"] + # Add other types if needed + return None + def update_page(page_id, props): return request("PATCH", f"https://api.notion.com/v1/pages/{page_id}", {"properties": props}) diff --git a/scripts/finish_task.py b/scripts/finish_task.py index 454b33f8..59df9b0c 100644 --- a/scripts/finish_task.py +++ b/scripts/finish_task.py @@ -21,29 +21,56 @@ def main(): task_id = session["task_id"] - # 1. Update Notion - # Calculate time - start = datetime.fromisoformat(session["start_time"]) - hours = (datetime.now() - start).total_seconds() / 3600 + # 1. Calculate Time & Update "Total Duration (h)" + start_utc = datetime.fromisoformat(session["start_time"]) + now_utc = datetime.now() + hours_invested = (now_utc - start_utc).total_seconds() / 3600 - # Get current duration - # (Skipping read for now, just appending blocks) + # Get current duration from Notion to add to it + current_duration = notion.get_property_value(task_id, "Total Duration (h)") or 0.0 + new_total = current_duration + hours_invested + # Update the number property + notion.update_page(task_id, { + "Total Duration (h)": {"number": round(new_total, 2)} + }) + + # 2. Append Status Report Block + # Convert UTC to Berlin Time (UTC+1/UTC+2) - simplified fixed offset for now or use library if available + # Since we can't easily install pytz/zoneinfo in restricted env, we add 1 hour (Winter) manually or just label it UTC for now. + # Better: Use the system time if container timezone is set, otherwise just print formatted string. + # Let's assume container is UTC. Berlin is UTC+1 (Winter). + + # Simple Manual TZ adjustment (approximate, since no pytz) + # We will just format the string nicely and mention "Session Time" + + timestamp_str = now_utc.strftime('%Y-%m-%d %H:%M UTC') + hours_str = f"{int(hours_invested):02d}:{int((hours_invested*60)%60):02d}" + + report_content = ( + f"Investierte Zeit in dieser Session: {hours_str}\n" + f"Neuer Status: Done\n\n" + f"Arbeitszusammenfassung:\n{summary}" + ) + blocks = [ { "object": "block", "type": "heading_2", - "heading_2": {"rich_text": [{"text": {"content": f"Update {datetime.now().strftime('%Y-%m-%d %H:%M')}"}}]} + "heading_2": {"rich_text": [{"text": {"content": f"🤖 Status-Update ({timestamp_str})"}}] } }, { "object": "block", - "type": "paragraph", - "paragraph": {"rich_text": [{"text": {"content": f"Time invested: {hours:.2f}h\n\n{summary}"}}]} + "type": "code", + "code": { + "rich_text": [{"type": "text", "text": {"content": report_content}}], + "language": "yaml" # YAML highlighting makes keys look reddish/colored often + } } ] notion.append_blocks(task_id, blocks) - # 2. Git Commit + # 3. Git Commit subprocess.run(["git", "add", "."]) subprocess.run(["git", "commit", "-m", f"[{task_id[:4]}] {summary}"])