167 lines
6.3 KiB
Python
Executable File
167 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
WORKSPACE = Path('/home/node/.openclaw/workspace')
|
|
STATE_PATH = WORKSPACE / 'state' / 'projects.json'
|
|
REMINDER_LIST = Path('/home/node/.openclaw/skills/reminder/scripts/list.sh')
|
|
|
|
|
|
@dataclass
|
|
class Reminder:
|
|
when_local: str
|
|
message: str
|
|
reminder_id: str
|
|
|
|
|
|
def run(cmd: list[str]) -> str:
|
|
res = subprocess.run(cmd, text=True, capture_output=True)
|
|
if res.returncode != 0:
|
|
raise RuntimeError((res.stderr or res.stdout).strip())
|
|
return res.stdout
|
|
|
|
|
|
def load_state() -> dict[str, Any]:
|
|
return json.loads(STATE_PATH.read_text())
|
|
|
|
|
|
def save_state(state: dict[str, Any]) -> None:
|
|
STATE_PATH.write_text(json.dumps(state, indent=2) + "\n")
|
|
|
|
|
|
def slug(text: str) -> str:
|
|
return re.sub(r'[^a-z0-9]+', '-', text.lower()).strip('-')
|
|
|
|
|
|
def parse_reminders() -> list[Reminder]:
|
|
out = run(['bash', str(REMINDER_LIST)])
|
|
reminders: list[Reminder] = []
|
|
current_when = None
|
|
current_msg = None
|
|
current_id = None
|
|
for line in out.splitlines():
|
|
if line.startswith('⏰ '):
|
|
current_when = line.replace('⏰ ', '', 1).strip()
|
|
current_msg = None
|
|
current_id = None
|
|
elif line.strip().startswith('ID:'):
|
|
current_id = line.split('ID:', 1)[1].strip()
|
|
if current_when and current_msg and current_id:
|
|
reminders.append(Reminder(when_local=current_when, message=current_msg, reminder_id=current_id))
|
|
current_when = None
|
|
current_msg = None
|
|
current_id = None
|
|
elif current_when and line.startswith(' ') and current_msg is None and line.strip() and not line.strip().startswith('ID:'):
|
|
current_msg = line.strip()
|
|
return reminders
|
|
|
|
|
|
def ensure_followup_task(state: dict[str, Any], title: str, notes: str = '') -> str:
|
|
tasklist = state['google']['tasklists']['claw_follow_ups']['id']
|
|
out = run([
|
|
'gws', 'tasks', 'tasks', 'insert',
|
|
'--params', json.dumps({'tasklist': tasklist}),
|
|
'--json', json.dumps({'title': title, 'notes': notes})
|
|
])
|
|
obj = json.loads(out)
|
|
return obj['id']
|
|
|
|
|
|
def ensure_calendar_event(state: dict[str, Any], summary: str, start_utc: str, end_utc: str, description: str = '') -> str:
|
|
cal_id = state['google']['calendar']['claw_ops']['id']
|
|
existing_raw = run([
|
|
'gws', 'calendar', 'events', 'list',
|
|
'--params', json.dumps({'calendarId': cal_id})
|
|
])
|
|
existing = json.loads(existing_raw)
|
|
for item in existing.get('items', []):
|
|
if item.get('summary') == summary and item.get('start', {}).get('dateTime') == start_utc:
|
|
return item['id']
|
|
out = run([
|
|
'gws', 'calendar', 'events', 'insert',
|
|
'--params', json.dumps({'calendarId': cal_id}),
|
|
'--json', json.dumps({
|
|
'summary': summary,
|
|
'description': description,
|
|
'start': {'dateTime': start_utc, 'timeZone': 'UTC'},
|
|
'end': {'dateTime': end_utc, 'timeZone': 'UTC'}
|
|
})
|
|
])
|
|
obj = json.loads(out)
|
|
return obj['id']
|
|
|
|
|
|
def sync_projects(state: dict[str, Any]) -> dict[str, Any]:
|
|
project_list_id = state['google']['tasklists']['claw_projects']['id']
|
|
created = []
|
|
for project in state['projects']:
|
|
ids = set(project.get('task_ids', []))
|
|
if not ids:
|
|
title = f"[Project] {project['title']}"
|
|
notes = f"Project ID: {project['id']}\nStatus: {project['status']}\nNext action: {project.get('next_action', '')}\nNotes ref: {project.get('notes_ref', '')}"
|
|
out = run([
|
|
'gws', 'tasks', 'tasks', 'insert',
|
|
'--params', json.dumps({'tasklist': project_list_id}),
|
|
'--json', json.dumps({'title': title, 'notes': notes})
|
|
])
|
|
obj = json.loads(out)
|
|
project.setdefault('task_ids', []).append(obj['id'])
|
|
created.append({'type': 'project-task', 'project_id': project['id'], 'task_id': obj['id']})
|
|
if project.get('next_action') and len(project.get('task_ids', [])) < 2:
|
|
title = f"[{project['title']}] {project['next_action']}"
|
|
notes = f"Project ID: {project['id']}\nSource: {project.get('notes_ref', '')}"
|
|
out = run([
|
|
'gws', 'tasks', 'tasks', 'insert',
|
|
'--params', json.dumps({'tasklist': project_list_id}),
|
|
'--json', json.dumps({'title': title, 'notes': notes})
|
|
])
|
|
obj = json.loads(out)
|
|
project.setdefault('task_ids', []).append(obj['id'])
|
|
created.append({'type': 'next-action-task', 'project_id': project['id'], 'task_id': obj['id']})
|
|
return {'created': created}
|
|
|
|
|
|
def sync_reminders(state: dict[str, Any]) -> dict[str, Any]:
|
|
reminder_state = state.setdefault('reminders', {'synced': {}})
|
|
synced = reminder_state.setdefault('synced', {})
|
|
created = []
|
|
skipped = []
|
|
for rem in parse_reminders():
|
|
if rem.reminder_id in synced:
|
|
continue
|
|
msg = rem.message.replace('\\n', '\n')
|
|
lower = msg.lower()
|
|
if 'vehicle registration renewal' in lower:
|
|
# known time conversion from existing reminder local labels
|
|
if '09:00 cdt' in rem.when_local.lower():
|
|
start, end = '2026-04-15T14:00:00Z', '2026-04-15T14:30:00Z'
|
|
elif '16:00 cdt' in rem.when_local.lower():
|
|
start, end = '2026-04-15T21:00:00Z', '2026-04-15T21:30:00Z'
|
|
else:
|
|
skipped.append({'reminder_id': rem.reminder_id, 'reason': 'unsupported-known-reminder-time'})
|
|
continue
|
|
event_id = ensure_calendar_event(state, 'Vehicle registration renewal - Tesla Model Y (VJF3166)', start, end, msg)
|
|
synced[rem.reminder_id] = {'kind': 'calendar', 'event_id': event_id}
|
|
created.append({'reminder_id': rem.reminder_id, 'calendar_event_id': event_id})
|
|
else:
|
|
skipped.append({'reminder_id': rem.reminder_id, 'reason': 'past-or-unscheduled-manual-review'})
|
|
return {'created': created, 'skipped': skipped}
|
|
|
|
|
|
def main() -> None:
|
|
state = load_state()
|
|
result = {
|
|
'projects': sync_projects(state),
|
|
'reminders': sync_reminders(state),
|
|
}
|
|
save_state(state)
|
|
print(json.dumps(result, indent=2))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|