ko
A personal command-line toolkit for the things I look up or grab all day — papers, X, Hacker News, the web, Google (Sheets/Docs/Calendar/Gmail), documents — without opening a browser tab.
It's opinionated, not generic: each command is a thin wrapper over the one library or API I settled on for that job, with my preferred defaults baked in and only a couple of flags exposed. Built so a human skimming --help and an AI agent calling it from bash get the same clean, pipeable output — plain text / TSV by default, --json when you want structure, errors to stderr. A few of the APIs are paid; at personal scale that's a few dollars a month, and life's too short to reimplement them.
Full docs: kotools.khalido.dev.
The tools
Read & search
| Command | What it does | What I use it for |
|---|---|---|
ko papers |
cross-publisher search + citation graph (OpenAlex, no key) | "state of the art on X" — find a seed paper, snowball cites/refs |
ko arxiv |
arxiv relevance search + paper → markdown | pull a specific paper to read or feed an agent |
ko hf |
HF Daily Papers: trending ML + linked code/models | what's hot in ML today, with repos to try |
ko hn |
Hacker News top / search / comment trees (no key) | practitioner signal + the actual discussion thread |
ko x |
X search, lists, user timelines (official XDK) | ko x ai = my AI list; search a list back months |
ko exa |
semantic web search + URL → markdown (Exa) | find posts/lab-pages a keyword search misses |
ko fetch |
any URL → clean markdown (PDF, arxiv, Wayback fallback) | ko <url> — read a page or PDF as text |
ko doc |
PDF/Office/image → text, fully local (no models) | ko report.pdf — no upload, no key |
ko yt |
YouTube → transcript (free, no key); -s summarizes |
"what does this 40-min talk actually say" |
ko tv |
movie/TV rating + where to stream (TMDB, AU) | "worth watching, and where can I?" |
ko tt |
TickTick lists + tasks (read-only, via MCP) | ko tt today — my open tasks |
AI agents
| Command | What it does | What I use it for |
|---|---|---|
ko ai |
the default agent — every tool above + memory | "find X, read it, tell me what matters" |
ko agent |
specialist agents (research/repo/tv), resumable |
deep literature digs; "how does repo X do Y" |
ko llm |
one-shot LLM, stdin-aware, never has tools | … | ko llm "summarize" inside a pipe |
ko brief |
morning brief: calendar + mail + HN + papers → one summary | the day's triage in one read |
| Command | What it does | What I use it for |
|---|---|---|
ko gsheets |
read & write Google Sheets (OAuth) | dump/read data + formulas, overwrite-guarded |
ko gdocs |
Markdown ↔ Google Docs + comments + tabs | push a proposal .md → Doc → read feedback back; per-tab get/edit |
ko cal |
Google Calendar agenda + quick-add | next 7 days; "when was my last dentist?" |
ko gmail |
read Gmail + attachments + drafts | ko gmail from alice; ko gmail attach <id> → ko doc; ko gmail draft reply <id> --md note.md |
Make & manage
| Command | What it does | What I use it for |
|---|---|---|
ko publish |
scaffold + deploy a site to Cloudflare | ship a landing page / write-up / mini-tool |
ko prompt |
my "how I build X" kickoff briefs | ko prompt research-papers → load into an agent |
ko refs |
manage ~/code/refs (reference repo clones) |
ko refs pulls all; add <url> clones + remembers |
Utilities: ko doctor (setup status — run it first), ko models (model strings for -m), ko billing (credits left), ko logs, ko mcp (inspect/call MCP servers). Every LLM call prints its cost to stderr — OpenRouter's actual billed cost when available. Agents keep their own markdown memory (plus a shared ~/.config/ko/memory.md about you) and save resumable sessions; ko agent sessions summarize builds a searchable index of them.
It composes
Plain text / TSV by default (add --json for structure), so ko pipes into itself, into ko llm, and into standard tools:
ko hn item 48480978 | ko llm "summarize the debate; what's the consensus?"
ko fetch https://example.com/post | ko llm "key claims as bullets"
ko papers search "retrieval augmented generation" --json | jq -r '.[].doi'
ko x search "claude code" --list ai --days 90 # my AI list, 3 months back
Install
uv tool install kotools # or one-off, no install: uvx kotools
The package is kotools; the command it installs is ko. Run ko doctor first to see what's set up.
For development (editable clone):
uv tool install --editable /path/to/kotools # or: uvx --from /path/to/kotools ko --help
API keys
Keys live in environment variables (shell profile or .env) or in ~/.config/ko/config.toml under a [keys] table (env always wins) — never in the repo. ko doctor shows each key's source (env / config / missing). What you need depends on what you use; most of ko works with no keys at all.
| Env var | Used by | Paid? | Notes |
|---|---|---|---|
EXA_API_KEY |
ko exa, agents |
💰 | Search $7/1k requests (contents for 10 results included); standalone contents $1/1k pages. exa.ai |
OPENROUTER_API_KEY |
ko llm, ko ai, agents, ko brief — the default for everything |
💰 | One prepaid pool, any model. Baked model tiers ride on it: basic = deepseek-v4-flash (llm/brief/summarize), medium = glm-5.2 (ko ai), smart = grok-latest (research). Override per-tier in [llm]; -m per run. ko billing/doctor show $ left; every call prints its actual cost to stderr. |
GEMINI_API_KEY |
-m google:… escape hatch |
💰 | Direct Gemini (-m google:gemini-3.5-flash), or for Gemini-only features later. |
X_BEARER_TOKEN |
ko x |
💰 | X API v2 Bearer Token. Pay-per-use since 2026 (prepaid credits, ~$0.005/post read). developer.x.com |
TMDB_READ_ACCESS_TOKEN |
ko tv |
free | v4 Read Access Token from TMDB settings. |
TICKTICK_API_KEY |
ko tt |
(TickTick sub) | TickTick app → Account → MCP → generate. Read-only here. |
S2_API_KEY |
ko papers (optional) |
free | Semantic Scholar key — adds tldr + similar; everything else works keyless. |
| — (Google OAuth) | ko gsheets |
free | Not a key: one-off browser consent, token cached locally. See below. |
| — | ko arxiv, ko hn, ko hf, ko papers, ko doc |
free | No auth at all. |
Where ko keeps things
Three XDG-style dirs, three jobs (same pattern as gh/opencode; identical on macOS and Linux):
| Dir | Job | Holds |
|---|---|---|
~/.config/ko/ |
your config — safe to dotfile-sync | config.toml (keys + settings), google_client.json, mcp.json, prompts/ overrides |
~/.local/state/ko/ |
machine-local state — never sync | OAuth tokens, agent sessions, ko.db, command logs, publish registry |
~/.cache/ko/ |
disposable — safe to delete | model catalogs and other regenerable caches |
Each is overridable via KO_CONFIG_DIR / KO_STATE_DIR / KO_CACHE_DIR. ko doctor
prints all three, plus every effective setting and where it resolved from
(env / config / default) — and warns if config.toml is malformed.
Google Sheets setup (one-off) — read & write
ko gsheets runs as you against your Google account (OAuth user flow, no service account). One
token grants read and write; reads use the narrower read-only scope under the hood.
Each step below links straight to the right console page. They all take a ?project=<project-id>
query param — paste your project's ID (from step 1) into the links, or just keep the project selected
in the console's top bar. The gcloud CLI can do step 2; steps 3–4 are console-only.
- Create a Google Cloud project → https://console.cloud.google.com/projectcreate . Note its
project ID (e.g.
kotools-500611) — the links below use it. - Enable the APIs → API Library:
enable Google Sheets, Google Docs (
ko gdocs), Google Calendar (ko cal), Gmail (ko gmail— reads are read-only;gmail.composeadds drafts, sending stays opt-in), and Google Drive (the narrowdrive.filescope only — see below).kostill can't browse your Drive; one token covers all. Fast path with the gcloud CLI:gcloud services enable sheets.googleapis.com docs.googleapis.com calendar-json.googleapis.com gmail.googleapis.com drive.googleapis.com - Configure + publish the OAuth consent screen →
Auth Platform overview
(set app name + support email, User type: External), then
Audience → Publish app.
This step decides whether your refresh token lasts:
- Workspace org and only need org accounts? User type: Internal — no token expiry, no publish needed. But Internal can't authorize a personal Gmail — use External if you want personal + work.
- External (any account): a project left in Testing expires its refresh token after 7 days and requires each account added as a Test user. Publish the app to remove both — it stays "unverified" (a warning you click through on your own app), but the token no longer expires.
- You don't configure scopes here —
korequests them in code (Sheets/Docs/Calendar + Gmail-readonly + Drivedrive.file). Adding an API later = one re-auth (ko gsheets auth --logoutthenko gsheets auth).
- Create OAuth credentials → Credentials → Create Credentials → OAuth client ID → Application type: Desktop app → Download the JSON.
- Save the JSON to
~/.config/ko/google_client.json(or setKO_GOOGLE_CLIENT_FILE=<path>). - Run
ko gsheets auth. A browser opens; approve (click through the "unverified app" warning). The refresh token caches at~/.local/state/ko/google_token.json(relocatable viaKO_STATE_DIR).
Reuse on other machines: that token file is portable — copy it to another machine's same path (or
point KO_STATE_DIR at it) to reuse the auth with no re-consent.
Already authed read-only? ko gsheets auth --logout, then ko gsheets auth to upgrade to read+write.
Write safety. set / put / header / add-tab / new / clear need the read+write grant. The
shape-aware writers refuse to overwrite non-empty cells (the error lists exactly which) unless you
pass --overwrite — including formulas that currently display blank.
Why OAuth, not a service account? A service account needs every sheet explicitly shared with its email — fine for bots, tedious for a personal read/write-anywhere CLI. OAuth gives access to anything the signed-in account can see. Scope to one folder? No — OAuth scopes are per-API, not per-resource; use a service account with individual share grants if you need tighter control.
Markdown ↔ Google Docs + comments (ko gdocs push/get/comments)
ko requests the narrow drive.file Drive scope — and deliberately not drive/drive.readonly.
drive.file grants access only to files ko itself creates or that you open by ID — it still
cannot browse, search, or list your Drive. That's exactly enough for a Markdown-first proposal loop,
without widening the blast radius:
ko gdocs push proposal.md --title "Acme — Proposal" --folder Proposals # .md → formatted Doc
# → review & comment in Google Docs (you + colleagues)
ko gdocs comments <doc> # read the feedback back (replies indented, [id] shown)
ko gdocs reply <doc> <comment-id> "on it" # reply to a thread without leaving the terminal
ko gdocs replace <doc> "Q2" "Q3" # surgical in-place edit — keeps comment threads
ko gdocs push proposal.md <doc> # re-push edits in place (same URL; diff+confirm, --force skips)
ko gdocs get <doc> -o proposal.md # pull the Doc back as real Markdown (tables incl.)
# Tabs — per-client shared docs keep one tab per client/topic:
ko gdocs tabs <doc> # list tabs (index, title, tabId; sub-tabs indented)
ko gdocs add-tab <doc> "Acme" # add a tab (Docs API; refuses a duplicate title)
ko gdocs get <doc> --tab Acme # read just that tab (title or tabId, case-insensitive)
ko gdocs append <doc> "note" --tab Acme # append to one tab's end
ko gdocs replace <doc> "Q2" "Q3" --tab Acme # scope the replace to one tab (refuses if it'd orphan a comment; --force)
Keep the Markdown in git as the source of truth; push to a Doc for review, export to reconcile.
What converts (and what doesn't — code blocks and blockquotes don't): see the tested support
matrix in docs/gdocs-markdown.md.
--folder takes a folder ID, a /folders/ URL, or a name (Proposals) that ko
find-or-creates. Caveat: because drive.file only sees files ko made, get/comments
work on docs ko pushed — not on a pre-existing doc someone else created (that returns 404).
For broad read access you'd need drive.readonly, which ko intentionally refuses. Images in a
pushed doc embed as base64 and don't round-trip cleanly — fine for text proposals.
Multiple Google accounts (work + personal)
ko gsheets supports several accounts — each keeps its own token, and you flip between them.
- Pick the active account (in priority order):
--account/-aon any command (ko gsheets -a personal info <id>); theKO_GOOGLE_ACCOUNTenv var; or[google] accountin~/.config/ko/config.toml. Unset ="default"(the single-account setup above, unchanged). - Auth each once:
ko gsheets -a work auththenko gsheets -a personal auth. Tokens cache per account —google_token_work.json,google_token_personal.json(thedefaultaccount keeps the legacygoogle_token.json).ko gsheets accountslists them (*= active). - OAuth client (the subtle bit). One Desktop client can authorize multiple accounts, so usually
a single
~/.config/ko/google_client.jsonis all you need — just auth each account into it. The exception: if your work account is a Workspace org with an Internal consent screen, that client can't authorize a personal Gmail (and vice-versa). Then give that account its own client from a separate GCP project, saved asgoogle_client_<account>.json(e.g.google_client_personal.json);kouses the per-account file if present, else falls back to the sharedgoogle_client.json. - Set your usual default with
[google] account = "work", and reach for-a personalnow and then.
Output conventions
- Default output is human-readable and designed to pipe (
ko gsheets getemits TSV,ko arxiv searchemits short line format). --jsoneverywhere structured data would help. Agents should prefer--json.- Errors go to stderr. Empty results are not errors — exit
0with a friendly message. - Exit codes:
0success,1runtime error (auth, network, API),2usage error.
Dev
cd kotools
uv sync
npm install # pinned wrangler for `ko publish` (needs Node; via fnm/nvm/etc.)
uv run pytest # offline by default; KO_LIVE_TESTS=1 enables live-API tests
uv run ko --help
Python 3.14, uv, ruff, pytest, typer. Candidate subcommands and design notes: docs/ideas.md.
License
GPL-3.0-or-later. See LICENSE.
Metadata
Release files for kotools 2026.7.27
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kotools-2026.7.27.tar.gz | 397.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kotools-2026.7.27-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 623.3 kB
Release files / kotools-2026.7.27.tar.gz
| Download URL | kotools-2026.7.27.tar.gz |
|---|---|
| Size | 397.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b2c11dc9ba0d4689bb70bea2cf7e01b66a165eef853da94c9dc79ca52c6fcc1c
|
|
BLAKE2b-256 checksum How to use checksums |
0af5ca5e81c8c53fad45ad3a320d7af9d0f28a662851dfc1ea67a1506d50131e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / kotools-2026.7.27-py3-none-any.whl
| Download URL | kotools-2026.7.27-py3-none-any.whl |
|---|---|
| Size | 226.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eb188316093e252fe72fc3d259d44193a79402cdc34e3c37d62b77ce3fa66f3e
|
|
BLAKE2b-256 checksum How to use checksums |
0c618fc88321fbc2cf124ea3c7ca39e0a33d7166553f90c6c59ac057951232e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|