Initial commit: OpenClaw ops workspace
This commit is contained in:
166
scripts/google-sync.py
Executable file
166
scripts/google-sync.py
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user