From 96abbd887aac30bc2e39689b1b8f29488b4206d9 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:29:41 -0400 Subject: [PATCH 1/3] feat: derive published browser counts, and add Naver Whale Answering "what does three lines actually mean" by doing it. Adding Naver Whale took three hand-written lines across two files: the reference table row, the family path, and the process name. It worked, and it also broke three tests, which is the useful part. The browser count published in the README and on the site was suddenly wrong, and nothing but those tests would have noticed. That made the claim false: adding a browser really cost three lines plus remembering to update two numbers and a list by hand. So the published numbers are derived now. sync-skills.py reads platforms.py and rewrites the count and the browser list in both the README and the site, and CI fails if they drift. Adding a browser is three lines and one command again, this time accurately. Whale is kept rather than reverted. It is a real browser with real users in Korea, and all three skills now cover it. --- README.md | 6 +- docs/ADDING-A-SKILL.md | 4 ++ references/app-data-locations.md | 1 + scripts/platforms.py | 2 + scripts/sync-skills.py | 62 +++++++++++++++++-- site/index.html | 6 +- .../references/app-data-locations.md | 1 + .../organizing-bookmarks/scripts/platforms.py | 2 + .../references/app-data-locations.md | 1 + .../organizing-folders/scripts/platforms.py | 2 + .../references/app-data-locations.md | 1 + .../organizing-notes/scripts/platforms.py | 2 + 12 files changed, 80 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index df4bd28..2485c18 100644 --- a/README.md +++ b/README.md @@ -142,11 +142,13 @@ skill covers every app that shares a storage format: | Format | Apps covered | |---|---| -| Chromium `Bookmarks` JSON | Chrome · Brave · Edge · Arc · Chromium · Vivaldi · Opera | +| Chromium `Bookmarks` JSON | Chrome · Brave · Edge · Chromium · Vivaldi · Arc · Opera · Whale | | Safari plist | Safari | | `places.sqlite` | Firefox · Tor Browser | -**Ten browsers, one markdown file.** Adding Vivaldi took three lines and no new skill. +**Eleven browsers, one markdown file.** Adding a browser is three lines in two files +and one `sync` command. No new skill, no new code, and every existing browser skill +covers it immediately. ## Platforms diff --git a/docs/ADDING-A-SKILL.md b/docs/ADDING-A-SKILL.md index dd17934..038916a 100644 --- a/docs/ADDING-A-SKILL.md +++ b/docs/ADDING-A-SKILL.md @@ -16,6 +16,10 @@ that category covers it immediately. No new skill, no new code. | `scripts/platforms.py`, the family table | where the data lives, on each platform | | `scripts/platforms.py`, `PROCESS_NAMES` | what the running process is called | +Then run `python3 scripts/sync-skills.py sync`. That regenerates the vendored copies +inside each skill and updates the browser count and list that the README and the site +publish, so nothing has to be remembered by hand. CI fails if you skip it. + **All three, or none.** Registering a path without a process name is the dangerous half-addition: `running()` returns `None`, so a skill cannot tell the app is open, and a browser that rewrites its store on exit will clobber the change. Opera shipped in diff --git a/references/app-data-locations.md b/references/app-data-locations.md index 9da9c32..bb7189c 100644 --- a/references/app-data-locations.md +++ b/references/app-data-locations.md @@ -50,6 +50,7 @@ row supports every app in it. | Vivaldi | `Vivaldi` | `vivaldi` | `Vivaldi\User Data` | | Opera | `com.operasoftware.Opera` | `opera` | `Opera Software\Opera Stable` | | Arc | `Arc/User Data` | not available | `Arc\User Data` | +| Whale | `Naver/Whale` | `naver-whale` | `Naver\Naver Whale\User Data` | The bookmarks file is `//Bookmarks`, where `` is `Default` or `Profile 1`, `Profile 2`, and so on. The resolver enumerates profiles; never assume diff --git a/scripts/platforms.py b/scripts/platforms.py index 840cdbb..0f20ade 100755 --- a/scripts/platforms.py +++ b/scripts/platforms.py @@ -51,6 +51,7 @@ def _appdata(kind="local"): "vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi", WINDOWS: r"Vivaldi\User Data"}, "arc": {MAC: "Arc/User Data", LINUX: None, WINDOWS: r"Arc\User Data"}, "opera": {MAC: "com.operasoftware.Opera", LINUX: "opera", WINDOWS: r"Opera Software\Opera Stable"}, + "whale": {MAC: "Naver/Whale", LINUX: "naver-whale", WINDOWS: r"Naver\Naver Whale\User Data"}, } FIREFOX = { @@ -70,6 +71,7 @@ def _appdata(kind="local"): "vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi-bin", WINDOWS: "vivaldi.exe"}, "arc": {MAC: "Arc", LINUX: None, WINDOWS: "Arc.exe"}, "opera": {MAC: "Opera", LINUX: "opera", WINDOWS: "opera.exe"}, + "whale": {MAC: "Whale", LINUX: "whale", WINDOWS: "whale.exe"}, "firefox": {MAC: "firefox", LINUX: "firefox", WINDOWS: "firefox.exe"}, "safari": {MAC: "Safari", LINUX: None, WINDOWS: None}, "obsidian": {MAC: "Obsidian", LINUX: "obsidian", WINDOWS: "Obsidian.exe"}, diff --git a/scripts/sync-skills.py b/scripts/sync-skills.py index e5ca1e3..b6eb9a8 100755 --- a/scripts/sync-skills.py +++ b/scripts/sync-skills.py @@ -9,10 +9,15 @@ So `references/` and `scripts/` stay the single source of truth that humans edit and tests run against, and this script copies what each skill needs into it. - sync copy shared files into every skill directory - check exit non-zero if any skill is out of date (used by CI) + sync copy shared files into every skill directory, and refresh the + browser counts and lists that the README and the site publish + check exit non-zero if anything is out of date (used by CI) + +Adding a browser should cost three lines in two files and this command. +Anything a human has to remember to update by hand will eventually be wrong, +which is why the published numbers are derived here rather than typed. """ -import argparse, filecmp, glob, hashlib, os, re, shutil, sys +import argparse, glob, hashlib, importlib.util, os, re, shutil, sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -45,6 +50,51 @@ def digest(path): return hashlib.sha256(fh.read()).hexdigest() +NUMBER = {1:"One",2:"Two",3:"Three",4:"Four",5:"Five",6:"Six",7:"Seven",8:"Eight", + 9:"Nine",10:"Ten",11:"Eleven",12:"Twelve",13:"Thirteen",14:"Fourteen"} + + +def load_platforms(): + spec = importlib.util.spec_from_file_location( + "platforms", os.path.join(ROOT, "scripts", "platforms.py")) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def published(check_only): + """Keep the numbers the README and the site advertise equal to the code.""" + pf = load_platforms() + chromium = [a.capitalize() for a in pf.CHROMIUM] + total = len(pf.CHROMIUM) + 1 + len(pf.FIREFOX) # + Safari + Firefox family + stale = [] + + site_path = os.path.join(ROOT, "site", "index.html") + site = open(site_path, encoding="utf-8").read() + new_site = re.sub(r'(class="stat">)\d+(<)', r"\g<1>%d\g<2>" % total, site) + chips = "".join('%s' % a for a in chromium) + new_site = re.sub(r'(
Chromium JSON
\s*
).*?(
)', + lambda m: m.group(1) + chips + m.group(2), new_site, flags=re.S) + + readme_path = os.path.join(ROOT, "README.md") + readme = open(readme_path, encoding="utf-8").read() + new_readme = re.sub(r'\*\*\w+ browsers, one markdown file\.\*\*', + "**%s browsers, one markdown file.**" % NUMBER[total], readme) + new_readme = re.sub(r'(\| Chromium `Bookmarks` JSON \| ).*?( \|)', + lambda m: m.group(1) + " \u00b7 ".join(chromium) + m.group(2), + new_readme) + + for path, old, new, label in ((site_path, site, new_site, "site/index.html"), + (readme_path, readme, new_readme, "README.md")): + if old == new: + continue + if check_only: + stale.append(label + " (published browser count or list)") + else: + open(path, "w", encoding="utf-8").write(new) + return stale + + def run(check_only): stale = [] for skill in skill_dirs(): @@ -74,15 +124,17 @@ def run(check_only): else: os.remove(os.path.join(skill, rel)) + stale += published(check_only) + if check_only: if stale: print("Vendored copies are out of date:\n " + "\n ".join(stale)) print("\nRun: python3 scripts/sync-skills.py sync") return 1 - print(f"All {len(skill_dirs())} skills are in sync.") + print(f"All {len(skill_dirs())} skills and published counts are in sync.") return 0 - print(f"Synced {len(skill_dirs())} skills.") + print(f"Synced {len(skill_dirs())} skills and the published browser counts.") return 0 diff --git a/site/index.html b/site/index.html index eb0f698..bcaec7f 100644 --- a/site/index.html +++ b/site/index.html @@ -598,7 +598,7 @@

Three skills, one habit.

browser

organizing‑bookmarks

Dedupes across tracking parameters, www, and trailing slashes. Prunes dead links. Flattens folder sprawl.

-
10browsers covered by this one file
+
11browsers covered by this one file
files
@@ -623,7 +623,7 @@

One skill, every app in the category.

Chromium JSON
-
ChromeBraveEdgeArcChromiumVivaldiOpera
+
ChromeBraveEdgeChromiumVivaldiArcOperaWhale
Safari plist
@@ -634,7 +634,7 @@

One skill, every app in the category.

FirefoxTor Browser
-

Adding Vivaldi took three lines and no new skill.

+

Adding a browser takes three lines and no new skill.

The same holds across operating systems. Chrome keeps identical bookmark JSON on Windows, macOS, and Linux, so only the path changes. macOS is the strictest of the three and the one that needs a permission grant.

diff --git a/skills/browser/organizing-bookmarks/references/app-data-locations.md b/skills/browser/organizing-bookmarks/references/app-data-locations.md index 9da9c32..bb7189c 100644 --- a/skills/browser/organizing-bookmarks/references/app-data-locations.md +++ b/skills/browser/organizing-bookmarks/references/app-data-locations.md @@ -50,6 +50,7 @@ row supports every app in it. | Vivaldi | `Vivaldi` | `vivaldi` | `Vivaldi\User Data` | | Opera | `com.operasoftware.Opera` | `opera` | `Opera Software\Opera Stable` | | Arc | `Arc/User Data` | not available | `Arc\User Data` | +| Whale | `Naver/Whale` | `naver-whale` | `Naver\Naver Whale\User Data` | The bookmarks file is `//Bookmarks`, where `` is `Default` or `Profile 1`, `Profile 2`, and so on. The resolver enumerates profiles; never assume diff --git a/skills/browser/organizing-bookmarks/scripts/platforms.py b/skills/browser/organizing-bookmarks/scripts/platforms.py index 840cdbb..0f20ade 100755 --- a/skills/browser/organizing-bookmarks/scripts/platforms.py +++ b/skills/browser/organizing-bookmarks/scripts/platforms.py @@ -51,6 +51,7 @@ def _appdata(kind="local"): "vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi", WINDOWS: r"Vivaldi\User Data"}, "arc": {MAC: "Arc/User Data", LINUX: None, WINDOWS: r"Arc\User Data"}, "opera": {MAC: "com.operasoftware.Opera", LINUX: "opera", WINDOWS: r"Opera Software\Opera Stable"}, + "whale": {MAC: "Naver/Whale", LINUX: "naver-whale", WINDOWS: r"Naver\Naver Whale\User Data"}, } FIREFOX = { @@ -70,6 +71,7 @@ def _appdata(kind="local"): "vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi-bin", WINDOWS: "vivaldi.exe"}, "arc": {MAC: "Arc", LINUX: None, WINDOWS: "Arc.exe"}, "opera": {MAC: "Opera", LINUX: "opera", WINDOWS: "opera.exe"}, + "whale": {MAC: "Whale", LINUX: "whale", WINDOWS: "whale.exe"}, "firefox": {MAC: "firefox", LINUX: "firefox", WINDOWS: "firefox.exe"}, "safari": {MAC: "Safari", LINUX: None, WINDOWS: None}, "obsidian": {MAC: "Obsidian", LINUX: "obsidian", WINDOWS: "Obsidian.exe"}, diff --git a/skills/files/organizing-folders/references/app-data-locations.md b/skills/files/organizing-folders/references/app-data-locations.md index 9da9c32..bb7189c 100644 --- a/skills/files/organizing-folders/references/app-data-locations.md +++ b/skills/files/organizing-folders/references/app-data-locations.md @@ -50,6 +50,7 @@ row supports every app in it. | Vivaldi | `Vivaldi` | `vivaldi` | `Vivaldi\User Data` | | Opera | `com.operasoftware.Opera` | `opera` | `Opera Software\Opera Stable` | | Arc | `Arc/User Data` | not available | `Arc\User Data` | +| Whale | `Naver/Whale` | `naver-whale` | `Naver\Naver Whale\User Data` | The bookmarks file is `//Bookmarks`, where `` is `Default` or `Profile 1`, `Profile 2`, and so on. The resolver enumerates profiles; never assume diff --git a/skills/files/organizing-folders/scripts/platforms.py b/skills/files/organizing-folders/scripts/platforms.py index 840cdbb..0f20ade 100755 --- a/skills/files/organizing-folders/scripts/platforms.py +++ b/skills/files/organizing-folders/scripts/platforms.py @@ -51,6 +51,7 @@ def _appdata(kind="local"): "vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi", WINDOWS: r"Vivaldi\User Data"}, "arc": {MAC: "Arc/User Data", LINUX: None, WINDOWS: r"Arc\User Data"}, "opera": {MAC: "com.operasoftware.Opera", LINUX: "opera", WINDOWS: r"Opera Software\Opera Stable"}, + "whale": {MAC: "Naver/Whale", LINUX: "naver-whale", WINDOWS: r"Naver\Naver Whale\User Data"}, } FIREFOX = { @@ -70,6 +71,7 @@ def _appdata(kind="local"): "vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi-bin", WINDOWS: "vivaldi.exe"}, "arc": {MAC: "Arc", LINUX: None, WINDOWS: "Arc.exe"}, "opera": {MAC: "Opera", LINUX: "opera", WINDOWS: "opera.exe"}, + "whale": {MAC: "Whale", LINUX: "whale", WINDOWS: "whale.exe"}, "firefox": {MAC: "firefox", LINUX: "firefox", WINDOWS: "firefox.exe"}, "safari": {MAC: "Safari", LINUX: None, WINDOWS: None}, "obsidian": {MAC: "Obsidian", LINUX: "obsidian", WINDOWS: "Obsidian.exe"}, diff --git a/skills/notes/organizing-notes/references/app-data-locations.md b/skills/notes/organizing-notes/references/app-data-locations.md index 9da9c32..bb7189c 100644 --- a/skills/notes/organizing-notes/references/app-data-locations.md +++ b/skills/notes/organizing-notes/references/app-data-locations.md @@ -50,6 +50,7 @@ row supports every app in it. | Vivaldi | `Vivaldi` | `vivaldi` | `Vivaldi\User Data` | | Opera | `com.operasoftware.Opera` | `opera` | `Opera Software\Opera Stable` | | Arc | `Arc/User Data` | not available | `Arc\User Data` | +| Whale | `Naver/Whale` | `naver-whale` | `Naver\Naver Whale\User Data` | The bookmarks file is `//Bookmarks`, where `` is `Default` or `Profile 1`, `Profile 2`, and so on. The resolver enumerates profiles; never assume diff --git a/skills/notes/organizing-notes/scripts/platforms.py b/skills/notes/organizing-notes/scripts/platforms.py index 840cdbb..0f20ade 100755 --- a/skills/notes/organizing-notes/scripts/platforms.py +++ b/skills/notes/organizing-notes/scripts/platforms.py @@ -51,6 +51,7 @@ def _appdata(kind="local"): "vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi", WINDOWS: r"Vivaldi\User Data"}, "arc": {MAC: "Arc/User Data", LINUX: None, WINDOWS: r"Arc\User Data"}, "opera": {MAC: "com.operasoftware.Opera", LINUX: "opera", WINDOWS: r"Opera Software\Opera Stable"}, + "whale": {MAC: "Naver/Whale", LINUX: "naver-whale", WINDOWS: r"Naver\Naver Whale\User Data"}, } FIREFOX = { @@ -70,6 +71,7 @@ def _appdata(kind="local"): "vivaldi": {MAC: "Vivaldi", LINUX: "vivaldi-bin", WINDOWS: "vivaldi.exe"}, "arc": {MAC: "Arc", LINUX: None, WINDOWS: "Arc.exe"}, "opera": {MAC: "Opera", LINUX: "opera", WINDOWS: "opera.exe"}, + "whale": {MAC: "Whale", LINUX: "whale", WINDOWS: "whale.exe"}, "firefox": {MAC: "firefox", LINUX: "firefox", WINDOWS: "firefox.exe"}, "safari": {MAC: "Safari", LINUX: None, WINDOWS: None}, "obsidian": {MAC: "Obsidian", LINUX: "obsidian", WINDOWS: "Obsidian.exe"}, From 7e0ca02ce56ccf155ec830d48e27a1b5026b8a91 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:31:55 -0400 Subject: [PATCH 2/3] docs(site): state what adding a browser actually costs The coverage section still said "three lines and no new skill", which omits the sync command and is the same overstatement just corrected everywhere else. It now says three lines and one command, names the three places, and cites Naver Whale as the browser added exactly that way. --- site/index.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/site/index.html b/site/index.html index bcaec7f..48e3e99 100644 --- a/site/index.html +++ b/site/index.html @@ -324,6 +324,7 @@ } @media(max-width:720px){ .row{grid-template-columns:1fr; gap:12px} } .punch{margin-top:36px; font-size:20px; font-weight:600; letter-spacing:-.02em} +.punch + p code{font:500 13px/1 var(--mono); background:var(--bg-3); padding:2px 6px; border-radius:5px} .punch em{font-style:normal; color:var(--accent-text)} /* ---------- contribute: code + prose ---------- */ @@ -634,8 +635,9 @@

One skill, every app in the category.

FirefoxTor Browser
-

Adding a browser takes three lines and no new skill.

-

The same holds across operating systems. Chrome keeps identical bookmark JSON on Windows, macOS, and Linux, so only the path changes. macOS is the strictest of the three and the one that needs a permission grant.

+

Adding a browser takes three lines and one command.

+

A row in the reference table, the data path, the process name, then one sync command. No new skill and no new code, and every browser skill covers it immediately. Naver Whale was added exactly that way.

+

The same holds across operating systems. Chrome keeps identical bookmark JSON on Windows, macOS, and Linux, so only the path changes. macOS is the strictest of the three and the one that needs a permission grant.

From b15010a36701b152b935ff0848e93863b3533bfe Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:32:41 -0400 Subject: [PATCH 3/3] docs: stop assuming the reader knows what Vivaldi is The coverage copy said "Adding Vivaldi took three lines", which only lands if you already know Vivaldi is a browser. Most readers do not, and swapping in Naver Whale made it worse. The prose now says every browser listed above was added that way, which needs no prior knowledge and is equally true. The niche names stay in the supported-browser list, which is exactly where an unfamiliar name is useful rather than confusing. --- docs/ADDING-A-SKILL.md | 5 +++-- site/index.html | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/ADDING-A-SKILL.md b/docs/ADDING-A-SKILL.md index 038916a..b12d440 100644 --- a/docs/ADDING-A-SKILL.md +++ b/docs/ADDING-A-SKILL.md @@ -26,8 +26,9 @@ a browser that rewrites its store on exit will clobber the change. Opera shipped exactly that state until a test caught it. A test now enforces the pairing, but the test only helps if you know why it is there. -Vivaldi was added this way. It is a Chromium browser, so it keeps bookmarks in the -same JSON format as Chrome, and it needed no new skill at all. +Every browser this repo supports beyond the first one was added this way. Most of them +are built on Chromium, which means they store bookmarks in exactly the same JSON format +as Chrome does, so supporting one more is a matter of saying where its copy lives. For the table row, include: diff --git a/site/index.html b/site/index.html index 48e3e99..cb2a3ad 100644 --- a/site/index.html +++ b/site/index.html @@ -636,7 +636,7 @@

One skill, every app in the category.

Adding a browser takes three lines and one command.

-

A row in the reference table, the data path, the process name, then one sync command. No new skill and no new code, and every browser skill covers it immediately. Naver Whale was added exactly that way.

+

A row in the reference table, the data path, the process name, then one sync command. No new skill and no new code, and every browser skill covers it immediately. Every browser listed above was added that way.

The same holds across operating systems. Chrome keeps identical bookmark JSON on Windows, macOS, and Linux, so only the path changes. macOS is the strictest of the three and the one that needs a permission grant.