setup/lib/windowed-runner.ts was the one place on main still printing
elapsed time as raw seconds (`(170s)`) instead of using the
minute-aware `fmtDuration` helper from #2108. Two spots — the live
spinner suffix that ticks during the build, and the
success/error completion suffix — both now go through `fmtDuration`,
so anything past 60 seconds renders as `Xm Ys` (e.g. `2m 50s`) like
the rest of the setup flow.
The miss happened because a separate PR (closed) was supposed to
remove the timer entirely from this file, so #2108 deliberately
skipped it. With that other PR closed, applying `fmtDuration` here
is the consistent fix.
Pure formatting change. The helper itself is unchanged from #2108;
behavior under 60s is identical (`Xs`); behavior past 60s now
matches everywhere else.
Step 1 of the Telegram channel's BotFather instructions used to read:
1. Open Telegram and message @BotFather
Two small UX issues with that:
- "BotFather" reads slightly sketchy without context — a first-time
user has no way to know it's the official, sanctioned account
rather than an impersonator.
- Typing the username from memory leaves room for picking a typo'd
impostor account (Telegram has many @BotF4ther / @BotFAther / etc.
look-alikes).
Update the line so the official-bot framing is part of the instruction
itself:
1. Open Telegram and message @BotFather — Telegram's official bot
for creating and managing bots
One-line change in the existing note() body. No new dependencies, no
asset churn, no other behavior change.
The OneCLI installer (curl onecli.sh/install | sh) doesn't pass
--remove-orphans to docker compose up. After the upstream service rename
(app -> onecli), the legacy onecli-app-1 container keeps :10254 bound
and crashes the new bring-up. This breaks /migrate-v2.sh on any host
that has a pre-rename OneCLI installed.
Workaround: before invoking the installer, remove containers in the
"onecli" compose project whose service name isn't in the v2 set
({onecli, postgres}). Label-keyed and no-op on fresh installs.
Filed upstream; remove this once the installer adds --remove-orphans.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous approach deleted the v1 unit file and symlinked it to v2,
making rollback impossible. Now we just disable v1 and leave the file
on disk so users can switch back with a single command.
Also adds rollback instructions to the migration summary output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After migration keeps v2, the old unslugged `nanoclaw.service` (or
`com.nanoclaw.plist`) was only disabled — the unit file stayed on disk.
A `systemctl --user restart nanoclaw` would start v1 instead of v2.
Now the migration removes the old file and symlinks it to the v2 unit,
so the legacy name transparently starts v2. Handles systemd (Linux/WSL)
and launchd (macOS). Idempotent — skips if the symlink already exists.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The migration script has interactive prompts and streams progress
output that gets collapsed when run via Claude Code's Bash tool.
Add a TTY guard that exits early with instructions to use the !
prefix instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The credential proxy already reads ANTHROPIC_AUTH_TOKEN (credential-proxy.ts
line 33) and uses it for OAuth-mode authentication, but setup/verify.ts did
not include it in its credential-detection regex. Users with
ANTHROPIC_AUTH_TOKEN in .env saw 'CREDENTIALS: missing' even though their
credentials were valid at runtime.
Add ANTHROPIC_AUTH_TOKEN to the regex and add a matching test case.
Closes gh-853
Container typecheck and bun install gracefully skip when bun isn't
installed on the host. Linux service restart now detects the actual
systemd service name instead of hardcoding 'nanoclaw'.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The skill was written for v1 and missed several v2 changes: container
rebuild after merge, dependency install for both pnpm and bun lockfiles,
container typecheck, channel/provider branch update awareness, and
platform-aware service restart instructions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pre-message printed by setup/register-claude-token.sh used to
say "A browser window will open for you to sign in with your
Claude account." Accurate on a laptop or desktop, but a lie on
headless devices (Pi, SSH'd-into Linux server, CI) where the
browser auto-open never lands and the user actually has to copy
the URL `claude setup-token` prints to another device.
Add a small bash isHeadless check (mirrors `isHeadless()` in
setup/platform.ts: Linux without DISPLAY / WAYLAND_DISPLAY) and
swap the heredoc accordingly:
- Headless: "A sign-in link will appear for you to sign in with
your Claude account. When you finish, we'll save the token
to your OneCLI vault automatically."
- GUI: existing "A browser window will open…" copy, unchanged.
The trailing "Press Enter to continue, or edit the command first."
line and the actual `claude setup-token` invocation are unchanged
— only the leading sentence flips.
/./ requires at least one character and silently drops messages with no
text (e.g. Telegram photo/video/file sent without a caption). Switching
to /[\s\S]*/ matches the empty string too, so media-only messages now
reach the router and then the agent.
The first-time setup picker only listed seven channels with bash
installers. Users wanting to install one of the other channels (matrix,
github, linear, webex, etc.) had no entry point from the picker and had
to know to run /add-<name> from Claude Code afterwards.
Add an "Other…" option that prompts for a free-text name, normalizes it
(accepts "matrix", "add-matrix", or "/add-matrix"), and prints a hint
telling the user to run /add-<name> from Claude Code after setup
finishes. The verify step's "What's left" panel already covers the
empty-channels case, so the user is not left thinking the channel was
wired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Skill files only — copied from PR #2192 (channels branch).
Source adapter (src/channels/deltachat.ts) lives on the channels
branch and is installed by the skill.
Co-Authored-By: Axel McLaren <scm@axml.uk>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If 1b-db is re-run after the v2 service has already started (e.g.
recovering from an earlier failure), the messaging_group it would
otherwise create may already exist — auto-created by the runtime router
on the first inbound message, with the router's default
unknown_sender_policy ('request_approval'), not the migration's intent
('public'). The previous reuse path skipped creation but never updated
the policy, so re-runs left the bot hanging every message waiting for
an approver that wasn't seeded yet.
When reusing an existing row that has zero wired agent_groups (signal
of a router auto-create), reset the policy to 'public'. Once any wiring
exists, the user has had a chance to tighten via the skill — leave it.
Also adds a CHANGELOG entry covering this and the two sibling fixes
(Discord DM resolution, symlink skip in copyTree).
fs.copyFileSync follows symlinks, so a single broken/dangling link in v1
(e.g. .claude-shared.md → /app/CLAUDE.md, a container-side path that
doesn't resolve on the host) crashed the alphabetical traversal with
ENOENT — preventing later folders, including the actual registered
group, from being copied.
Check entry.isSymbolicLink() and skip with a one-line log. v2 uses
composed CLAUDE.md fragments, so v1's container-path symlinks have no v2
meaning and don't need to be carried forward.
The resolver only enumerated guild channels, so any v1 install whose
registered Discord chat was a DM (a common case for personal-bot
installs) failed 1b-db with "not found in any guild" — leaving the
migration without an agent_group or wiring, and the user with a bot that
received messages but had nowhere to route them.
Add an unresolved-channel classification pass: for any v1 channel id not
found in a guild, GET /channels/<id> and emit discord:@me:<id> when the
type is DM (1) or GROUP_DM (3). Matches the runtime adapter's
guild_id || "@me" encoding. Other types / 404 / 403 keep current
skip-with-warning behavior.
Caller passes the v1 channel id list (already on hand). Test coverage
extends the existing mock-fetch pattern with DM, GROUP_DM, orphan, and
dedupe cases.
Trust the agent to figure out which failed steps actually stop
routing. The rule is the goal ("can the bot route one message?"),
not a hardcoded list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2b-channel-auth: copies the Baileys keystore + channel-specific env
keys. Without it WhatsApp can't connect — saw this firsthand when
the original candidatePaths bug left env_keys=0,files=0.
3c-auth: registers Anthropic credentials in OneCLI. 3b installs the
gateway; 3c puts the secret in the vault. Without 3c every agent
request 401s regardless of 3b's status.
1c-groups stays deferred — agent runs on stock CLAUDE.md without it,
but routing works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous version spelled out launchctl/systemctl commands, log lines
to grep for, diagnostic recipes — the agent reading this skill knows
all of that. Keep only the parts that aren't obvious from the rest of
the codebase: which steps are blocking vs deferred, the smoke-test
ordering, and the non-destructive framing for the user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0 used to be "triage every failed step before doing anything
else", which front-loaded a bunch of fixes for things that don't
actually block the user from proving v2 works. Restructure:
- 0a — fix blockers only (1b/1d/2c/2d/3a/3b/3e). Defer non-blockers
(1a, 1c, 1e, 2b, 3c) — most surface naturally in later phases.
- 0b — smoke test: switch v1 → v2, send a real message, verify the
routing chain in logs/nanoclaw.log. AskUserQuestion gates whether
to continue.
- Revert recipe (launchctl/systemctl) called out as always-available,
not destructive — v1 process, data, and credentials are untouched.
Up-front list of what the script handled now also mentions the
WhatsApp LID resolution and Baileys keystore copy, so users see
exactly what continuity they're getting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README: replace the one-line v1 migration note with a collapsed
<details> block. Quick Start stays compact for the common case (fresh
install) while v1 users get the actual instructions. Calls out
explicitly that the script must be run from a real terminal — not from
inside a Claude session — so the channel-select / switchover prompts
and the Node/pnpm/Docker bootstrap all work.
migrate-from-v1 skill: add a Preflight section that aborts if
logs/setup-migration/handoff.json is missing. Without this, invoking
the skill before the script just leads Claude to start guessing /
running shell commands. The new message redirects them to the script
and tells them it'll hand back to Claude on completion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1 stored every WhatsApp DM as `<phone>@s.whatsapp.net`. v2's WA
adapter sometimes resolves the chat to `<lid>@lid` instead — when
WhatsApp delivers via the LID protocol and Baileys hasn't yet learned
a LID→phone mapping for that contact (cold cache after migration).
The router then can't find the phone-keyed messaging_group and
silently drops the message at router.ts:184.
Baileys persists every LID↔phone pair it has ever learned to disk as
`store/auth/lid-mapping-<phone>.json` (forward) and
`lid-mapping-<lid>_reverse.json` (reverse). v1 will already have these
populated for every contact it has talked to. New step 2d-whatsapp-lids
parses the reverse files and writes paired LID-keyed `messaging_groups`
+ `messaging_group_agents` rows so both `<phone>@s.whatsapp.net` and
`<lid>@lid` route to the same agent_group with the same engage rules.
No Baileys boot, no WhatsApp connectivity required — pure filesystem
read of files we've already copied via 2b-channel-auth. Step is
no-op-on-skip if either store/auth or whatsapp DM rows are missing.
Anything that slips through (a contact whose LID v1 never learned)
falls back to the runtime approval flow once the WA adapter sets
isMention=true on DMs — each unknown LID DM auto-creates an
approval-required messaging_group and the owner gets a one-tap
register prompt.
Verified end-to-end on a 12-group v1 install: 3 DM rows aliased,
inbound DM routed via the LID-keyed row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1 didn't track is_group separately; db.ts hardcoded `is_group: 1` for
every messaging_group. v2 uses is_group=0 to collapse DM sub-thread
sessions and to drive routing decisions, so getting it wrong is latent
risk on otherwise-working installs.
New helper inferIsGroup(channelType, platformId) lives in shared.ts so
tasks.ts and any future migration step can reuse it. Inferred per
channel:
- whatsapp: `<id>@g.us` is a group, anything else is a DM
- telegram: negative chat IDs are groups, positive are DMs
- everything else: default to 1 (least surprising for chats v1 chose
to register, where DM auto-create paths weren't used)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
migrate-v2.sh
Replace `declare -A STEP_RESULTS` with two parallel indexed arrays
(STEP_NAMES + STEP_STATUSES) plus a `record_step` helper. macOS ships
bash 3.2 which has no associative arrays — `declare -A` errored out
silently and every `STEP_RESULTS["1a-env"]=...` triggered a fatal
bash arithmetic error (interpreting "1a" as a number). Visible
symptom: `steps: {}` in handoff.json. Latent symptom: phase 2c's
install loop sometimes bailed mid-iteration before invoking the
channel install script, leaving channel code uninstalled while
reporting `overall_status: success`.
migrate-v2-reset.sh
Cover the gaps that left install side-effects in place between
iterations:
- Remove untracked adapter files in src/channels/ (mirror the
pattern already used for container/skills/).
- Restore tracked setup helpers that channel installs overwrite
(setup/whatsapp-auth.ts, setup/pair-telegram.ts, setup/index.ts)
and remove untracked ones they create (setup/groups.ts).
- Restore package.json + pnpm-lock.yaml (channel installs add
deps like @whiskeysockets/baileys).
Setup/migrate-v2/* is intentionally not touched — that's where user
WIP lives.
Verified end-to-end: reset → migrate → all 9 steps reported in
handoff.json with status "success", phase 2c install actually runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>