@egradman/jio
v1.0.0
Published
Personal bookmark manager - extract, index, and search vault links
Readme
jio
I keep my brain in Obsidian. Throughout the day, as I collect useful links either for work or for personal projects or fun, I jam them in Obsidian. Hence the name.
Jio is designed for people who keep notes in Markdown files. It scans your vault for links, builds a full-text search index, and lets you find any bookmark instantly -- from the terminal, a browser search bar, or any HTTP client.
It extracts URLs from your Markdown vault, indexes them with SQLite FTS5, and provides fast search through both a CLI and an HTTP API. It has a Cloudflare Worker, and a tool to sync your database to that worker, enabling you to search your bookmarks using an OpenSearch compatible API (Chrome custom search engine, Raycast Quicklink, etc).
I primarily interact with it using Openclaw. My HEARTBEAT.md includes a directive to rebuild and sync my bookmarks. I can also ask Openclaw to find any "forgotten links," which have been added but not navigated in the first week, as a weak proxy for things I added but did not follow up on.
Because I don't like to mix business and business and business and personal, you can scope your bookmarks by name and create tokens that restrict the search engine to individual scopes: so the Chrome custom search engine on my Dayjob Mac only has access to Dayjob links.
Jio is distributed as a skill. Install it in OpenClaw, ask OpenClaw to configure it for your Cloudflare Workers, assign a master token, deploy it, and enjoy!
Features
- Vault scanning -- Extracts Markdown links (
[title](url)) and bare URLs from.mdfiles - Full-text search -- SQLite FTS5 with Porter stemming and Unicode support
- Scopes -- Organize bookmarks by project or category using directory-level config or YAML frontmatter
- HTTP API -- Token-authenticated search with OpenSearch-compatible JSON responses
- Browser integration -- Add as a custom search engine in Chrome, Firefox, or any browser
- Click tracking -- Tracks which links you actually use and when
- Forgotten links -- Surface bookmarks you saved but never visited
- CLI-first -- Full functionality available from the command line
Installation
Run directly with npx (no install)
npx @egradman/jio refresh --vault ~/notes
npx @egradman/jio search "kubernetes deploy"Install globally
npm install -g @egradman/jioThen use jio directly:
jio refresh --vault ~/notes
jio search "kubernetes deploy"Install from source
git clone https://github.com/egradman/jio.git
cd jio
npm install
npm linkRequirements
- Node.js 18+
- A directory of Markdown files (your "vault")
Quick Start
# Import bookmarks from your vault
jio refresh --vault ~/notes
# Search from the terminal
jio search "kubernetes deploy"
# Open the first matching result in your browser
jio open "github actions"All commands also work with npx @egradman/jio if you haven't installed globally.
Configuration
Config File (jio_conf.json)
The CLI looks for a jio_conf.json config file in three locations (first found wins):
./jio_conf.json(project directory)~/.config/jio_conf.json(XDG config)~/.jio_conf.json(home directory)
cp jio_conf.example.json jio_conf.json
# Edit jio_conf.json with your values{
"masterToken": "your-secret-here",
"syncServer": "https://jio.example.com"
}This is the recommended way to configure jio sync. The file is gitignored. For per-project config, place it in the project directory. For global config, place it in your home directory as ~/.jio_conf.json.
Environment Variables
Create a .env file in the project root for CLI settings:
# Path to SQLite database (default: ./bookmarks.db)
DB_PATH=./bookmarks.db
# Path to your Markdown vault (default: ~/vault)
VAULT_PATH=~/vaultEnvironment variables MASTER_TOKEN and SYNC_SERVER are also supported as a fallback for jio sync, but jio_conf.json is preferred.
Scopes
Scopes let you organize bookmarks into categories. There are two ways to assign scopes:
Directory-level -- Place a .jio.json file in any vault directory:
{
"scope": "work"
}All links discovered in that directory (and subdirectories) inherit the scope.
File-level -- Add YAML frontmatter to any Markdown file:
---
jio_scope: personal
---
# My notes
...File-level scopes override directory-level scopes.
CLI Reference
jio refresh
Scan your vault and import/update bookmarks.
jio refresh [--vault <path>] [--quiet]| Option | Description |
|--------|-------------|
| --vault <path> | Path to vault directory (default: $VAULT_PATH or ~/vault) |
| --quiet | Suppress output |
jio search <query>
Search bookmarks from the terminal.
jio search "docker compose" [--scope <scope>] [--limit <n>] [--json]| Option | Description |
|--------|-------------|
| --scope <scope> | Filter results to a specific scope |
| --limit <n> | Maximum number of results |
| --json | Output as JSON |
jio open <query>
Search and open the first matching result in your default browser.
jio open "grafana dashboards"jio forgotten
Find bookmarks you saved but never clicked. Queries the remote Cloudflare Worker (not the local database).
jio forgotten [--days <n>] [--scope <scope>] [--limit <n>] [--all] [--json] [--server <url>] [--token <token>]| Option | Description |
|--------|-------------|
| --days <n> | Minimum age in days (default: 14) |
| --scope <scope> | Filter by scope |
| --limit <n> | Maximum number of results (default: 20) |
| --all | Show all forgotten links (sets limit to 1000) |
| --json | Output as JSON |
| --server <url> | Worker URL (overrides jio_conf.json and SYNC_SERVER) |
| --token <token> | Master token (overrides jio_conf.json and MASTER_TOKEN) |
jio stats
Display bookmark statistics. Queries the remote Cloudflare Worker (not the local database).
jio stats [--json] [--server <url>] [--token <token>]| Option | Description |
|--------|-------------|
| --json | Output as JSON |
| --server <url> | Worker URL (overrides jio_conf.json and SYNC_SERVER) |
| --token <token> | Master token (overrides jio_conf.json and MASTER_TOKEN) |
jio tokens
Manage API tokens for HTTP access.
# List all tokens
jio tokens list [--json]
# Create a new token
jio tokens add --name "chrome" [--scope <scope>]
# Delete a token
jio tokens delete --id <id>Tokens are read-only and used for search. Tokens with a --scope can only search within that scope. Tokens without a scope have access to all bookmarks. Write operations (sync, token management on the Worker) use the MASTER_TOKEN env var instead.
jio sync
Push local bookmarks to a remote Cloudflare Worker.
jio sync [--server <url>] [--token <master-token>]| Option | Description |
|--------|-------------|
| --server <url> | Worker URL |
| --token <token> | Master token |
Resolution order for both values: CLI flags > jio_conf.json > environment variables (SYNC_SERVER, MASTER_TOKEN).
If you have jio_conf.json configured, just run jio sync with no flags.
HTTP API (Cloudflare Worker)
These endpoints are served by the Cloudflare Worker. Search endpoints require a read-only API token. Write endpoints require the master token.
GET /
Returns an HTML page with usage instructions for the API.
GET /search/:token?q=<query>
Search bookmarks.
Parameters:
| Parameter | Description |
|-----------|-------------|
| q | Search query string |
Behavior:
- Returns up to 10 results ranked by FTS5 relevance
- If the query ends with a number (e.g.,
"grafana 2"), that number selects a specific result from the list - Scoped tokens only return results within their scope
- Accepts
Accept: application/jsonfor JSON responses
JSON response format (OpenSearch Suggestions):
[
"query",
["Title 1", "Title 2"],
["Description 1", "Description 2"],
["https://url1.com", "https://url2.com"]
]HTML response: An interactive results page with keyboard navigation (press 1-9 to open a result).
GET /go/:token?url=<url>
Redirect to a URL while tracking the click. Increments the followed_count and updates last_followed for the bookmark.
POST /import/:masterToken
Import links into the database. Requires the master token.
Request body:
{
"links": [
{ "url": "https://example.com", "title": "Example", "description": "...", "scope": "work" }
]
}Response:
{ "imported": 42 }Links are upserted -- existing URLs have their title, description, and scope updated.
GET /tokens/:masterToken
List all API tokens. Requires the master token.
POST /tokens/:masterToken
Create a new API token. Requires the master token.
Request body:
{ "name": "chrome", "scope": "work" }Response:
{ "token": "abc123...", "name": "chrome", "scope": "work" }DELETE /tokens/:masterToken/:id
Delete an API token by ID. Requires the master token.
GET /stats/:masterToken
Returns bookmark statistics. Requires the master token.
Response:
{
"total": 500,
"withClicks": 42,
"totalClicks": 128,
"forgottenCount": 200,
"byScope": [{ "scope": "work", "count": 300 }, { "scope": "personal", "count": 200 }],
"topClicked": [{ "url": "https://example.com", "title": "Example", "scope": "work", "followed_count": 15 }]
}GET /forgotten/:masterToken
Returns links that were saved but never clicked. Requires the master token.
| Parameter | Description |
|-----------|-------------|
| days | Minimum age in days (default: 14) |
| scope | Filter by scope |
| limit | Maximum number of results (default: 20) |
Response:
{
"forgotten": [{ "url": "https://example.com", "title": "Example", "scope": "work", "discovered_at": "2025-01-01", "followed_count": 0 }],
"days": 14,
"count": 1
}Browser Search Engine Setup
After deploying the Worker and creating a token, add jio as a custom search engine in your browser:
URL: https://jio.<your-subdomain>.workers.dev/search/YOUR_TOKEN?q=%sIn Chrome: Settings > Search engine > Manage search engines > Add.
Database
Jio uses SQLite with WAL mode for concurrent reads. The database is created automatically on first run and contains:
- links -- Bookmark records with URL, title, description, scope, and usage stats
- links_fts -- FTS5 virtual table for full-text search (Porter stemmer, Unicode61 tokenizer)
- api_tokens -- Token-based API authentication
The database file is stored at DB_PATH (default: ./bookmarks.db).
How Link Extraction Works
During jio refresh, Jio scans all .md files in your vault and extracts:
- Markdown links --
[link text](https://example.com)-- the link text becomes the title - Bare URLs --
https://example.comfound anywhere in the text
It automatically filters out:
- Localhost/loopback URLs
- Asset files (
.png,.jpg,.css,.js,.woff, etc.) - Duplicate URLs (only the first occurrence is stored)
Links are upserted -- existing URLs are updated with new metadata, and new URLs are inserted.
Cloudflare Workers Deployment
Jio runs on Cloudflare Workers with D1 for globally distributed, serverless bookmark search.
Authenticate with Cloudflare
If deploying from a headless/SSH machine, wrangler login can't open a browser directly. Two options:
Port-forward (recommended for one-off setup):
# On your local machine
ssh -L 8976:localhost:8976 your-server
# On the remote machine
npx wrangler login --browser false
# Open the printed URL in your local browser to authorizeAPI token (better for CI or persistent remote access):
- Go to https://dash.cloudflare.com/profile/api-tokens
- Create a token using the "Edit Cloudflare Workers" template
- Export it:
export CLOUDFLARE_API_TOKEN="your-token"
Setup and Deploy
# 1. Install dependencies
npm install
# 2. Create the D1 database
npx wrangler d1 create jio-db
# Copy the database_id from the output into wrangler.toml
# 3. Run schema migrations against remote D1
npx wrangler d1 migrations apply jio-db --remote
# 4. Generate a master token and set it everywhere
MASTER_TOKEN=$(openssl rand -hex 32)
echo "$MASTER_TOKEN" | npx wrangler secret put MASTER_TOKEN
# Write CLI config so `jio sync` works
mkdir -p ~/.config
cat > ~/.config/jio_conf.json <<EOF
{
"masterToken": "$MASTER_TOKEN",
"syncServer": "https://jio.<your-subdomain>.workers.dev"
}
EOF
# 5. Deploy the Worker
npx wrangler deploy
# 6. Sync local bookmarks to the Worker
jio refresh --vault ~/vault
jio syncManaging Search Tokens
Create and manage search tokens on the Worker using the master token:
# Create a search token
curl -X POST https://jio.example.com/tokens/<master-token> \
-H 'Content-Type: application/json' \
-d '{"name": "chrome", "scope": "work"}'
# List tokens
curl https://jio.example.com/tokens/<master-token>
# Delete a token
curl -X DELETE https://jio.example.com/tokens/<master-token>/1Alternative: SQL dump (for initial setup or bulk replace):
sqlite3 bookmarks.db .dump > d1-import.sql
npx wrangler d1 execute jio-db --file=d1-import.sqlLocal Development
# Copy the example and set your local master token
cp .dev.vars.example .dev.vars
# Start the local Worker
npm run worker:devThis starts a local Worker with a local D1 database at http://localhost:8787. Wrangler automatically loads .dev.vars as environment variables during local development. The .dev.vars file is gitignored.
Environment Variables
Set BASE_URL in wrangler.toml under [vars] for non-secret config:
[vars]
BASE_URL = "https://jio.example.com"MASTER_TOKEN should never go in wrangler.toml (it would be committed to git). Instead:
- Local dev: Put it in
.dev.vars(gitignored, loaded automatically bywrangler dev) - Production: Set it with
npx wrangler secret put MASTER_TOKEN(encrypted, stored by Cloudflare)
OpenClaw Skill
Jio is distributed as an OpenClaw skill. The SKILL.md file contains the skill definition that tells OpenClaw how to configure and use jio. You can install this skill in OpenClaw to enable your AI agent to manage and search your bookmarks automatically, including refreshing your vault, querying forgotten links, and managing tokens.
License
ISC
