tabai is a CLI tool that lets you control Google Chrome from your terminal using plain English. It connects a local Ollama language model to your browser via a Chrome extension and native messaging bridge, so you can close tabs, organize bookmarks, restore sessions, and more — all without touching the mouse.
# 1. Install CLI (also auto-compiles the native messaging binary on macOS)
cd /Users/(username)/Documents/browser_assistant/tabai/cli
npm install
npm link
# 2. Pull the Ollama model
ollama pull qwen3.5:2b
# 3. Load the Chrome extension
# Open chrome://extensions
# Enable "Developer mode" (top-right toggle)
# Click "Load unpacked" → select /Users/(username)/Documents/browser_assistant/tabai/extension
# Copy the Extension ID Chrome assigns
# 4. Register native messaging host (macOS)
mkdir -p ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts
cp /Users/(username)/Documents/browser_assistant/tabai/extension/com.tabai.bridge.json \
~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts/
# 5. Edit the copied manifest to set your actual values
# File: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.tabai.bridge.json
# Set "path" to: "/Users/(username)/Documents/browser_assistant/tabai/extension/native-host-bin"
# Set "allowed_origins" to: ["chrome-extension://YOUR_EXTENSION_ID/"]
# 6. Reload the extension at chrome://extensions (click refresh icon)
# 7. Add alias (optional)
echo 'alias t="tabai"' >> ~/.zshrc && source ~/.zshrc
# 8. Run it
tabai "what tabs do I have open?"
t "close all youtube tabs"
t "group tabs by domain"- Node.js v18 or later
- Google Chrome (or Chromium-based browser)
- Ollama installed and running locally (https://ollama.com)
cd /path/to/browser_assistantcd tabai/cli
npm install
npm linkThis makes the tabai command available globally. On macOS, npm install also automatically compiles the native messaging binary (native-host-bin) using your current node path.
- Open Chrome and go to
chrome://extensions - Enable Developer mode (toggle in the top-right corner)
- Click Load unpacked
- Select the
tabai/extensiondirectory - Note the Extension ID that Chrome assigns (a long string like
abcdefghijklmnopqrstuvwxyz)
The bridge server needs to be registered as a Chrome native messaging host.
macOS important: Chrome on macOS requires the native messaging host to be a compiled Mach-O binary, not a script. Even with a valid shebang and
chmod +x, Chrome will silently refuse to execute.jsor.shfiles — the process dies before the first line of code runs. The solution is a tiny C wrapper (native-host-wrapper.c) thatexecs node withnative-host.js. This binary is automatically compiled duringnpm install(step 2).
macOS:
# The binary (native-host-bin) was already built by npm install.
# If you need to rebuild manually:
# cd /path/to/tabai/extension
# cc -DNODE_PATH='"'$(which node)'"' -o native-host-bin native-host-wrapper.c
# Create the manifest directory if it doesn't exist
mkdir -p ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts
# Copy the manifest
cp /path/to/tabai/extension/com.tabai.bridge.json \
~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts/
# Edit the manifest to set the correct path and extension ID:
# "path": "/path/to/tabai/extension/native-host-bin"
# "allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID/"]Note: The node path is baked into the binary at compile time (from
which node). If you switch node versions via nvm, re-runnpm installincli/to recompile. Changes tonative-host.jsdo not require recompiling.
Linux:
# On Linux, Chrome can run scripts directly — no binary wrapper needed
chmod +x /path/to/tabai/extension/native-host.js
mkdir -p ~/.config/google-chrome/NativeMessagingHosts
cp /path/to/tabai/extension/com.tabai.bridge.json \
~/.config/google-chrome/NativeMessagingHosts/
# Edit the manifest: set "path" and "allowed_origins"
# "path": "/path/to/tabai/extension/native-host.js"
# "allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID/"]Replace /path/to/tabai with your actual path, and YOUR_EXTENSION_ID with the ID from step 3.
ollama pull qwen3.5:2bEnsure Ollama is running (it starts automatically on macOS; on Linux run ollama serve).
Add this to your ~/.zshrc or ~/.bashrc:
alias t="tabai"Then reload:
source ~/.zshrcThe extension expects a tabai/extension/icon.png file (128x128 pixels). This is purely cosmetic for the extensions page. You can use any PNG or skip it — the extension works without it.
tabai "close all youtube tabs"Or with the alias:
t "close all youtube tabs"t "close all tabs"
t "close the twitter tab"
t "close all tabs except the one I'm looking at"
t "close duplicate tabs"
t "close tabs I haven't used in a while"t "open github.com"
t "open hacker news and reddit"
t "open my usual morning sites: gmail, calendar, github"t "find my jira tab"
t "which tab has the deployment docs"
t "do I have any stackoverflow tabs open"t "group tabs by domain"
t "group all the google docs tabs together"
t "pin the gmail tab"
t "unpin all tabs"
t "mute the youtube tab"t "bookmark all open tabs"
t "bookmark this tab"
t "find my bookmarks about kubernetes"
t "list all bookmarks"t "restore the last tab I closed"
t "reopen the last 3 tabs I closed"
t "save this session"
t "show my saved sessions"
t "restore yesterday's session"
t "what tabs did I close recently"t "how many tabs do I have open"
t "show me all my tabs"
t "reload all tabs"
t "duplicate this tab"Settings live in tabai/config.json:
{
"ollamaUrl": "http://localhost:11434",
"model": "qwen3.5:2b",
"think": false,
"bridgePort": 9999,
"confirmDestructive": true
}Use a different model:
tabai --model llama3:8b "close all reddit tabs"Skip confirmation for destructive actions:
tabai -y "close all tabs"
tabai --no-confirm "close all tabs"Use a different Ollama server:
tabai --ollama-url http://192.168.1.50:11434 "group tabs by domain"Change the bridge port (set both for bridge and CLI):
TABAI_PORT=8888 tabai "list tabs"Or change bridgePort in config.json (the CLI also reads this).
Terminal Bridge Server Chrome Extension
| | |
| tabai "close yt tabs" | |
| ----HTTP POST /action->| |
| |---native messaging msg---->|
| | | chrome.tabs.remove(...)
| |<--native messaging resp----|
| <---HTTP JSON response-| |
| | |
- The CLI sends your natural language command to Ollama, which returns a structured action.
- The CLI sends the action to the bridge server on
localhost:9999via HTTP. - The bridge server forwards the action to the Chrome extension via native messaging.
- The extension executes the action using Chrome APIs and returns the result.
- The result flows back to the CLI and is displayed in the terminal.
Every tabai invocation is automatically logged to ~/.tabai/calls.jsonl as a JSON Lines file. Each entry records:
| Field | Description |
|---|---|
timestamp |
When the call was made |
command |
Your natural language input |
tabs |
All open tabs at the time (title + url) |
llmAction |
What the LLM returned (before validation) |
finalAction |
What actually executed (after client-side corrections) |
wasOverridden |
true if validation changed the LLM's output |
result / error |
Execution outcome |
model |
Which Ollama model was used |
This log is useful for finding patterns where the LLM gets things wrong, so you can add more correction rules and improve tab handling over time.
# View all logged calls
cat ~/.tabai/calls.jsonl | jq .
# Show calls where client-side validation corrected the LLM
cat ~/.tabai/calls.jsonl | jq 'select(.wasOverridden)'
# Show failed calls
cat ~/.tabai/calls.jsonl | jq 'select(.error != null)'
# Count calls per action type
cat ~/.tabai/calls.jsonl | jq -r '.finalAction.action' | sort | uniq -c | sort -rn
# See what the LLM wanted vs what actually ran (for overrides only)
cat ~/.tabai/calls.jsonl | jq 'select(.wasOverridden) | {command, llm: .llmAction.action, final: .finalAction.action}'- Make sure the Chrome extension is loaded and enabled at
chrome://extensions - Verify the native messaging host manifest is in the correct directory:
- macOS:
~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.tabai.bridge.json - Linux:
~/.config/google-chrome/NativeMessagingHosts/com.tabai.bridge.json
- macOS:
- Check that the
pathin the manifest points to the correct file:- macOS: must point to
native-host-bin(the compiled binary), notnative-host.js - Linux: can point to
native-host.jsdirectly
- macOS: must point to
- Check that the
allowed_originscontains your extension's ID - Reload the extension at
chrome://extensions(click the refresh icon)
Another instance of the bridge server is running. Kill it:
lsof -ti:9999 | xargs killOr change the port in config.json and set TABAI_PORT accordingly.
- Make sure Ollama is running:
ollama serve - Verify the model is pulled:
ollama list - Pull it if missing:
ollama pull qwen3.5:2b - Check that
ollamaUrlinconfig.jsonmatches your Ollama server address
Run npm link again from the tabai/cli directory:
cd tabai/cli && npm link- macOS: make sure you compiled the binary wrapper. Chrome on macOS will not execute scripts (
.js,.sh) as native messaging hosts — it requires a Mach-O binary. If you see repeated "Native host has exited" errors, this is almost certainly the cause. Fix:Then ensure the manifestcd tabai/extension cc -o native-host-bin native-host-wrapper.c chmod +x native-host-binpathpoints tonative-host-bin, notnative-host.js. - Run
native-host.jsmanually to check for startup errors:node tabai/extension/native-host.js 2>&1 & # Then test the HTTP bridge: curl http://127.0.0.1:9999/ping # Kill it when done: kill %1
- Check
~/.tabai/native-host.logfor crash logs written by the native host. - Check Chrome's extension service worker logs: go to
chrome://extensions, find tabai bridge, and click "Inspect views: service worker"
The extension rebuilds its tab index on startup. If tabs appear missing, reload the extension at chrome://extensions.
- Use a smaller Ollama model for faster inference (e.g.,
qwen3.5:1b) - Ensure Ollama is using GPU acceleration: check
ollama ps - The native messaging roundtrip adds minimal latency; slowness is almost always model inference time