hatebucli

mcp
Guvenlik Denetimi
Uyari
Health Uyari
  • No license — Repository has no license file
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Community trust — 10 GitHub stars
Code Gecti
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
  • Permissions — No dangerous permissions requested

Bu listing icin henuz AI raporu yok.

SUMMARY

Fast, script-friendly CLI for Hatena Bookmark

README.md

@yuiseki/hatebucli

Your Hatena Bookmark history, on your machine and answerable.

Hatena Bookmark gives you a feed of what you bookmarked, one day at a time, and
not much else. hatebu syncs those days into a local cache and then answers
questions over the whole of it: what you were reading in 2019, which sites you
keep coming back to, when a subject first showed up, whether you have already
bookmarked the page in front of you. Everything except today is answered from
the cache, so it works offline and it stays fast on an archive of hundreds of
thousands of bookmarks.

It also runs as an MCP server, so an assistant can ask the same
questions without you translating each one into flags.

Install

Not on npm yet. Install it from a checkout:

git clone https://github.com/yuiseki/hatebucli.git
cd hatebucli
npm install && npm run build
npm link            # puts `hatebu` on your PATH

Needs Node 22.12 or newer. That is where require() of an ES module landed,
which some of the dependencies now need; Node 20 and earlier went end of life
on 2026-04-30. Run it on an older one and it says so rather than failing from
inside node_modules.

Getting started

hatebu config set username <your_hatena_id>
hatebu sync --days 30
hatebu words

sync is what fills the cache, one request per day, spaced out because the
feed is somebody else's server. Nothing else here fetches anything, apart from
today.

Commands

Every command that reports bookmarks or rankings takes -j, --json.

One day

hatebu list                      # today, fresh from the feed
hatebu list --date 2026-02-18    # an earlier day, from the cache
Option
-d, --date <yyyy-mm-dd> the day. Defaults to today
-j, --json

Search

hatebu search 地図                          # every cached day, best match first
hatebu search zenn.dev --field url
hatebu search 地図 --date 2026-02-18 -l 50

A query is one or more terms separated by whitespace, and every term has to
appear as a substring. Japanese needs no special case: it does not put spaces
between words, so 地図 finds 地図帳 and 白地図 on its own. 地図 AI is two
terms and needs both. Results are ordered by score, then newest first, and the
last line says how many matched when the limit cut them.

To count what came from a site, use hatebu lookup instead: it matches the
hostname, where a URL search matches the text of the URL anywhere in it.

Option
-f, --field <all|title|url> where to match. Default all
-d, --date <yyyy-mm-dd> one day only
-l, --limit <number> default 10
-j, --json

Rankings over a range

hatebu domains --date 2026       # the sites of a year
hatebu tags --date 2026-02       # the tags of a month
hatebu words                     # what last week was about
hatebu words --today
hatebu stats --days 30 --top 20  # a window as one summary

domains, tags and words take the same range options. With none of them
the range is the week ending yesterday.

Option
--date <yyyy|yyyy-mm|yyyy-mm-dd> a day, a month or a year
--today today only. Cannot be combined with --date
-l, --limit <number> rows. Default 10, capped at 10 for domains and tags, 30 for words
--by <distinctive|count> words only. Default distinctive
-j, --json

words runs the titles through a Japanese morphological analyser and ranks by
how much more the range used each word than the archive usually does, so the
answer is what the stretch was about rather than which words it contained. A
word counts once per bookmark however often the title repeats it.

hatebu words --rebuild-background   # once, about a minute
hatebu words --date 2013

The background is one file of word counts over the whole archive, about 1MB.
Building it tokenizes every title, which takes a minute, so it is a command you
run rather than something that happens during sync; a day of new bookmarks
moves a background of 374,000 by nothing. Without it, words falls back to raw
counts and says so. --by count asks for those on purpose.

Ranking this way is what makes a year legible:

2013   js node grunt javascript fluentd css backbone sass angularjs chef
2019   nhk ニュース 朝日新聞 ライブドアニュース 放火 yahoo pay 香港 文春

Frequency alone answers ai github for and the https for every year of this
archive. See ADR 010.

stats puts a window together: how much, at what hours, on what weekdays, and
the sites and tags that led it. Markdown to read, --json to compute from; the
two are cut the same way, so a bucket nobody bookmarked in is absent from both
rather than reported as a zero.

Option
--date <yyyy|yyyy-mm|yyyy-mm-dd> the day the window ends on. Defaults to yesterday
--days <number> window length. Default 7
--top <number> rows per section. Default 10, capped at 20
-j, --json

Each of these reports the days of the range the cache does not hold, so a low
count can be told apart from a gap in the sync.

The whole archive

hatebu lookup https://example.com/article   # have I bookmarked this page?
hatebu lookup example.com                   # what have I read from this site?
hatebu timeline --query maplibre --bars     # when was I reading about this?
hatebu timeline --by month --tag 地図
hatebu tagged 地図                           # what did I file under this tag?
hatebu random -n 5 --from 2010-01-01        # dig something out of the archive

lookup answers about one page when given a full URL, and about a site when
given a bare hostname. A subdomain counts as part of the site, and the scheme
and a trailing slash are ignored, because neither makes it a different page. A
URL that was never bookmarked still reports how much came from the same site.

timeline counts per year or per month, oldest first, and takes --tag,
--domain, --query, --from and --to in any combination. --bars draws
each row. random takes the same filters.

These read every cached day rather than an index. Over an archive of a few
hundred thousand bookmarks that is under a second.

Choosing a best of the day

hatebu pick                        # yesterday's bookmarks, numbered
hatebu pick 12                     # the entry page for number 12
hatebu pick --weekly               # the week's daily_best
hatebu pick --monthly --date 2026-09

pick writes nothing to Hatena. It lays out the candidates and gives you the
entry page for the one you chose; you add the daily_best tag there, the way
you would add any tag. The tag comes back into the cache on the next sync,
which is what makes the weekly round able to read what the daily round chose:

round candidates tag to add
pick the day's bookmarks daily_best
pick --weekly daily_best of the last seven days weekly_best
pick --monthly weekly_best of the month monthly_best

pick fetches the days of its window first, unless the cache already shows the
round as decided, because a tag added on the entry page is invisible until that
day is fetched again. A decided round costs no requests; an undecided week
costs its seven days. --no-sync reads the cache as it stands.

A round that is already decided shows what was chosen and stops, rather than
asking you to read the day again. A day may have more than one best; they are
all shown, and all of them go forward as candidates for the weekly round. --all lists the candidates anyway, with the
chosen one marked, and a number still resolves against that list if you change
your mind. See ADR 011 for why this does not
go through the write API.

Filling the cache

hatebu sync                       # yesterday
hatebu sync --days 30             # yesterday back thirty days
hatebu sync --date 2019-07-04     # one day
hatebu import ./old-bookmarks     # a legacy <dir>/YYYY/MM/*.json tree

sync never fetches today by default: today is still changing, and a cached
copy of a half-finished day would be wrong from the moment it was written.
--days and --date cannot be combined.

Configuration

hatebu config set username <your_hatena_id>
hatebu config get username

Where things live

Bookmarks ${XDG_CACHE_HOME:-~/.cache}/hatebucli/YYYY/MM/DD.json
Username ~/.config/hatebu/credentials.json
HATENA_USER the user ID, and it wins over the stored one
HATENA_BOOKMARK_RSS_URL override the feed URL. %s is the user ID

The cache is plain JSON per day and safe to copy, diff or back up. Earlier
versions also kept a search index under hatebucli/index; nothing writes there
any more and it can be deleted (see
ADR 005).

What the archive can and cannot tell you

  • A day that was never synced is not a day with nothing in it, and the
    commands say which is which rather than reporting a zero.
  • Tags come from the feed as it stood on the day. A stretch from before you
    started tagging has no tags, which is a gap in the data and not a gap in your
    reading; words is the tool for those years.
  • Comments are only present where you left one, which for most people is
    almost nowhere.
  • Re-syncing an old day is safe and sometimes an improvement: a legacy import
    can carry a URL where the title should be, and the feed still has the title.

MCP server

hatebu --mcp-server runs the CLI as a Model Context Protocol server over
stdio, so an MCP client can read your bookmarks. --mcp, mcp-server and
mcp start the same thing.

hatebu --mcp-server

It needs a username before it starts, from hatebu config set username or from
HATENA_USER in the client's environment. stdout carries only JSON-RPC;
anything meant for a human goes to stderr.

Configured in a client:

{
  "mcpServers": {
    "hatebu": {
      "command": "node",
      "args": ["/path/to/hatebucli/bin/hatebu.js", "--mcp-server"],
      "env": { "HATENA_USER": "your_username" }
    }
  }
}

Once this is on npm the command becomes
npx -y @yuiseki/hatebucli --mcp-server.

Everything except today is answered from the local cache, so run hatebu sync
before pointing a client at it.

Tools

Ten, all read-only. Nothing writes to your account, and there is no sync tool:
fetching is a decision about somebody else's server, so it stays a command you
run.

Tool Arguments
hatebu_search query (required, up to 200 characters), field, date, limit (default 10, max 100). Returns match_count and returned_count
hatebu_list date (defaults to today)
hatebu_domains date, today, limit
hatebu_tags date, today, limit
hatebu_words date, today, by, limit (default 30)
hatebu_stats date, days, top
hatebu_lookup url_or_domain (required), limit
hatebu_timeline by, tag, domain, query, from, to
hatebu_tagged tag (required), limit
hatebu_random count, tag, domain, query, from, to

They answer the same numbers as the matching command, down to the defaults, and
a test holds them to it. What differs is what a model needs and a terminal does
not:

  • Every ranking returns missing_dates alongside the numbers, so a model can
    tell a quiet week from an unsynced one.
  • hatebu_list says whether the day was in the cache at all.
  • hatebu_stats returns the Markdown and the JSON as two blocks, so a model
    gets both without a second call. The JSON is byte for byte what
    stats --json prints.
  • hatebu_search reports match_count as well as returned_count, so a
    question about how many were bookmarked is not answered by counting rows the
    limit allowed through, --json
    returns, since a title is matched per character and they are single letters;
    matchedIn stays, because which field matched is worth knowing.

A bad argument comes back as an error on that call. The server keeps answering.

Development

npm install
npm run build
npm test

CI runs the same on Node 22, 24 and 26. npm install wants npm 11 or newer:
npm 10.9.3 cannot resolve this dependency set and dies in arborist with
Cannot read properties of null (reading 'edgesOut'). npm ci against the
committed lockfile is fine on either.

The tests spawn the built CLI and assert on its output, and the MCP tests speak
JSON-RPC to it over a pipe, so they cover the framing too. Nothing in them
touches your real cache or reaches Hatena: the fetching paths run against a
stub feed served from a second process.

To unlink a local build again:

npm unlink -g @yuiseki/hatebucli

The version in package.json is the only way to tell two builds apart, so
bump it whenever a copy goes anywhere: a global install, a directory a service
runs from, a tarball. Three copies all claiming the same version is how you end
up debugging the wrong one.

The decisions behind the shape of this thing are in docs/ADR,
including why search scans instead of keeping an index and why the MCP server
is the CLI rather than a second program.

Yorumlar (0)

Sonuc bulunamadi