diff --git a/workflowy-helper/.gitignore b/workflowy-helper/.gitignore
new file mode 100644
index 0000000..7d8a661
--- /dev/null
+++ b/workflowy-helper/.gitignore
@@ -0,0 +1,5 @@
+env/
+.env/
+venv/
+__pycache__/
+*.egg-info/
diff --git a/workflowy-helper/requirements.txt b/workflowy-helper/requirements.txt
new file mode 100644
index 0000000..8a0b32b
--- /dev/null
+++ b/workflowy-helper/requirements.txt
@@ -0,0 +1,5 @@
+browser-cookie3>=0.19.0
+rich>=13.8.0
+requests>=2.32.0
+uuid>=1.30
+click>=8.0.0
diff --git a/workflowy-helper/setup.py b/workflowy-helper/setup.py
new file mode 100644
index 0000000..d0cae94
--- /dev/null
+++ b/workflowy-helper/setup.py
@@ -0,0 +1,18 @@
+from setuptools import setup
+
+with open('requirements.txt') as f:
+ requirements = f.read().splitlines()
+
+setup(
+ name='wf',
+ packages=['wf'],
+ install_requires=requirements,
+ description='A command line interface for using WorkFlowy.',
+ author='Gregory Leeman',
+ author_email='email@gregoryleeman.com',
+ entry_points={
+ 'console_scripts': [
+ 'wf = wf.script:cli'
+ ],
+ },
+)
diff --git a/workflowy-helper/wf/__init__.py b/workflowy-helper/wf/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/workflowy-helper/wf/script.py b/workflowy-helper/wf/script.py
new file mode 100755
index 0000000..894c900
--- /dev/null
+++ b/workflowy-helper/wf/script.py
@@ -0,0 +1,951 @@
+#!/Users/gl6/env/bin/python
+
+import os
+import json
+import requests
+import uuid
+import browser_cookie3
+import time
+from datetime import datetime, timedelta
+import re
+import click
+import logging
+from rich.console import Console
+from rich.theme import Theme
+from rich.logging import RichHandler
+
+
+
+STORAGE_FILE = os.path.join(os.path.expanduser('~'), '.wf.json')
+
+INBOX_ID = "13859f14-79ab-b758-7224-8dad0236f1e2"
+TASKS_ID = "042d7e11-f613-856c-fb97-c0e6ee7ece05"
+DIARY_ID = "01205285-f5b1-8026-0cfa-942f514e297e"
+PLANNER_IDS = {
+ "life": "f6f993a3-c696-c070-296e-6e5055fc834f",
+ "admin": "d3911123-138e-8eb3-f2ba-2495d8169660",
+ "art": "c2e9f4b2-59ce-d127-4da6-949e54ec1442",
+ "health": "4fc929b8-f04b-0dde-9a1a-9d889a13316d",
+ "mind": "c6db70a1-72e3-dbcc-f4b9-bfb10a1b4280",
+ "social": "60e3d667-eb40-3d26-cc3f-6b151cc5efa4",
+ "church": "5c68fad5-ad2b-8018-6535-b1462aed1277",
+ "work": "4c4970fc-9023-f861-1392-dbf88dd89187",
+ "affirm": "a1412d52-c72c-0612-f5b1-48874ef03943",
+}
+
+solarized_theme = Theme({
+ "base03": "bright_black",
+ "base02": "black",
+ "base01": "bright_green",
+ "base00": "bright_yellow",
+ "base0": "bright_blue",
+ "base1": "bright_cyan",
+ "base2": "white",
+ "base3": "bright_white",
+ "orange": "bright_red",
+ "violet": "bright_magenta",
+ "red": "red",
+ "bold red": "bold red",
+ "underline base03": "underline bright_black",
+ "underline base02": "underline black",
+ "underline base01": "underline bright_green",
+ "underline base00": "underline bright_yellow",
+ "underline base0": "underline bright_blue",
+ "underline base1": "underline bright_cyan",
+ "underline base2": "underline white",
+ "underline base3": "underline bright_white",
+ "underline orange": "bright_red underline",
+ "underline violet": "bright_magenta underline",
+ "bold base03": "bold bright_black",
+ "bold base02": "bold black",
+ "bold base01": "bold bright_green",
+ "bold base00": "bold bright_yellow",
+ "bold base0": "bold bright_blue",
+ "bold base1": "bold bright_cyan",
+ "bold base2": "bold white",
+ "bold base3": "bold bright_white",
+ "bold orange": "bright_red bold",
+ "bold violet": "bright_magenta bold",
+})
+
+
+console = Console(highlight=False, theme=solarized_theme)
+logging.basicConfig(handlers=[RichHandler(level="NOTSET", console=console)])
+logger = logging.getLogger('rich')
+logger.setLevel(logging.INFO)
+
+
+# helpers {{{
+
+
+def get_ordinal(n):
+ if 10 <= n % 100 <= 20:
+ suffix = 'th'
+ else:
+ suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
+ return str(n) + suffix
+
+
+def get_today():
+ now = datetime.now()
+ return now.strftime("%a, %b %d, %Y")
+ # return now.strftime(f"%a {get_ordinal(now.day)} %b")
+
+def get_sunday():
+ now = datetime.now()
+ sunday = now - timedelta(days=now.weekday()) - timedelta(days=1)
+ return sunday.strftime("%a, %b %d, %Y")
+ # return now.strftime(f"%a {get_ordinal(now.day)} %b")
+
+
+def generate_uuid():
+ return str(uuid.uuid4())
+
+
+def load_storage():
+ if os.path.exists(STORAGE_FILE):
+ logger.debug(f"Loading storage from {STORAGE_FILE}")
+ with open(STORAGE_FILE, "r") as f:
+ return json.load(f)
+ return {}
+
+
+def save_storage(data):
+ with open(STORAGE_FILE, "w") as f:
+ json.dump(data, f)
+
+
+def save_to_storage(key, value):
+ storage = load_storage()
+ storage[key] = value
+ save_storage(storage)
+
+
+def load_from_storage(key):
+ storage = load_storage()
+ if key in storage:
+ return storage[key]
+ return {}
+
+
+def clear_storage():
+ if os.path.exists(STORAGE_FILE):
+ os.remove(STORAGE_FILE)
+
+
+# }}}
+
+
+def refresh_cookie(): # {{{
+ logger.debug("Refreshing session cookie")
+ cookies = browser_cookie3.chrome()
+ session_cookie = None
+ for cookie in cookies:
+ if cookie.name == "sessionid" and "workflowy.com" in cookie.domain:
+ session_cookie = cookie.value
+ break
+ if session_cookie:
+ logger.debug(f"Found session cookie: {session_cookie}")
+ save_to_storage("session_cookie", session_cookie)
+ return True
+ else:
+ logger.error("Session cookie not found. Are you logged into Workflowy?")
+ return False
+
+# }}}
+
+
+def check_cookie(): # {{{
+ session_cookie = load_from_storage("session_cookie")
+ if session_cookie:
+ logger.debug(f"Session cookie found: {session_cookie}")
+ else:
+ logger.error("Session cookie not found. Run refresh_cookie() first.")
+ return False
+
+ url = "https://workflowy.com/get_initialization_data?client_version=15"
+ headers = {"Cookie": f"sessionid={session_cookie}"}
+ response = requests.get(url, headers=headers)
+ if response.status_code == 200:
+ logger.debug("Session cookie is valid.")
+ return True
+ else:
+ logger.error(f"Session cookie is invalid. Status code {response.status_code}")
+ return False
+
+# }}}
+
+
+def refresh_workflowy_data(): # {{{
+ session_cookie = load_from_storage("session_cookie")
+ if not session_cookie:
+ console.log("Session cookie not found. Run refresh_cookie() first.")
+ return
+
+ url = "https://workflowy.com/get_initialization_data?client_version=15"
+ headers = {"Cookie": f"sessionid={session_cookie}"}
+ response = requests.get(url, headers=headers)
+
+ if response.status_code == 200:
+ try:
+ data = response.json()
+ globals_data = {item[0]: item[1] for item in data["globals"]}
+
+ storage = load_storage()
+ storage["userid"] = globals_data["USER_ID"]
+ storage["joined"] = data["projectTreeData"]["mainProjectTreeInfo"][
+ "dateJoinedTimestampInSeconds"
+ ]
+ storage["transid"] = data["projectTreeData"]["mainProjectTreeInfo"][
+ "initialMostRecentOperationTransactionId"
+ ]
+ storage["pollid"] = generate_uuid() # Simulate g() + g()
+ storage["root"] = {"nm": "root", "ch": data["projectTreeData"]["mainProjectTreeInfo"]["rootProjectChildren"]}
+ save_storage(storage)
+ console.log("Successfully refreshed and saved Workflowy data.")
+ return True
+ except Exception as e:
+ console.log(f"Error parsing response: {e}")
+ return False
+ else:
+ console.log(f"Error fetching Workflowy data: Status code {response.status_code}")
+ return False
+
+# }}}
+
+
+def check_workflowy_data(): # {{{
+ storage = load_storage()
+ if not storage:
+ console.log("Workflowy data is not initialized. Run the initialization first.")
+ return False
+ if not storage.get("userid") or not storage.get("transid") or not storage.get(
+ "pollid"
+ ):
+ console.log("Workflowy data is incomplete. Run the initialization again.")
+ return False
+ return True
+
+# }}}
+
+
+def clip_to_workflowy(name, description, parent_id): # {{{
+ storage = load_storage()
+ if not storage:
+ console.log("Workflowy data is not initialized. Run the initialization first.")
+ return
+
+ new_uuid = generate_uuid()
+ timestamp = int(time.time()) - storage.get("joined", 0)
+
+ request = [
+ {
+ "most_recent_operation_transaction_id": storage.get("transid"),
+ "operations": [
+ {
+ "type": "create",
+ "data": {
+ "projectid": new_uuid,
+ "parentid": parent_id,
+ "priority": 9999,
+ },
+ "client_timestamp": timestamp,
+ "undo_data": {},
+ },
+ {
+ "type": "edit",
+ "data": {
+ "projectid": new_uuid,
+ "name": name,
+ "description": description,
+ },
+ "client_timestamp": timestamp,
+ "undo_data": {
+ "previous_last_modified": timestamp,
+ "previous_name": "",
+ "previous_description": "",
+ },
+ },
+ ],
+ }
+ ]
+
+ data = {
+ "client_id": "2015-11-17 19:25:15.397732",
+ "client_version": 15,
+ "push_poll_id": storage.get("pollid"),
+ "push_poll_data": json.dumps(request),
+ "crosscheck_user_id": storage.get("userid"),
+ }
+
+ headers = {"Cookie": f"sessionid={storage.get('session_cookie')}"}
+
+ response = requests.post("https://workflowy.com/push_and_poll", data=data, headers=headers)
+ if response.status_code == 200:
+ resp_obj = response.json()
+ if resp_obj.get("logged_out"):
+ console.log("Error: Logged out of Workflowy!")
+ elif not resp_obj.get("results"):
+ console.log("Error: Unknown error!")
+ else:
+ storage["transid"] = resp_obj["results"][0][
+ "new_most_recent_operation_transaction_id"
+ ]
+ save_storage(storage)
+ console.print("[green]Successfully clipped to Workflowy![/green]")
+ else:
+ console.log(f"Error: Failed with status code {response.status_code}")
+
+
+# }}}
+
+
+def simplify_project(project_data, full_data, follow_mirrors=False): # {{{
+ if project_data.get("metadata", {}).get("mirror", {}).get("originalId"):
+ if follow_mirrors:
+ originalId = project_data["metadata"]["mirror"]["originalId"]
+ project_data, full_data = find_project_by_id(full_data, full_data, originalId)
+ else:
+ return None, full_data
+ if project_data.get("metadata", {}).get("isReferencesRoot"):
+ if project_data.get("ch"):
+ if len(project_data["ch"]) > 0:
+ project_data["nm"] = "backlinks"
+ project_data["metadata"]["layoutMode"] = "h1"
+ else:
+ return None, full_data
+ else:
+ return None, full_data
+ if project_data:
+ simplified_project = {
+ "name": project_data.get("nm", ""),
+ "id": project_data.get("id", ""),
+ "children": [],
+ "description": project_data.get("no", "").rstrip().replace("\n$", ""),
+ "format": project_data.get("metadata", {}).get("layoutMode", None)
+ }
+ children = project_data.get("ch", [])
+ for child in children:
+ simplified_child, full_data = simplify_project(child, full_data, follow_mirrors=follow_mirrors)
+ if simplified_child:
+ simplified_project["children"].append(simplified_child)
+ return simplified_project, full_data
+ return None, full_data
+
+# }}}
+
+
+def flatten(children): # {{{
+ flattened = []
+ for child in children:
+ try:
+ grand_children = child.get("children", [])
+ except Exception as e:
+ print(f"child: {child} {e}")
+ grand_children = []
+ child["children"] = []
+ flattened.append(child)
+ flattened.extend(flatten(grand_children))
+ return flattened
+# }}}
+
+
+def flatten_project(project_data): # {{{
+ project_data["children"] = flatten(project_data.get("children", []))
+ return project_data
+
+# }}}
+
+
+# def filter_project_any(project_data, filters): # {{{
+# filtered_project = {
+# "name": project_data["name"],
+# "id": project_data.get("id", ""),
+# # "description": project_data["description"],
+# "children": [],
+# "format": project_data["format"]
+# }
+# children = project_data.get("children", [])
+# for child in children:
+# include = False
+# for filter_text in filters:
+# if filter_text.lower() in child["name"].lower():
+# include = True
+# break
+# if include:
+# filtered_project["children"].append(filter_project_any(child, filters))
+# return filtered_project
+
+def filter_project_any(project_data, filters, include_headers=False, all_children=False): # {{{
+ include = False
+ if include_headers:
+ if project_data["format"] == "h1" or project_data["format"] == "h2":
+ include = True
+ for filter_text in filters:
+ if filter_text in project_data["name"]:
+ include = True
+ break
+
+ children = []
+ for child in project_data.get("children", []):
+ if all_children and include:
+ logger.debug(f"Not filtering children of {project_data['name']}")
+ pass
+ else:
+ child = filter_project_any(child, filters, include_headers=include_headers, all_children=all_children)
+ if child:
+ children.append(child)
+
+ project_data["children"] = children
+
+ if include or children:
+ return project_data
+ else:
+ return None
+
+# }}}
+
+
+def filter_project_all(project_data, filters, include_headers=False, all_children=False): # {{{
+ include = True
+ if include_headers and (project_data["format"] == "h1" or project_data["format"] == "h2"):
+ pass
+ else:
+ for filter_text in filters:
+ if filter_text not in project_data["name"]:
+ if filter_text not in project_data.get("description", ""):
+ include = False
+ break
+ if include:
+ logger.debug(f"Including {project_data['name']}")
+ logger.debug(f"all_children: {all_children}")
+ children = []
+ for child in project_data.get("children", []):
+ if all_children and include:
+ logger.debug(f"Not filtering children of {project_data['name']}")
+ pass
+ else:
+ child = filter_project_all(child, filters, include_headers=include_headers, all_children=all_children)
+ if child:
+ children.append(child)
+
+ project_data["children"] = children
+
+ if include or children:
+ return project_data
+ else:
+ return None
+
+# }}}
+
+
+def replace(project_data, regex, replacement): # {{{
+ project_data["name"] = re.sub(regex, replacement, project_data["name"])
+ children = project_data.get("children", [])
+ for child in children:
+ replace(child, regex, replacement)
+ return project_data
+
+# }}}
+
+
+def strip(project_data, regex): # {{{
+ project_data = replace(project_data, regex, "")
+ return project_data
+
+# }}}
+
+
+def remove_double_spaces(project_data): # {{{
+ # project_data["name"] = re.sub(r"\s+", " ", project_data["name"])
+ project_data["name"] = project_data["name"].replace(" ", " ")
+ children = project_data.get("children", [])
+ for child in children:
+ remove_double_spaces(child)
+ return project_data
+# }}}
+
+
+highlights = {
+ "@done": "green",
+ "@missed": "red",
+ "@na": "blue",
+ "#WORK": "red",
+}
+
+
+def highlight(project_data): # {{{
+ for key, value in highlights.items():
+ regex = f"{key}\\b"
+ project_data["name"] = re.sub(regex, f"[{value}]{key}[/]", project_data["name"])
+ children = project_data.get("children", [])
+ for child in children:
+ highlight(child)
+ return project_data
+# }}}
+
+
+colors1 = [
+ [["xTASK", "#READY"], "blue", True],
+ [["xTASK", "#MAYBE"], "violet", True],
+ [["xTASK", "#WAITING"], "cyan", True],
+ [["xTASK", "#DAILY"], "green", True],
+ [["xTASK", "#WEEKLY"], "green", True],
+ [["xTASK", "#IRREGULAR"], "green", True],
+ [["xPROJECT", "#ACTIVE"], "magenta", True],
+ [["xPROJECT", "#STALLED"], "cyan", True],
+ [["xPROJECT", "#PLANT"], "orange", True],
+ [["xSOMEDAY"], "violet", True],
+ [["xHABIT"], "orange", True],
+ [["xSTORY"], "cyan", True],
+ [["xGOAL"], "yellow", True],
+ [["xVIS"], "red", True],
+ [["xRESPONSIBILITY"], "red", True],
+ [["Sunday"], "underline violet", False],
+ [["Monday"], "underline red", False],
+ [["Tuesday"], "underline cyan", False],
+ [["Wednesday"], "underline magenta", False],
+ [["Thursday"], "underline green", False],
+ [["Friday"], "underline yellow", False],
+ [["Saturday"], "underline blue", False],
+ [["#r"], "black on blue", False],
+ [["#g"], "black on yellow", False],
+ [["#w"], "red", False],
+ [["#p"], "green", False],
+]
+
+
+def recolor(project_data, colors): # {{{
+ for rule in colors:
+ keywords = rule[0]
+ color = rule[1]
+ hide = rule[2]
+ match = True
+ for keyword in keywords:
+ if keyword not in project_data["name"]:
+ match = False
+ break
+ if match:
+ project_data["name"] = f"[{color}]{project_data['name']}[/]"
+ if hide:
+ for keyword in keywords:
+ project_data["name"] = project_data["name"].replace(keyword, "")
+ project_data["name"] = project_data["name"].rstrip() + f" [base01]{" ".join(keywords)}[/]"
+
+ children = project_data.get("children", [])
+ for child in children:
+ recolor(child, colors)
+ return project_data
+# }}}
+
+def print_pretty(data, indent=0, color="grey", show_description=True, show_id=False): # {{{
+ try:
+ for item in data["children"]:
+ if item["name"] == "backlinks":
+ # console.print(" " * indent + "[base01]• backlinks[/]")
+ continue
+ console.print(" " * indent + f"[base3]•[/] [{color}]{item['name']}[/][base01]{' ' + item['id'].split('-')[4] if show_id else ''}[/]")
+ if item["description"] and show_description:
+ console.print(" " * (indent + 1) + f"[base01]{item['description'].replace('\n', '\n' + ' ' * (indent + 1))}[/]")
+ if item["children"]:
+ print_pretty(item, indent + 1, color, show_description=show_description, show_id=show_id)
+
+ except Exception as e:
+ console.log(f"Error: {e}")
+
+
+# }}}
+
+
+def generate_d3_mindmap_html(data, show_description=True, show_id=False): # {{{
+ import json
+ import re
+
+ # Map of Rich tag colors to hex values or CSS color names
+ color_map = {
+ "base3": "#073642",
+ "base2": "#002b36",
+ "base1": "#586e75",
+ "base0": "#657b83",
+ "base00": "#839496",
+ "base01": "#93a1a1",
+ "base02": "#eee8d5",
+ "base03": "#fdf6e3",
+ "yellow": "#b58900",
+ "orange": "#cb4b16",
+ "red": "#dc322f",
+ "magenta": "#d33682",
+ "violet": "#6c71c4",
+ "blue": "#268bd2",
+ "cyan": "#2aa198",
+ "green": "#859900",
+ }
+
+ def parse_rich_tags(text):
+ text = re.sub(r"\[(underline)? ?([a-zA-Z0-9]+)?\](.*?)\[/\]",
+ lambda m: f'