TreeMap
무료 오픈소스 디스크 사용량 시각화 도구로, 사각형 트리맵·중복 파일 탐색·디스크 가득 예측·정리 시 파일을 휴지통으로만 이동한다
작은 일들을 챙기고 적절한 타이밍을 알려주는 개인 비서, Mac에서 실행되고 휴대폰으로 메시지 하나에 연결됩니다
A personal assistant that keeps track of the little things and knows when to let you be. It runs on your Mac and lives a message away on your phone.
생산성TypeScript · HTMLGitHub @eduardosilveiradev온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기A proactive personal assistant on Telegram or iPhone, built as a Claude Code channel plugin. Reminders, mail and calendar, never sends without a yes.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A proactive personal assistant that lives in your Telegram.
It doesn't wait for you to ask. It reminds you at the right moment, tells you when an email that matters lands, and stays quiet the rest of the time — which is the hard part.
you → remind me thursday 9am to pay the rent
otto → 👍
(thursday, 9am)
otto → rent is due today.
otto → a power bill came in, due friday. $214.
You don't install anything. You paste a piece of text into Cowork and it installs itself.
Open Cowork and paste this:
<task>
Install Otto for me, from scratch. I don't know my way around a terminal — you
do everything.
</task>
<step_1_download>
git clone --depth 1 https://github.com/eduardosilveiradev/otto.git ~/.otto
</step_1_download>
<step_2_follow_the_guide>
Read all of ~/.otto/otto/skills/setup/SKILL.md before you start and follow it to
the letter, beginning to end, without skipping a step. That file is the
installer — this text only points you at it.
</step_2_follow_the_guide>
<how_to_talk_to_me>
One question at a time: wait for my answer before the next one, and confirm what
you understood. No jargon and don't ask me to run a command — you're the one who
runs commands.
</how_to_talk_to_me>
<step_3_at_the_end>
Add ~/.otto/.claude-plugin as a marketplace and install the otto plugin — that's
what gives me the /otto:setup and /otto:access commands.
</step_3_at_the_end>
<how_i_know_it_worked>
Don't tell me it works by inference. Only after you've seen, with your own eyes:
- the service active (bash ~/.otto/scripts/otto-ctl.sh status)
- the "polling as @yourbotname" line in the log
- the bot answering a real message I sent
If any step fails, say which one failed and what it costs me in practice.
</how_i_know_it_worked>
From there it's a conversation: it asks your name, your timezone, what you do, walks you through creating the Telegram bot, and installs the rest on its own. You just answer questions and type your computer password once — that's what lets Otto speak without being asked.
It takes about five minutes, most of it waiting on your answers.
| Linux or macOS | Ubuntu, Fedora, Arch, Alpine, macOS 12+ — the installer picks the right way to keep it running |
| Claude Code | installed and logged in |
| A Telegram bot | free, takes 1 minute — setup walks you through it |
| Google account | optional, only if you want the email watch |
| Keeps it alive | Survives logout | Log | |
|---|---|---|---|
| Linux with systemd | otto.service, a user service | yes (linger) | journalctl --user -u otto.service |
| macOS | com.otto.agent, a LaunchAgent | no — comes back when you log in | ~/.otto/state/otto.log |
| Linux without systemd | background process + @reboot cron | no | ~/.otto/state/otto.log |
On macOS Otto lives inside your login session: it pauses while the Mac sleeps
and stops if you log out, then picks itself back up. If you want it awake
overnight, stop the Mac from sleeping (System Settings → Lock Screen / Energy,
or leave caffeinate -s running).
Optional. /otto:setup pairs it if you say yes; this is what it does, so you
know what is exposed:
webhook in ~/.otto/state/access.json,
with a random token and host left at 127.0.0.1. It answers only on the
computer itself.tailscale serve --bg --set-path /app http://127.0.0.1:4589/app. That is
serve, not funnel: only your own Tailscale devices can reach it, never the
internet.bun otto/apptokens.ts new <phone>. Only a hash
is kept on disk. bun otto/apptokens.ts list / revoke <id> — a revoked
phone is disconnected within seconds.In the app you enter the https address tailscale serve status prints, and the
token.
One thing worth knowing: the listener also has a /status endpoint that Otto's
own tool-call hook posts to, so the app can show what Otto is doing. It accepts
requests only from the computer itself, carrying the Claude Code process id —
that is its whole protection, and it can do nothing but show a status line in
the app. The tailscale serve command above does not publish it.
If you build the app yourself, its release script writes the build to
~/.otto/state/app-release/. The app then asks /app/update (with its token)
whether a newer build exists, and SideStore fetches the build, its icon and its
source listing from /app/release/<key>/…. SideStore can't send a token, so those
paths are guarded by the random key in app-release/key instead. With no
app-release/ directory they answer 404.
A sideloaded app on a free Apple ID gets no push notifications, and while it is
closed it has no connection to post its own. If you install
Bark and put its device key in
~/.otto/state/.env as BARK_KEY=…, a message for the app that finds it closed
also goes to Bark's server (api.day.app), which pushes it to your phone;
tapping the banner opens the Otto app. Add BARK_ICON= with an image URL to
show your own icon on the banner. Otto also pushes through Bark, even with the
app open or on Telegram, when a message must not be missed: a reminder you set,
a one-time code, a security alert. That sends the message text, cut to 1000
characters, to Bark. Leave the key out and nothing leaves the computer.
Optional, and only for a browser on one of your own Tailscale devices. Otto
answers a dashboard web app under /app/dash/*, on the same listener and the
same tailscale serve path as the iPhone app, so nothing new is published.
otto/dash.ts is the API and
otto/dash-types.ts its wire contract; every answer is an
explicit pick of the stored record, so a new field never leaks by accident.
bun otto/dash.ts allow https://<dashboard host>
(deny and list too). Any other origin gets a 403 with no CORS headers.tailscale serve status prints. It
shows a pairing code; send pair <code> to Otto on Telegram or in the app,
check what is asking (origin, read or read + control, browser) and tap
approve. The token it mints is bound to that origin, works only under
/app/dash/*, and expires; bun otto/apptokens.ts list / revoke <id>.The Host header must be this machine's ts.net name (or a private LAN address),
which stops DNS rebinding. Sending a chat message from the dashboard needs a
read + control token. The calls pages answer 501 here: there is no phone
relay in this repo. bun otto/dash-dev.ts runs the same API on a port of its
own against your real state, with throwaway tokens, for working on a dashboard.
Two things run on their own, on the Claude plan this computer is logged into:
Both are on by default. They use the same usage limits as everything else you do with Claude Code.
Otto can draft and install changes to its own recipes, skills and code, but that
ships off. To turn it on, run /otto:access self-edit on from your terminal
(it sets "selfEdit": true in access.json). The rules it then follows — and
that the server enforces — are in SELFEDIT.md.
/settings (or asking Otto for its settings) sends one message of on/off
buttons: email scans, quiet hours, voice replies, holding nudges during
meetings, every recurring trigger, and the MCP servers Claude Code knows for
Otto's project. Otto handles the tap itself and edits the message in place.
Each switch writes one named field. The allowlist, pairing and secrets are never
on the page. A connector switch edits that project's disabledMcpServers in
~/.claude.json (the same list /mcp uses) and takes effect from the next
session or an /mcp reconnect. Otto's own server is never listed.
Every message you send is written to ~/.otto/state/inbound-journal.json
before Otto sees it, and crossed off once Otto replies or reacts in that chat.
If Otto restarts before answering, it is handed the message again on the next
start, at most once. Only messages younger than redeliverHours in
access.json are replayed (default 6; 0 turns it off).
Off by default. Normally a message sent while Otto's machine is asleep waits at
Telegram, and you hear nothing. The relay is a Cloudflare Worker that takes
Telegram's webhook instead, queues what Otto can't take, tells you once per
outage, and hands the queue over when Otto is back ("telegramMode": "relay" in
access.json). Deploy, enable and disable steps are in
relay/README.md. Its tests run with cd relay && bun test.
A single process (otto/server.ts) running three things at once:
Around those: memory and open loops, a rolling brief of your day attached to
every scheduled event, a hold queue for quiet hours, the iPhone app transport,
and a watchdog (below). Each lives in its own module under otto/, with tests in
otto/tests/ (cd otto && bun test). Claude Code starts the server through
otto/supervisor.ts, which holds the connection so the reload tool can restart
it on new code without a reconnect.
To see everything Otto remembers at once, bun otto/memviewer.ts --open writes
one self-contained page, memory-viewer.html, to ~/.otto/state/: both memory
stores with search and filters, open loops by due date, never-recalled and stale
badges, and a summary at the top from a headless Sonnet run with no tools. The
summary is cached in memory-summary.json against a hash of what it was given,
so reopening is free; --refresh forces a new one. It only reads the stores.
The page holds personal data, which is why it lives in the state folder and
never in the repo.
The page's Graph tab lays memories out by meaning. Each memory is embedded
locally by otto/memembed.py with
multilingual-e5-small,
so the text never leaves your machine; vectors are cached in
memory-embeddings.json under a hash of the exact text, and a rerun embeds only
what changed. They are clustered with k-means, and each cluster is named once by
Sonnet and cached in memory-clusters.json (--no-labels uses keyword terms
instead, --recluster picks the number of clusters again). The graph draws
[[links]], similar memories, and close neighbours across clusters. A search
clears the stage down to its matches, set around the query in the middle, each
one closer the better it matches and tied to it by a faint spoke; emptying the
box or pressing Escape puts the full graph back exactly where it was. Its graph
libraries load from jsDelivr at pinned versions; offline, the list still works.
--open serves the page on 127.0.0.1 and opens it there, which adds search by
meaning; --serve does the same without opening a browser. Opened straight from
the state folder as a file, search falls back to keywords.
Embedding needs a Python with torch and transformers, taken from
$OTTO_EMBED_PYTHON or ~/.otto/state/embed/.venv. One-time setup:
uv venv --python 3.12 ~/.otto/state/embed/.venv && uv pip install --python ~/.otto/state/embed/.venv/bin/python torch transformers.
The model (about 470 MB) downloads on first use. Without it the graph still
draws, with explicit links only.
The personality — who you are, your timezone, what matters to you, the tone —
lives in a CLAUDE.md file that setup writes from
templates/CLAUDE.md.template. It's plain text: open it and edit it
whenever you want to change how it behaves.
/otto:setup | install or reconfigure from scratch |
/otto:access | change quiet hours, cadence, who has access, the app, self-editing |
/otto:status | is every part of Otto actually up? |
/otto:recipes | ready-made automations: weekly review, bill reminders, follow-ups… |
bash scripts/otto-ctl.sh status | is it up? |
bash scripts/otto-ctl.sh logs | see what it's doing |
bash scripts/otto-ctl.sh restart | restart it |
bash scripts/install.sh --check | diagnostics, changes nothing |
otto-ctl.sh speaks systemd, launchd or plain process depending on where you
are — so the same command works everywhere. Underneath it's
systemctl --user … otto.service on Linux and
launchctl … gui/$(id -u)/com.otto.agent on macOS.
The watchdog (otto/watchdog.ts) probes Otto's moving parts
every 15 minutes and, when one fails, may run only the repairs below. Each
attempt is written to capability-log.json with what it did and what undoing it
would take. It never edits config, secrets or access.json; anything else is
reported, not fixed.
| part | repair |
|---|---|
| voice daemon | pkill -f voiced.py (only if you installed one; it respawns on the next spoken reply) |
| webhook listener | lsof -ti tcp:<port> -sTCP:LISTEN | xargs -r kill — frees the loopback port from a dead holder |
| Telegram poller, the session itself | bash ~/.otto/scripts/otto-ctl.sh restart |
| gog auth | none unattended — gog auth login needs a browser, so it is reported |
| email scan | none — reported only |
Backups. At 05:30 otto/backup.ts packs ~/.otto/state
(no logs, nothing over 20 MB) and the durable memory markdown
(~/.claude/memory/global, every ~/.claude/projects/*/memory) into one tar.gz
with a sha256 manifest, encrypts it with AES-256-GCM, and keeps the newest
backup.keep (default 14) in backup.dir (default ~/.otto/backups), both in
access.json. It then runs a restore drill on the archive it just wrote:
decrypt into a temp dir, check every hash and that every .json parses, delete
the temp dir. otto_status shows the last night; you hear about it only after
three failed nights in a row. By hand:
bun otto/backup.ts run | list | verify <file> | restore <file> <dest>;
restore only extracts into an empty dest away from the live state. The key
is created 0600 at ~/.otto/state/backup.key on the first run: copy it
somewhere off the machine, because without it every archive is unreadable.
Domain and TLS expiry. List hostnames under domains in access.json.
Once a day otto/domains.ts reads each certificate's expiry
off a TLS handshake and the registration expiry from RDAP (rdap.org). One
warning at 30 days, one at 7 and one on expiry, per domain per expiry date; a
failed lookup is retried the next day and shown only in otto_status.
Upgrading from 1.x: git -C ~/.otto pull, then
bash ~/.otto/scripts/install.sh --service-only. On its first start Otto moves
its state from ~/.claude/channels/otto to ~/.otto/state and leaves a symlink
behind; nothing in it is rewritten.
Going back to 1.x (tagged v1.0.0):
bash ~/.otto/scripts/otto-ctl.sh stop
git -C ~/.otto checkout v1.0.0
bash ~/.otto/scripts/install.sh --service-only
1.x reads ~/.claude/channels/otto, which still points at your state, so
nothing needs moving. If you also want the state back where it was: with Otto
stopped, rm ~/.claude/channels/otto and
mv ~/.otto/state ~/.claude/channels/otto. Do that before running 1.x, whose
.gitignore does not know about state/ — so never git add -A in ~/.otto.
~/.otto/state/, with 600 permissions. That folder sits inside the clone
but is in .gitignore, so nothing personal goes to the repository. Never run
git clean -fdx in ~/.otto — -x deletes ignored files, which is all of
Otto's state.~/.claude/channels/otto: the
first start moves it to ~/.otto/state and leaves a symlink behind. To undo,
stop Otto, rm ~/.claude/channels/otto and
mv ~/.otto/state ~/.claude/channels/otto.127.0.0.1 and reaches your phone only through
tailscale serve. Leave host alone; never use tailscale funnel for it.
The one exception is the optional relay, which funnels the /telegram path
alone, and that path refuses anything without the relay's secret.| Symptom | Cause | Fix |
|---|---|---|
| replies, but never starts a conversation | channel not authorized | sudo bash scripts/allow-channel-plugin.sh |
| doesn't reply at all | service down | bash scripts/otto-ctl.sh restart |
409 Conflict in the log | two Ottos on the same bot | one device per Telegram bot only |
| dies when you log out (Linux) | linger off | sudo loginctl enable-linger $USER |
| quiet overnight (macOS) | the Mac went to sleep | keep it awake, or accept the gap |
| gone after a reboot (no systemd) | @reboot entry missing | crontab -l, add it back |
| ignores your messages | wrong ID | /otto:access list and check with @userinfobot |
MIT.