Bookmarkit is a modern, React‑powered application that helps you organize and query your bookmarks using natural language. It can be used both as a Vite/React web app and packaged as a Chrome extension.
Key capabilities include:
- Natural-language search and AI-driven actions
- Multi-provider LLM integration (Gemini, OpenAI, Grok, Ollama, LM Studio)
- Import/export to JSON or Netscape HTML
- Local or optional Firebase storage backend
The project is built with Vite, styled with Tailwind CSS, and designed for flexibility through runtime configuration and modular architecture.
- Natural language search (AI agent)
- Examples: “find github”, “find tags: react then sort by rating descending”, “show 3 stars or more”, “remove duplicates”
- Persist sorted order across all bookmarks (e.g., “reorder descending by title”)
- Import/Export
- JSON array of bookmarks
- Netscape Bookmark HTML (compatible with browsers’ export files)
- Bookmark management
- Add, edit, delete, tag, folder, rating, favicon support
- Multi‑select (Cmd/Ctrl+Click), open in new tab (Shift+Click)
- Detect and remove duplicates (by title + URL)
- URL status
- Lightweight validity check via HEAD request from the extension service worker
- One‑click “Ignore checking” toggle per bookmark
- LLM integration (runtime‑configurable)
- Gemini, OpenAI (ChatGPT), Grok (x.ai), Ollama (local), LM Studio (local)
- Model discovery (where supported), custom base URLs, stored per provider
- Storage backends
- Local (browser) by default
- Optional Firebase (Cloud Firestore) backend
- Keyboard shortcuts
- Click selects; Cmd/Ctrl+Click multi‑selects; Shift+Click opens
- Double‑click or E to edit
- Esc clears selection
- Cmd/Ctrl+A select all, Cmd/Ctrl+D delete selected
- D deletes (with confirmation), Space opens selected
- Top search bar: type natural language queries and hit Enter.
- Options: type “options” in the search bar to open provider settings.
- Import/Export: button in the header for JSON/HTML.
- Remove Duplicates: button in the header or type “remove duplicates”.
- Node.js — version pinned in
.nvmrc(nvm use) - npm
- Google Chrome (for the extension)
- Optional:
- API keys for LLMs (Gemini/OpenAI/Grok), or
- Local LLM runtime (Ollama or LM Studio)
- Download
bookmarkit-v<version>.zipfrom the latest release. - Optionally verify it against the published checksum:
shasum -a 256 -c bookmarkit-v<version>.zip.sha256
- Unzip it.
- Open
chrome://extensions, enable Developer mode, click Load unpacked, and select the unzipped folder. - Pin the toolbar icon. Clicking it opens quick-add for the current tab; "Open full app" in the popup opens the full bookmark manager.
Want a build of an unreleased branch? Every CI run attaches the same zip as a bookmarkit-extension
artifact — open the run from the
Actions tab and download it from the
run summary.
Node is pinned in .nvmrc (nvm use picks it up).
git clone https://github.com/qwts/bookmarkit.git
cd bookmarkit
npm install
npm run build:chrome # -> dist/, load that folder as unpacked
npm run devis not a dev server. It runsvite build --mode development— a sourcemapped, unminified build intodist/. There is no HMR: re-run it after each change and reload the extension fromchrome://extensions.npm run previewservesdist/over HTTP, but the default storage backend needs thechrome.bookmarksAPI, so the app only really works when loaded as an extension (or built with__use_firebase__).
Load either the released zip or your own dist/ build (see Install above) via
chrome://extensions -> Developer mode -> Load unpacked.
The extension ships two surfaces from one build:
| Surface | Entry | What it is |
|---|---|---|
| Toolbar popup | popup.html |
Quick-add for the current tab: prefilled title/URL, tags, rating, folder. Detects an already-saved URL and edits it instead of duplicating. |
| Full app | index.html |
The complete manager (search, agent, import/export, options). Opened from the popup's "Open full app". |
Permissions requested (public/manifest.json):
bookmarks— the default store keeps title/URL in the real Chrome bookmark tree.storage— settings, themes, and the per-bookmark metadata layer.<all_urls>— lets the background service worker run URL reachability checks from a privileged context (bypassing page CORS), and lets the popup read the active tab's title/URL. Requests are restricted to public http(s) hosts; private, loopback, and link-local addresses are blocked, and redirects are not followed.
You can configure everything at runtime in the Options dialog (type “options” in the search bar). Settings persist per browser in localStorage.
Supported providers:
- Gemini
- OpenAI (ChatGPT)
- Grok (x.ai)
- Ollama (local)
- LM Studio (local)
Options per provider:
- API key (remote providers)
- Base URL (OpenAI/Grok optional; Ollama/LM Studio required, e.g., http://localhost:11434 or http://localhost:1234)
- Model: auto‑discovery where supported, or type manually
Local providers:
- Ollama: install and run, pull a model (e.g., llama3.1), set base URL: http://localhost:11434
- LM Studio: run the local server, set base URL (default is often http://localhost:1234)
Tip: If an LLM call fails or returns invalid output, the app gracefully falls back to a general search.
You can set global defaults with Vite’s define. This is optional; the in‑app Options are usually enough.
vite.config.(js|ts) example:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// You can also read from process.env or .env files (VITE_* vars)
export default defineConfig({
plugins: [react()],
define: {
__llm_provider__: JSON.stringify("gemini"),
__llm_options__: JSON.stringify({
// apiKey: process.env.VITE_GEMINI_API_KEY,
// model: 'gemini-2.0-flash'
}),
__use_firebase__: JSON.stringify(false),
__firebase_config: JSON.stringify(undefined),
__app_id: JSON.stringify("bookmarkit"),
__initial_auth_token: JSON.stringify(undefined),
},
});If you set use_firebase to true, also provide a valid __firebase_config (see Firebase section).
The app can use Firebase (Cloud Firestore) instead of local storage.
Steps:
- Create a Firebase project and enable Firestore
- Grab your web app config:
- apiKey, authDomain, projectId, etc.
- Provide it at build time:
- Set use_firebase: true
- Set __firebase_config: the JSON stringified Firebase config
- Rebuild and run
Auth:
- The code exposes an optional __initial_auth_token if you want to inject an auth token at boot.
- If you don’t provide auth, your store module may default to anonymous or local. See your store implementation for details.
Data model:
- Bookmarks are stored with timestamps (createdAt/updatedAt)
- Reorder and live updates are supported via store methods
- JSON
- Exports an array of bookmark objects
- Import expects the same: an array
Bookmark JSON shape (id is optional on import):
[
{
"id": "optional",
"title": "Example",
"url": "https://example.com",
"description": "Short description",
"tags": ["reference", "web"],
"rating": 4,
"folderId": "work",
"faviconUrl": "https://example.com/favicon.ico",
"createdAt": "2024-01-01T12:00:00.000Z",
"updatedAt": "2024-01-01T12:00:00.000Z",
"urlStatus": "valid"
}
]- HTML (Netscape Bookmark File)
- Compatible with exports from Chrome/Firefox/etc.
- You can upload the file or paste its contents
- Export creates a standard bookmark HTML with fields like ADD_DATE, LAST_MODIFIED, ICON, DESCRIPTION
- find github
- find tags: react then sort by rating descending
- filter rating >= 4 then sort by title asc
- show 3 stars or more
- remove duplicates
- show all
- reorder ascending by title
- limit first 10
- options (opens Options dialog)
- import or export (opens Import/Export)
The agent plans actions (search, filter, sort, limit, persist reorder) and updates the view accordingly.
- Click: select a bookmark
- Cmd/Ctrl+Click: toggle multi‑select
- Shift+Click: open in new tab
- Double‑click or E: edit selected
- Esc: clear selection
- Cmd/Ctrl+A: select all visible
- Cmd/Ctrl+D or D: delete selected (with confirmation)
- Space (on focused tile): select/open depending on context
- The extension checks URLs with a HEAD request issued from the background service worker, which runs in a privileged context and so bypasses page CORS. No third-party proxy is involved — the URL is sent only to the site itself.
- Only public http(s) hosts are checked. Private, loopback, link-local, and cloud-metadata addresses are refused, and redirects are not followed (both are SSRF guards).
- If a site is blocked or unreachable, status may show “invalid”.
- You can toggle “Ignore checking” per bookmark.
- Local mode: data stays in your browser (local storage/IndexedDB per store implementation)
- Firebase mode: data is stored in your Firebase project
- API keys you enter in Options are saved to localStorage in your browser
- LLM calls are made from your browser to providers you select
- LLM errors/failures:
- Ensure API key and base URL (if applicable) are set in Options
- Try the “Refresh” button next to Model
- Use a smaller model or local provider for testing
- Extension shows blank page:
- Confirm manifest.json exists in dist
- Load unpacked pointing to the dist folder after a build
- Check Chrome console for CSP/network issues and adjust host_permissions
- URL check always invalid:
- Some sites block HEAD requests; toggle “Ignore checking”
| Script | What it does |
|---|---|
npm run dev |
Sourcemapped, unminified build to dist/. Not a dev server. |
npm run build |
Production build to dist/. |
npm run build:chrome |
Production build + copies the extension files into dist/. |
npm run preview |
Serves dist/ over HTTP (see the caveat under Build from source). |
npm run lint |
ESLint. |
npm test |
Vitest suite once (npm run test:watch to watch). |
npm run ci |
The full gate CI runs: version policy, lint, tests, extension build. |
npm run changeset |
Record a changeset describing a user-facing change. |
npm run licenses:notices |
Regenerate locked production dependency notices. |
npm run package:release |
Build and package release/bookmarkit-v<version>.zip + checksum. |
Releases are automated; nobody tags or uploads by hand.
- A PR with a user-facing change adds a changeset (
npm run changeset). - When it merges, Version cut opens or refreshes a rolling
chore: version bookmarkitPR that applies the pending changesets and bumpspackage.json,public/manifest.json, andpackage-lock.jsontogether. - Merging that PR is the release action: it tags
v<version>and triggers Release, which re-runs the full gate against the tag and publishes the zip +.sha256to a GitHub Release.
npm run check:version-policy enforces that the three version artifacts never drift apart — a
manifest that disagrees with package.json would ship a build that lies about its own version.
Issues and PRs are welcome. Start with CONTRIBUTING.md; the repository architecture is documented in DESIGN.md.
Bookmarkit is available under the MIT License. Bundled dependencies retain their own terms; see THIRD-PARTY-NOTICES.md.