#!/usr/bin/env python3 """ PKM Host API Server DEVONthink + OmniFocus AppleScript 중계용 경량 HTTP 서버. NanoClaw 컨테이너에서 호출. LaunchAgent(GUI 세션)로 실행 필수. 범위: DEVONthink + OmniFocus 전용. 이 이상 확장하지 않을 것. """ import json import subprocess import sys from flask import Flask, request, jsonify app = Flask(__name__) def run_applescript(script: str, timeout: int = 120) -> str: result = subprocess.run( ['osascript', '-e', script], capture_output=True, text=True, timeout=timeout ) if result.returncode != 0: raise RuntimeError(result.stderr.strip()) return result.stdout.strip() # --- DEVONthink --- @app.route('/devonthink/stats') def devonthink_stats(): try: script = ( 'tell application id "DNtp"\n' ' set today to current date\n' ' set time of today to 0\n' ' set stats to {}\n' ' repeat with db in databases\n' ' set dbName to name of db\n' ' set addedCount to 0\n' ' set modifiedCount to 0\n' ' repeat with rec in children of root of db\n' ' try\n' ' if creation date of rec >= today then set addedCount to addedCount + 1\n' ' if modification date of rec >= today then set modifiedCount to modifiedCount + 1\n' ' end try\n' ' end repeat\n' ' if addedCount > 0 or modifiedCount > 0 then\n' ' set end of stats to dbName & ":" & addedCount & ":" & modifiedCount\n' ' end if\n' ' end repeat\n' ' set AppleScript\'s text item delimiters to "|"\n' ' return stats as text\n' 'end tell' ) result = run_applescript(script) stats = {} if result: for item in result.split('|'): parts = item.split(':') if len(parts) == 3: stats[parts[0]] = {'added': int(parts[1]), 'modified': int(parts[2])} total_added = sum(s['added'] for s in stats.values()) total_modified = sum(s['modified'] for s in stats.values()) return jsonify(success=True, data={ 'databases': stats, 'total_added': total_added, 'total_modified': total_modified }) except Exception as e: return jsonify(success=False, error=str(e)), 500 @app.route('/devonthink/search') def devonthink_search(): q = request.args.get('q', '') limit = int(request.args.get('limit', '10')) if not q: return jsonify(success=False, error='q parameter required'), 400 try: script = ( 'tell application id "DNtp"\n' f' set results to search "{q}"\n' ' set output to {}\n' f' set maxCount to {limit}\n' ' set i to 0\n' ' repeat with rec in results\n' ' if i >= maxCount then exit repeat\n' ' set recName to name of rec\n' ' set recDB to name of database of rec\n' ' set recDate to modification date of rec as text\n' ' set end of output to recName & "||" & recDB & "||" & recDate\n' ' set i to i + 1\n' ' end repeat\n' ' set AppleScript\'s text item delimiters to linefeed\n' ' return output as text\n' 'end tell' ) result = run_applescript(script) items = [] if result: for line in result.split('\n'): parts = line.split('||') if len(parts) == 3: items.append({'name': parts[0], 'database': parts[1], 'modified': parts[2]}) return jsonify(success=True, data=items, count=len(items)) except Exception as e: return jsonify(success=False, error=str(e)), 500 @app.route('/devonthink/inbox-count') def devonthink_inbox_count(): try: script = ( 'tell application id "DNtp"\n' ' set inboxDB to database "Inbox"\n' ' return count of children of root of inboxDB\n' 'end tell' ) count = int(run_applescript(script)) return jsonify(success=True, data={'inbox_count': count}) except Exception as e: return jsonify(success=False, error=str(e)), 500 # --- OmniFocus --- @app.route('/omnifocus/stats') def omnifocus_stats(): try: script = ( 'tell application "OmniFocus"\n' ' tell default document\n' ' set today to current date\n' ' set time of today to 0\n' ' set completedCount to count of (every flattened task whose completed is true and completion date >= today)\n' ' set addedCount to count of (every flattened task whose creation date >= today)\n' ' set overdueCount to count of (every flattened task whose completed is false and due date < today and due date is not missing value)\n' ' return (completedCount as text) & "|" & (addedCount as text) & "|" & (overdueCount as text)\n' ' end tell\n' 'end tell' ) result = run_applescript(script) parts = result.split('|') return jsonify(success=True, data={ 'completed': int(parts[0]) if len(parts) > 0 else 0, 'added': int(parts[1]) if len(parts) > 1 else 0, 'overdue': int(parts[2]) if len(parts) > 2 else 0 }) except Exception as e: return jsonify(success=False, error=str(e)), 500 @app.route('/omnifocus/overdue') def omnifocus_overdue(): try: script = ( 'tell application "OmniFocus"\n' ' tell default document\n' ' set today to current date\n' ' set time of today to 0\n' ' set overdueTasks to every flattened task whose completed is false and due date < today and due date is not missing value\n' ' set output to {}\n' ' repeat with t in overdueTasks\n' ' set taskName to name of t\n' ' set dueDate to due date of t as text\n' ' set projName to ""\n' ' try\n' ' set projName to name of containing project of t\n' ' end try\n' ' set end of output to taskName & "||" & projName & "||" & dueDate\n' ' end repeat\n' ' set AppleScript\'s text item delimiters to linefeed\n' ' return output as text\n' ' end tell\n' 'end tell' ) result = run_applescript(script) tasks = [] if result: for line in result.split('\n'): parts = line.split('||') tasks.append({ 'name': parts[0], 'project': parts[1] if len(parts) > 1 else '', 'due_date': parts[2] if len(parts) > 2 else '' }) return jsonify(success=True, data=tasks, count=len(tasks)) except Exception as e: return jsonify(success=False, error=str(e)), 500 @app.route('/omnifocus/today') def omnifocus_today(): try: script = ( 'tell application "OmniFocus"\n' ' tell default document\n' ' set today to current date\n' ' set time of today to 0\n' ' set tomorrow to today + 1 * days\n' ' set todayTasks to every flattened task whose completed is false and ((due date >= today and due date < tomorrow) or (defer date >= today and defer date < tomorrow))\n' ' set output to {}\n' ' repeat with t in todayTasks\n' ' set taskName to name of t\n' ' set projName to ""\n' ' try\n' ' set projName to name of containing project of t\n' ' end try\n' ' set end of output to taskName & "||" & projName\n' ' end repeat\n' ' set AppleScript\'s text item delimiters to linefeed\n' ' return output as text\n' ' end tell\n' 'end tell' ) result = run_applescript(script) tasks = [] if result: for line in result.split('\n'): parts = line.split('||') tasks.append({'name': parts[0], 'project': parts[1] if len(parts) > 1 else ''}) return jsonify(success=True, data=tasks, count=len(tasks)) except Exception as e: return jsonify(success=False, error=str(e)), 500 @app.route('/health') def health(): return jsonify(success=True, service='pkm-api', endpoints=[ '/devonthink/stats', '/devonthink/search?q=', '/devonthink/inbox-count', '/omnifocus/stats', '/omnifocus/overdue', '/omnifocus/today' ]) if __name__ == '__main__': port = int(sys.argv[1]) if len(sys.argv) > 1 else 9900 print(f'PKM API Server starting on port {port}') app.run(host='127.0.0.1', port=port)