From 2f7901ee4202a26757a5237e987a8ec5a1799e29 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Wed, 4 Feb 2026 23:32:00 -0800 Subject: [PATCH] remove atlas for now (#108) --- skills/.curated/atlas/LICENSE.txt | 201 -------- skills/.curated/atlas/SKILL.md | 99 ---- skills/.curated/atlas/agents/openai.yaml | 6 - skills/.curated/atlas/assets/atlas-small.svg | 4 - skills/.curated/atlas/assets/atlas.png | Bin 11162 -> 0 bytes .../.curated/atlas/references/atlas-data.md | 26 - skills/.curated/atlas/scripts/atlas_cli.py | 473 ------------------ skills/.curated/atlas/scripts/atlas_common.py | 168 ------- 8 files changed, 977 deletions(-) delete mode 100644 skills/.curated/atlas/LICENSE.txt delete mode 100644 skills/.curated/atlas/SKILL.md delete mode 100644 skills/.curated/atlas/agents/openai.yaml delete mode 100644 skills/.curated/atlas/assets/atlas-small.svg delete mode 100644 skills/.curated/atlas/assets/atlas.png delete mode 100644 skills/.curated/atlas/references/atlas-data.md delete mode 100644 skills/.curated/atlas/scripts/atlas_cli.py delete mode 100644 skills/.curated/atlas/scripts/atlas_common.py diff --git a/skills/.curated/atlas/LICENSE.txt b/skills/.curated/atlas/LICENSE.txt deleted file mode 100644 index 13e25df..0000000 --- a/skills/.curated/atlas/LICENSE.txt +++ /dev/null @@ -1,201 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf of - any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don\'t include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/skills/.curated/atlas/SKILL.md b/skills/.curated/atlas/SKILL.md deleted file mode 100644 index c4c600c..0000000 --- a/skills/.curated/atlas/SKILL.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -name: "atlas" -description: "macOS-only AppleScript control for the ChatGPT Atlas desktop app. Use only when the user explicitly asks to control Atlas tabs/bookmarks/history on macOS and the \"ChatGPT Atlas\" app is installed; do not trigger for general browser tasks or non-macOS environments." ---- - - -# Atlas Control (macOS) - -Use the bundled CLI to control Atlas and inspect local browser data. - -## Quick Start - -Set a stable path to the CLI: - -```bash -export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" -export ATLAS_CLI="$CODEX_HOME/skills/atlas/scripts/atlas_cli.py" -``` - -User-scoped skills install under `$CODEX_HOME/skills` (default: `~/.codex/skills`). - -Then run: - -```bash -uv run --python 3.12 python "$ATLAS_CLI" app-name -uv run --python 3.12 python "$ATLAS_CLI" tabs --json -``` - -The CLI requires the Atlas app bundle in `/Applications` or `~/Applications`: - -- `ChatGPT Atlas` - -If AppleScript fails with a permissions error, grant Automation permission in System Settings > Privacy & Security > Automation, allowing your terminal to control ChatGPT Atlas. - -## Tabs Workflow - -1. List tabs to get `window_id` and `tab_index`: - -```bash -uv run --python 3.12 python "$ATLAS_CLI" tabs -``` - -2. Focus a tab using the `window_id` and `tab_index` from the listing: - -```bash -uv run --python 3.12 python "$ATLAS_CLI" focus-tab -``` - -3. Open a new tab: - -```bash -uv run --python 3.12 python "$ATLAS_CLI" open-tab "https://chatgpt.com/" -``` - -Optional maintenance commands: - -```bash -uv run --python 3.12 python "$ATLAS_CLI" reload-tab -uv run --python 3.12 python "$ATLAS_CLI" close-tab -``` - -## Bookmarks and History - -Atlas stores Chromium-style profile data under `~/Library/Application Support/com.openai.atlas/browser-data/host/`. - -List bookmarks: - -```bash -uv run --python 3.12 python "$ATLAS_CLI" bookmarks --limit 100 -``` - -Search bookmarks: - -```bash -uv run --python 3.12 python "$ATLAS_CLI" bookmarks --search "docs" -``` - -Search history: - -```bash -uv run --python 3.12 python "$ATLAS_CLI" history --search "openai docs" --limit 50 -``` - -History for today (local time): - -```bash -uv run --python 3.12 python "$ATLAS_CLI" history --today --limit 200 --json -``` - -The history command copies the SQLite database to a temporary location to avoid lock errors. - -If history looks stale or empty, ask the user which Atlas install they are using, then check both Atlas data roots and inspect the one with the most recent `History` file: - -- `~/Library/Application Support/com.openai.atlas/browser-data/host/` -- `~/Library/Application Support/com.openai.atlas.beta/browser-data/host/` - -## References - -Read `references/atlas-data.md` in the skill folder (for example, `$CODEX_HOME/skills/atlas/references/atlas-data.md`) when adjusting data paths or timestamps. diff --git a/skills/.curated/atlas/agents/openai.yaml b/skills/.curated/atlas/agents/openai.yaml deleted file mode 100644 index 6612200..0000000 --- a/skills/.curated/atlas/agents/openai.yaml +++ /dev/null @@ -1,6 +0,0 @@ -interface: - display_name: "Atlas" - short_description: "Use to manage tabs in ChatGPT Atlas and access your browser history." - icon_small: "./assets/atlas-small.svg" - icon_large: "./assets/atlas.png" - default_prompt: "List all of my currently open browser tabs and call out duplicates." diff --git a/skills/.curated/atlas/assets/atlas-small.svg b/skills/.curated/atlas/assets/atlas-small.svg deleted file mode 100644 index e631bc0..0000000 --- a/skills/.curated/atlas/assets/atlas-small.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/skills/.curated/atlas/assets/atlas.png b/skills/.curated/atlas/assets/atlas.png deleted file mode 100644 index 854bb737666316c3eee50a6fefea28f5db90f351..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11162 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 sO1)Wnj6PdF$aR|#WLolj$(3pN|C0U5=#B8T` - -Key files in the active profile: - -- `History` (SQLite DB, table `urls`) -- `Bookmarks` (JSON) - -The active profile is derived from `profile.last_used` in `Local State`, with a fallback to `Default`. - -## AppleScript Assumptions - -Atlas appears to expose a Safari-style AppleScript dictionary with: - -- `every window` -- `every tab of w` -- `active tab index` -- `make new tab with properties {URL:"..."}` -- `close tab ` -- `reload tab ` - -Treat Atlas as a single app with one AppleScript target. diff --git a/skills/.curated/atlas/scripts/atlas_cli.py b/skills/.curated/atlas/scripts/atlas_cli.py deleted file mode 100644 index 96207ef..0000000 --- a/skills/.curated/atlas/scripts/atlas_cli.py +++ /dev/null @@ -1,473 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import sqlite3 -import sys -from dataclasses import asdict, dataclass -from datetime import datetime, timedelta -from pathlib import Path -from typing import Iterable, Sequence - -from atlas_common import ( - AtlasError, - copy_sqlite_db, - detect_atlas_app_name, - detect_tab_capable_app_name, - get_bookmarks_path, - get_history_path, - tell_atlas, -) - -ROW_SEP = "|||" -CHROME_EPOCH_OFFSET_SECONDS = 11_644_473_600 -DEFAULT_HISTORY_LIMIT = 200 -DEFAULT_BOOKMARK_LIMIT = 200 - - -@dataclass(slots=True) -class Tab: - title: str - url: str - window_id: int - tab_index: int - is_active: bool - - -@dataclass(slots=True) -class HistoryRow: - id: int - url: str - title: str - last_visited_at: str - - -@dataclass(slots=True) -class Bookmark: - id: str - name: str - url: str - date_added: str - folder: str | None = None - - -def _escape_applescript_string(value: str) -> str: - return value.replace("\\", "\\\\").replace('"', '\\"') - - -def _chrome_microseconds_to_iso_date(value: str | None) -> str: - if not value: - return "unknown" - - try: - timestamp = int(value) - except ValueError: - return "unknown" - - unix_seconds = timestamp / 1_000_000 - CHROME_EPOCH_OFFSET_SECONDS - try: - dt = datetime.fromtimestamp(unix_seconds) - except (OverflowError, OSError, ValueError): - return "unknown" - return dt.date().isoformat() - - -def _chrome_time_from_unix_seconds(unix_seconds: float) -> int: - return int((unix_seconds + CHROME_EPOCH_OFFSET_SECONDS) * 1_000_000) - - -def _chrome_time_bounds_today() -> tuple[int, int]: - now = datetime.now().astimezone() - start = now.replace(hour=0, minute=0, second=0, microsecond=0) - end = start + timedelta(days=1) - return ( - _chrome_time_from_unix_seconds(start.timestamp()), - _chrome_time_from_unix_seconds(end.timestamp()), - ) - - -def get_tabs() -> list[Tab]: - tab_app = detect_tab_capable_app_name() - script_body = f""" -set tabList to {{}} - -repeat with w in every window - set windowId to id of w - set tabIndex to 0 - set activeIndex to active tab index of w - try - set winTabs to every tab of w - repeat with t in winTabs - set tabIndex to tabIndex + 1 - set tabTitle to title of t - set tabURL to URL of t - set isActive to (tabIndex = activeIndex) - set end of tabList to {{tabTitle, tabURL, windowId, tabIndex, isActive}} - end repeat - end try -end repeat - -set output to "" -repeat with tabInfo in tabList - set output to output & item 1 of tabInfo & "{ROW_SEP}" & item 2 of tabInfo & "{ROW_SEP}" & item 3 of tabInfo & "{ROW_SEP}" & item 4 of tabInfo & "{ROW_SEP}" & item 5 of tabInfo & "\\n" -end repeat - -return output -""".strip() - - raw = tell_atlas(script_body, app_name=tab_app) - if not raw: - return [] - - tabs: list[Tab] = [] - for line in raw.splitlines(): - parts = line.split(ROW_SEP) - if len(parts) != 5: - continue - title, url, window_id, tab_index, is_active = parts - try: - tabs.append( - Tab( - title=title, - url=url, - window_id=int(window_id), - tab_index=int(tab_index), - is_active=is_active == "true", - ) - ) - except ValueError: - continue - - return tabs - - -def open_new_tab(url: str) -> None: - escaped_url = _escape_applescript_string(url) - script_body = f""" -activate -delay 0.1 -open location "{escaped_url}" - -activate -""".strip() - tell_atlas(script_body) - - -def focus_tab(window_id: int, tab_index: int) -> None: - tab_app = detect_tab_capable_app_name() - script_body = f""" -activate -set _wnd to first window whose id is {window_id} -set index of _wnd to 1 -set active tab index of _wnd to {tab_index} -return true -""".strip() - tell_atlas(script_body, app_name=tab_app) - - -def close_tab(window_id: int, tab_index: int) -> None: - tab_app = detect_tab_capable_app_name() - script_body = f""" -set _wnd to first window whose id is {window_id} -tell _wnd - close tab {tab_index} -end tell -""".strip() - tell_atlas(script_body, app_name=tab_app) - - -def reload_tab(window_id: int, tab_index: int) -> None: - tab_app = detect_tab_capable_app_name() - script_body = f""" -set _wnd to first window whose id is {window_id} -tell _wnd - reload tab {tab_index} -end tell -""".strip() - tell_atlas(script_body, app_name=tab_app) - - -def _history_query( - terms: Sequence[str], - limit: int, - *, - chrome_start: int | None = None, - chrome_end: int | None = None, -) -> tuple[str, list[str | int]]: - params: list[str | int] = [] - conditions: list[str] = [] - - if terms: - for term in terms: - conditions.append("(url LIKE ? OR title LIKE ?)") - like = f"%{term}%" - params.extend([like, like]) - - if chrome_start is not None: - conditions.append("last_visit_time >= ?") - params.append(chrome_start) - - if chrome_end is not None: - conditions.append("last_visit_time < ?") - params.append(chrome_end) - - where_clause = "" - if conditions: - where_clause = "\nWHERE " + " AND ".join(conditions) - - base = f""" -WITH filtered AS ( - SELECT id, url, title, last_visit_time - FROM urls{where_clause} -), -ranked AS ( - SELECT - id, - url, - title, - last_visit_time, - ROW_NUMBER() OVER ( - PARTITION BY url - ORDER BY last_visit_time DESC, id DESC - ) AS rn - FROM filtered -) -SELECT - id, - url, - title, - datetime(last_visit_time / 1000000 + (strftime('%s', '1601-01-01')), 'unixepoch', 'localtime') AS lastVisitedAt -FROM ranked -WHERE rn = 1 -ORDER BY last_visit_time DESC -LIMIT ? -""".strip() - - params.append(limit) - return base, params - - -def search_history(search_text: str | None, limit: int, *, today: bool = False) -> list[HistoryRow]: - history_path = get_history_path() - db_copy = copy_sqlite_db(history_path) - - terms = [t for t in (search_text or "").split() if t] - chrome_start: int | None = None - chrome_end: int | None = None - if today: - chrome_start, chrome_end = _chrome_time_bounds_today() - - query, params = _history_query(terms, limit, chrome_start=chrome_start, chrome_end=chrome_end) - - conn = sqlite3.connect(db_copy) - conn.row_factory = sqlite3.Row - try: - rows = conn.execute(query, params).fetchall() - finally: - conn.close() - - history: list[HistoryRow] = [] - for row in rows: - history.append( - HistoryRow( - id=int(row["id"]), - url=str(row["url"] or ""), - title=str(row["title"] or ""), - last_visited_at=str(row["lastVisitedAt"] or ""), - ) - ) - return history - - -def _iter_bookmark_nodes(node: dict, folder: str) -> Iterable[Bookmark]: - node_type = str(node.get("type") or "") - if node_type == "url" and node.get("url"): - yield Bookmark( - id=str(node.get("id") or ""), - name=str(node.get("name") or "Untitled"), - url=str(node.get("url") or ""), - date_added=_chrome_microseconds_to_iso_date(node.get("date_added")), - folder=folder or None, - ) - - children = node.get("children") or [] - if not isinstance(children, list): - return - - current_folder = str(node.get("name") or folder) - for child in children: - if isinstance(child, dict): - yield from _iter_bookmark_nodes(child, current_folder) - - -def get_bookmarks(limit: int, search_text: str | None) -> list[Bookmark]: - bookmarks_path = get_bookmarks_path() - data = json.loads(bookmarks_path.read_text(encoding="utf-8")) - - roots = data.get("roots") or {} - ordered_roots = [ - ("Bookmarks Bar", roots.get("bookmark_bar") or {}), - ("Other Bookmarks", roots.get("other") or {}), - ("Synced Bookmarks", roots.get("synced") or {}), - ] - - bookmarks: list[Bookmark] = [] - for folder_name, root in ordered_roots: - if isinstance(root, dict): - bookmarks.extend(_iter_bookmark_nodes(root, folder_name)) - - if search_text: - lowered = search_text.lower() - bookmarks = [ - b - for b in bookmarks - if lowered in b.name.lower() or lowered in b.url.lower() or lowered in (b.folder or "").lower() - ] - - return bookmarks[:limit] - - -def _print_table(rows: Iterable[Sequence[object]], headers: Sequence[str]) -> None: - widths = [len(h) for h in headers] - materialized = [tuple(str(cell) for cell in row) for row in rows] - for row in materialized: - for i, cell in enumerate(row): - widths[i] = max(widths[i], len(cell)) - - def fmt_row(row: Sequence[str]) -> str: - return " ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)) - - print(fmt_row(headers)) - print(fmt_row(["-" * w for w in widths])) - for row in materialized: - print(fmt_row(row)) - - -def _render_tabs(tabs: list[Tab], as_json: bool) -> None: - if as_json: - print(json.dumps([asdict(t) for t in tabs], indent=2)) - return - - rows = [ - ( - "*" if t.is_active else " ", - t.window_id, - t.tab_index, - t.title, - t.url, - ) - for t in tabs - ] - _print_table(rows, headers=["A", "window_id", "tab", "title", "url"]) - - -def _render_history(rows: list[HistoryRow], as_json: bool) -> None: - if as_json: - print(json.dumps([asdict(r) for r in rows], indent=2)) - return - - table = [(r.last_visited_at, r.title, r.url) for r in rows] - _print_table(table, headers=["last_visited_at", "title", "url"]) - - -def _render_bookmarks(rows: list[Bookmark], as_json: bool) -> None: - if as_json: - print(json.dumps([asdict(r) for r in rows], indent=2)) - return - - table = [(r.folder or "", r.name, r.url, r.date_added) for r in rows] - _print_table(table, headers=["folder", "name", "url", "date_added"]) - - -def _parse_args(argv: Sequence[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Control ChatGPT Atlas from the terminal.") - sub = parser.add_subparsers(dest="command", required=True) - - sub.add_parser("app-name", help="Print the detected Atlas application name") - - tabs_parser = sub.add_parser("tabs", help="List open Atlas tabs") - tabs_parser.add_argument("--json", action="store_true", help="Emit JSON instead of a table") - - open_parser = sub.add_parser("open-tab", help="Open a new Atlas tab") - open_parser.add_argument("url", help="URL to open") - - focus_parser = sub.add_parser("focus-tab", help="Focus a tab by window id and tab index") - focus_parser.add_argument("window_id", type=int, help="Atlas window id (from tabs output)") - focus_parser.add_argument("tab_index", type=int, help="Tab index within the window (1-based)") - - close_parser = sub.add_parser("close-tab", help="Close a tab by window id and tab index") - close_parser.add_argument("window_id", type=int, help="Atlas window id (from tabs output)") - close_parser.add_argument("tab_index", type=int, help="Tab index within the window (1-based)") - - reload_parser = sub.add_parser("reload-tab", help="Reload a tab by window id and tab index") - reload_parser.add_argument("window_id", type=int, help="Atlas window id (from tabs output)") - reload_parser.add_argument("tab_index", type=int, help="Tab index within the window (1-based)") - - history_parser = sub.add_parser("history", help="Search Atlas history") - history_parser.add_argument("--search", default=None, help="Space-separated search terms") - history_parser.add_argument("--limit", type=int, default=DEFAULT_HISTORY_LIMIT, help="Max rows to return") - history_parser.add_argument( - "--today", - action="store_true", - help="Limit results to pages visited today (local time)", - ) - history_parser.add_argument("--json", action="store_true", help="Emit JSON instead of a table") - - bookmarks_parser = sub.add_parser("bookmarks", help="List Atlas bookmarks") - bookmarks_parser.add_argument("--search", default=None, help="Filter bookmarks by text") - bookmarks_parser.add_argument("--limit", type=int, default=DEFAULT_BOOKMARK_LIMIT, help="Max rows to return") - bookmarks_parser.add_argument("--json", action="store_true", help="Emit JSON instead of a table") - - return parser.parse_args(argv) - - -def main(argv: Sequence[str] | None = None) -> int: - args = _parse_args(argv or sys.argv[1:]) - - try: - if args.command == "app-name": - print(detect_atlas_app_name()) - return 0 - - if args.command == "tabs": - _render_tabs(get_tabs(), as_json=args.json) - return 0 - - if args.command == "open-tab": - open_new_tab(args.url) - print(f"Opened tab: {args.url}") - return 0 - - if args.command == "focus-tab": - focus_tab(args.window_id, args.tab_index) - print(f"Focused window id {args.window_id}, tab {args.tab_index}") - return 0 - - if args.command == "close-tab": - close_tab(args.window_id, args.tab_index) - print(f"Closed window id {args.window_id}, tab {args.tab_index}") - return 0 - - if args.command == "reload-tab": - reload_tab(args.window_id, args.tab_index) - print(f"Reloaded window id {args.window_id}, tab {args.tab_index}") - return 0 - - if args.command == "history": - rows = search_history(args.search, max(1, args.limit), today=args.today) - _render_history(rows, as_json=args.json) - return 0 - - if args.command == "bookmarks": - rows = get_bookmarks(max(1, args.limit), args.search) - _render_bookmarks(rows, as_json=args.json) - return 0 - - raise AtlasError(f"Unknown command: {args.command}") - except AtlasError as exc: - print(f"atlas error: {exc}", file=sys.stderr) - return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/skills/.curated/atlas/scripts/atlas_common.py b/skills/.curated/atlas/scripts/atlas_common.py deleted file mode 100644 index 9c91bc9..0000000 --- a/skills/.curated/atlas/scripts/atlas_common.py +++ /dev/null @@ -1,168 +0,0 @@ -from __future__ import annotations - -import json -import shutil -import subprocess -import tempfile -from functools import lru_cache -from pathlib import Path -from typing import Any - -ATLAS_APP_NAME = "ChatGPT Atlas" - -LOCAL_STATE_PATH = ( - Path.home() - / "Library" - / "Application Support" - / "com.openai.atlas" - / "browser-data" - / "host" - / "Local State" -) - - -class AtlasError(RuntimeError): - """Raised when Atlas-specific operations fail.""" - - -def _app_bundle_paths(app_name: str) -> list[Path]: - return [ - Path("/Applications") / f"{app_name}.app", - Path.home() / "Applications" / f"{app_name}.app", - ] - - -def is_app_installed(app_name: str) -> bool: - return any(path.exists() for path in _app_bundle_paths(app_name)) - - -@lru_cache(maxsize=1) -def detect_atlas_app_name() -> str: - """Return the Atlas app name when it is installed.""" - if is_app_installed(ATLAS_APP_NAME): - return ATLAS_APP_NAME - - raise AtlasError("Could not find ChatGPT Atlas. Install the ChatGPT Atlas app.") - - -def _ensure_local_state_path(path: Path = LOCAL_STATE_PATH) -> Path: - if not path.exists(): - raise AtlasError(f"Local State file not found at: {path}") - return path - - -def read_local_state(path: Path = LOCAL_STATE_PATH) -> dict[str, Any]: - local_state_path = _ensure_local_state_path(path) - try: - return json.loads(local_state_path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exc: - raise AtlasError(f"Failed to parse Local State JSON at: {local_state_path}") from exc - - -def get_active_profile_path() -> Path: - """Resolve the active Atlas profile directory, falling back to Default.""" - local_state = read_local_state() - host_root = LOCAL_STATE_PATH.parent - - last_used = local_state.get("profile", {}).get("last_used") - if isinstance(last_used, str) and last_used: - candidate = host_root / last_used - if candidate.exists(): - return candidate - - default_profile = host_root / "Default" - if default_profile.exists(): - return default_profile - - raise AtlasError( - "Unable to resolve an Atlas profile directory. " - f"Checked last_used={last_used!r} and Default under: {host_root}" - ) - - -def get_history_path() -> Path: - return get_active_profile_path() / "History" - - -def get_bookmarks_path() -> Path: - return get_active_profile_path() / "Bookmarks" - - -def copy_sqlite_db(path: Path) -> Path: - """Copy a SQLite DB to a temp location to avoid lock errors.""" - if not path.exists(): - raise AtlasError(f"SQLite database not found at: {path}") - - tmp_dir = Path(tempfile.mkdtemp(prefix="atlas-db-")) - dest = tmp_dir / path.name - shutil.copy2(path, dest) - return dest - - -def _run_applescript_raw(script: str) -> subprocess.CompletedProcess[str]: - if shutil.which("osascript") is None: - raise AtlasError("osascript is not available on PATH; Atlas control requires macOS") - - return subprocess.run( - ["osascript", "-e", script], - check=False, - capture_output=True, - text=True, - ) - - -def _applescript_hint(stderr: str) -> str | None: - lower = stderr.lower() - if "-1743" in stderr or "not authorized" in lower or "not permitted" in lower: - return ( - "Grant Automation permission in System Settings > Privacy & Security > Automation, " - "and allow your terminal to control ChatGPT Atlas." - ) - return None - - -def run_applescript(script: str) -> str: - """Execute AppleScript via osascript and return stdout.""" - completed = _run_applescript_raw(script) - if completed.returncode != 0: - stderr = (completed.stderr or "").strip() - hint = _applescript_hint(stderr) - if hint: - stderr = f"{stderr} (Hint: {hint})" - raise AtlasError(f"AppleScript failed: {stderr}") - return (completed.stdout or "").strip() - - -@lru_cache(maxsize=4) -def is_tab_capable(app_name: str) -> bool: - """Return True if the app supports basic window/tab AppleScript queries.""" - if not is_app_installed(app_name): - return False - - probe = f'tell application "{app_name}" to get count of windows' - completed = _run_applescript_raw(probe) - if completed.returncode != 0: - stderr = (completed.stderr or "").strip() - hint = _applescript_hint(stderr) - if hint: - raise AtlasError(f"AppleScript probe failed: {stderr} (Hint: {hint})") - return False - return True - - -@lru_cache(maxsize=1) -def detect_tab_capable_app_name() -> str: - """Return the Atlas app name when it supports tab scripting.""" - app_name = detect_atlas_app_name() - if not is_tab_capable(app_name): - raise AtlasError( - "ChatGPT Atlas is installed but does not appear to expose window/tab scripting." - ) - return app_name - - -def tell_atlas(script_body: str, app_name: str | None = None) -> str: - """Wrap a script body in a tell application block for the detected Atlas app.""" - target_app = app_name or detect_atlas_app_name() - script = f'tell application "{target_app}"\n{script_body}\nend tell' - return run_applescript(script)