{"id":49182296,"url":"https://github.com/franckferman/bmctl","last_synced_at":"2026-04-23T02:01:28.237Z","repository":{"id":343285953,"uuid":"1177079136","full_name":"franckferman/bmctl","owner":"franckferman","description":"bmctl Firefox bookmark toolkit - audit duplicates, compare exports, merge collections, and generate an interactive dashboard from the command line.","archived":false,"fork":false,"pushed_at":"2026-03-27T00:45:06.000Z","size":39,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"stable","last_synced_at":"2026-03-27T06:29:48.936Z","etag":null,"topics":["bookmark","bookmark-manager","bookmark-search","bookmark-web","bookmarker","bookmarking","bookmarklet","bookmarklets","bookmarks","bookmarks-app","bookmarks-manager","bookmarks-menu","bookmarks-search"],"latest_commit_sha":null,"homepage":"https://franckferman.github.io/bmctl/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/franckferman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-09T17:11:46.000Z","updated_at":"2026-03-27T00:36:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/franckferman/bmctl","commit_stats":null,"previous_names":["franckferman/bmctl"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/franckferman/bmctl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franckferman%2Fbmctl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franckferman%2Fbmctl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franckferman%2Fbmctl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franckferman%2Fbmctl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/franckferman","download_url":"https://codeload.github.com/franckferman/bmctl/tar.gz/refs/heads/stable","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/franckferman%2Fbmctl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32162611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-22T17:06:48.269Z","status":"online","status_checked_at":"2026-04-23T02:00:06.710Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bookmark","bookmark-manager","bookmark-search","bookmark-web","bookmarker","bookmarking","bookmarklet","bookmarklets","bookmarks","bookmarks-app","bookmarks-manager","bookmarks-menu","bookmarks-search"],"created_at":"2026-04-23T02:01:26.661Z","updated_at":"2026-04-23T02:01:28.169Z","avatar_url":"https://github.com/franckferman.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# bmctl\n\n**Firefox bookmark toolkit** - audit duplicates, compare exports, merge collections, and generate an interactive self-hosted dashboard from the command line.\n\n[![Demo](https://img.shields.io/badge/demo-live-brightgreen?style=flat-square)](https://franckferman.github.io/bmctl/demo/)\n[![Python Version](https://img.shields.io/badge/Python-3-3776AB?style=flat-square\u0026logo=python\u0026logoColor=white)](https://www.python.org)\n[![License](https://img.shields.io/badge/license-AGPL--3.0-blue?style=flat-square)](LICENSE)\n\n\u003c/div\u003e\n\n---\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Features](#features)\n- [Quick Start](#quick-start)\n- [Project Structure](#project-structure)\n- [Installation](#installation)\n- [Getting Started](#getting-started)\n- [Commands](#commands)\n  - [audit](#audit)\n  - [compare](#compare)\n  - [merge](#merge)\n  - [export](#export)\n  - [dashboard](#dashboard)\n- [URL Deduplication](#url-deduplication)\n- [Firefox JSON Compatibility](#firefox-json-compatibility)\n- [License](#license)\n\n---\n\n## Overview\n\nbmctl is a single-file Python CLI for managing Firefox bookmark exports (`.json`). It parses the full folder hierarchy, preserves the original Firefox tree order, and provides five commands covering the full lifecycle of a bookmark collection.\n\nNo installation required beyond Python itself. No database, no daemon, no config file. One file, one command, one output.\n\n---\n\n## Features\n\n| Command | What it does |\n|---|---|\n| `audit` | Duplicate detection, stats, folder tree visualization |\n| `compare` | Diff two exports - added, removed, net delta |\n| `merge` | Merge two collections into a Netscape HTML file with conflict resolution |\n| `export` | Flat export to CSV, Excel, or Markdown |\n| `dashboard` | Self-contained interactive HTML dashboard, no server required |\n\nURL normalization across all commands: scheme, `www.`, trailing slash, and `utm_*` parameters are stripped before comparison. `http://www.github.com/` and `https://github.com` are treated as the same URL.\n\n---\n\n## Quick Start\n\n```bash\n# 1. Get the tool\ngit clone https://github.com/franckferman/bmctl.git\ncd bmctl\n\n# 2. Export your bookmarks from Firefox\n#    Bookmarks \u003e Show All Bookmarks \u003e Import and Backup \u003e Backup...\n#    Save as bookmarks.json\n\n# 3. Run your first audit\npython3 bmctl.py audit -i bookmarks.json\n\n# Generate an interactive dashboard\npython3 bmctl.py dashboard -i bookmarks.json -o dashboard.html\n# Open dashboard.html in your browser\n```\n\n**Comparing two exports:**\n\n`compare` works on two separate Firefox exports taken at different points in time. Export once, wait (add/remove bookmarks), export again, then diff:\n\n```bash\n# First export saved as bookmarks-jan.json\n# Second export saved as bookmarks-feb.json\n\npython3 bmctl.py compare -o bookmarks-jan.json -n bookmarks-feb.json\n```\n\n**Merging two collections:**\n\nUseful if you have bookmarks spread across two Firefox profiles or two machines. The merge takes the union of both collections. For each URL:\n\n- If it appears in only one collection, it is kept as-is.\n- If it appears in both collections in the **same folder**, the most recent version is kept and tags are merged.\n- If it appears in both collections in **different folders** (conflict), you are prompted to choose which folder to keep - or use `--no-confirm` to automatically keep the most recent.\n\nNo entries are silently deleted. Every URL from both files ends up in the output.\n\n```bash\npython3 bmctl.py merge -b profile-a.json -n profile-b.json -o merged.html\n# merged.html is a Netscape HTML file - import it in any browser via:\n# Bookmarks \u003e Show All Bookmarks \u003e Import and Backup \u003e Restore... (or Import HTML)\n```\n\nNo install step required. `bmctl.py` is a single file - you can also just download it directly without cloning the full repo.\n\n---\n\n## Project Structure\n\n```\nbmctl.py\n  BookmarkNode          Data model for a single bookmark\n  UrlNormalizer         URL normalization / deduplication\n  BookmarkDatabase      JSON parser + in-memory index\n  BookmarkAuditor       Duplicate detection + reporting\n  BookmarkComparator    Two-database diff\n  BookmarkMerger        Merge + conflict resolution + HTML export\n  BookmarkDashboardGen  Interactive HTML dashboard generator\n  BookmarkExporter      CSV / Excel / Markdown export\n```\n\n`BookmarkDatabase` is the core - it walks the Firefox JSON tree recursively, assigns each bookmark its full folder path, and builds a URL index for deduplication. All five commands consume it.\n\n---\n\n## Installation\n\n**Prerequisites:** Python 3\n\nNo pip install required for core functionality. `pandas` and `openpyxl` are only needed for `export --format xlsx`:\n\n```bash\npip install -r requirements.txt\n```\n\nNo other dependencies.\n\n---\n\n## Getting Started\n\nExport your bookmarks from Firefox:\n\n```\nBookmarks \u003e Show All Bookmarks \u003e Import and Backup \u003e Backup...\n```\n\nSave as `.json`. This file is the input for all commands.\n\n---\n\n## Commands\n\n### `audit`\n\nInspect a single export: duplicate detection, stats, and optional folder tree.\n\n```bash\npython bmctl.py audit -i bookmarks.json\n```\n\n```\n======================================================================\n                  GLOBAL AUDIT REPORT\n======================================================================\n Total bookmarks found        : 3110\n Folders scanned              : 244\n Unique links                 : 3059\n Duplicates detected          : 51 (1.6%)\n======================================================================\n\n[!] Top 10 most duplicated links:\n  - GitHub\n    URL : https://github.com\n    Found 3 times:\n      * Folder: Dev\n      * Folder: CTI, OSINT \u0026 SocMint \u003e Code Search Engines\n```\n\nUse `--show-tree` to verify that your folder structure was parsed correctly before generating a dashboard:\n\n```bash\npython bmctl.py audit -i bookmarks.json --show-tree\n```\n\n```\n  |   [   0]  Cybersecurity \u0026 CTI\n    +-- [   5]  C2 Frameworks\n    +-- [   7]  OSINT \u0026 Recon\n    +-- [   5]  Exploitation\n  |   [   0]  Development\n    +-- [   4]  Python\n    +-- [   4]  Go\n  |   [   6]  Tools\n  |   [   6]  Blogs \u0026 News\n```\n\n**Flags:**\n\n| Flag | Default | Description |\n|---|---|---|\n| `-i / --input` | required | Firefox JSON export |\n| `--top N` | `10` | Show top N most duplicated URLs |\n| `--show-short` | off | Stats only, skip duplicate list |\n| `--show-tree` | off | Print full folder hierarchy with bookmark counts |\n\n---\n\n### `compare`\n\nDiff two exports. Shows what was added and removed between two snapshots of the same collection.\n\n```bash\npython bmctl.py compare -o bookmarks-old.json -n bookmarks-new.json\n```\n\n```\n======================================================================\n                  COMPARISON REPORT\n======================================================================\n Unique bookmarks V1 (old)    : 2980\n Unique bookmarks V2 (new)    : 3059\n Net delta                    : +79\n======================================================================\n [+] New bookmarks added      : 102\n [-] Old bookmarks removed    : 23\n======================================================================\n\n[+] PREVIEW OF NEW ENTRIES (Max 15):\n  + HackTricks                     (Folder: Cybersecurity \u003e Documentation \u0026 Articles)\n    https://book.hacktricks.xyz\n```\n\n**Flags:**\n\n| Flag | Default | Description |\n|---|---|---|\n| `-o / --old` | required | Old JSON export |\n| `-n / --new` | required | New JSON export |\n| `--show-full` | off | Show complete added/removed lists (no limit) |\n| `--show-short` | off | Stats only, skip item lists |\n\n---\n\n### `merge`\n\nMerge two bookmark collections into a single Netscape HTML file (importable by any browser). Detects URL conflicts (same URL in different folders) and resolves them.\n\n```bash\npython bmctl.py merge -b bookmarks-base.json -n bookmarks-new.json -o merged.html\n```\n\nWithout `--no-confirm`, conflicts trigger an interactive prompt:\n\n```\n[?] FOLDER CONFLICT DETECTED FOR:\n    - URL  : https://example.com\n    - Title: Example Site\n    In which folder(s) do you want to keep it?\n      1) [Keep] -\u003e Dev \u003e Tools\n      2) [Keep] -\u003e Misc.\n      3) Skip / Keep most recent version\n    Your choice (1, 2...) :\n```\n\nTags from all instances are merged onto the surviving node.\n\nWith automatic conflict resolution (keeps most recent):\n\n```bash\npython bmctl.py merge -b base.json -n new.json -o merged.html --no-confirm\n```\n\n**Flags:**\n\n| Flag | Default | Description |\n|---|---|---|\n| `-b / --base` | required | Base JSON export |\n| `-n / --new` | required | JSON to merge in |\n| `-o / --output` | required | Output HTML file |\n| `--no-confirm` | off | Auto-resolve conflicts silently (keeps most recent) |\n\n---\n\n### `export`\n\nExport to a flat format. All formats include: Title, URL, Folder path, Tags, Date added.\n\n```bash\n# CSV\npython bmctl.py export -i bookmarks.json --format csv -o bookmarks.csv\n\n# Excel\npython bmctl.py export -i bookmarks.json --format xlsx -o bookmarks.xlsx\n\n# Markdown (organized by folder)\npython bmctl.py export -i bookmarks.json --format md -o bookmarks.md\n```\n\n**Flags:**\n\n| Flag | Default | Description |\n|---|---|---|\n| `-i / --input` | required | Firefox JSON export |\n| `--format` | required | `csv`, `xlsx`, or `md` |\n| `-o / --output` | required | Output file path |\n\n---\n\n### `dashboard`\n\nGenerate a fully self-contained single-file HTML dashboard. No server required - open directly in a browser.\n\n```bash\npython bmctl.py dashboard -i bookmarks.json -o dashboard.html\n```\n\n**Dashboard features:**\n- Sidebar with full collapsible folder tree (Firefox order preserved)\n- Three views: Dashboard (widget grid), Cards, Table\n- Global search across title, URL and tags\n- \"Recent additions\" quick view (last 50)\n- Folder-aware widget titles (relative path in folder view, full path in global view)\n- Pure black enterprise theme\n\n**Flags:**\n\n| Flag | Default | Description |\n|---|---|---|\n| `-i / --input` | required | Firefox JSON export |\n| `-o / --output` | `dashboard.html` | Output HTML file |\n\n\u003e **WSL users:** use Linux paths to avoid backslash stripping.\n\u003e ```bash\n\u003e # Correct\n\u003e python bmctl.py dashboard -i /mnt/c/Users/you/Desktop/bookmarks.json \\\n\u003e                           -o /mnt/c/Users/you/Desktop/dashboard.html\n\u003e # Wrong - bash strips backslashes without quotes\n\u003e python bmctl.py dashboard -i ... -o C:\\Users\\you\\Desktop\\dashboard.html\n\u003e ```\n\n---\n\n## URL Deduplication\n\nbmctl normalizes URLs before comparing them to find true duplicates:\n\n| Rule | Example |\n|---|---|\n| Scheme normalization | `http://` = `https://` |\n| Strip `www.` prefix | `www.github.com` = `github.com` |\n| Strip trailing slash | `github.com/` = `github.com` |\n| Strip `utm_*` params | `?utm_source=...` removed |\n| Query params preserved | `?q=foo` != `?q=bar` |\n\n`http://www.github.com/` and `https://github.com` resolve to the same canonical URL.\n\n---\n\n## Firefox JSON Compatibility\n\nbmctl handles both Firefox export formats:\n\n| Format | Bookmark | Folder |\n|---|---|---|\n| Legacy | `typeCode: 1` | `typeCode: 2` |\n| Modern | `type: \"text/x-moz-place\"` | `type: \"text/x-moz-place-container\"` |\n| Fallback | presence of `uri` field | presence of `children` field |\n\n---\n\n## License\n\nThis project is licensed under the [GNU Affero General Public License v3.0](LICENSE) (AGPL-3.0).\n\nAny use, modification, or distribution - including over a network - requires the full source code to remain open under the same license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffranckferman%2Fbmctl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffranckferman%2Fbmctl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffranckferman%2Fbmctl/lists"}