When pasting an invalid token, the old value stayed in the input
field. Pasting a new token appended to the old one instead of
replacing it, causing repeated validation failures.
Add clearOnError: true to all 8 password prompts across setup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When re-running setup on a machine that already has a .env with
channel tokens or OneCLI config, detect them early and offer to
reuse instead of prompting the user to paste everything again.
- Add detectExistingEnv() to parse .env and group known keys
- Add detectExistingDisplayName() to read display name from v2.db
- Defer display name prompt until actually needed (cli-agent or channel)
- Skip cli-agent and first-chat when groups are already wired
- Add token reuse checks to Telegram, Discord, Slack, Teams, iMessage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clack's `p.note` defaults to `format: e => styleText("dim", e)`, which
fades note bodies regardless of the project's stated readability stance
(see comment on `dimWrap` in setup/lib/theme.ts: "prose renders at the
terminal's regular weight"). The dim styling makes body copy hard to
read on dark terminals and visibly washes out brand-colored segments
embedded in cards (e.g. the chip + bold heading rows).
Add a `note()` helper in setup/lib/theme.ts that wraps `p.note` with a
pass-through formatter, and route every setup-flow `p.note` call site
through it: setup/auto.ts, every setup/channels/*.ts adapter, and the
two setup/lib/claude-* helpers.
Pre-styled segments (brandBold, brandChip, formatPairingCard,
formatCodeCard) now render at full strength instead of being faded
alongside surrounding prose.
Slack interactive buttons (channel approval cards) require Interactivity
to be enabled in the app settings. Without it, button clicks silently
fail to reach the host. Added the step to both the setup wizard
post-install checklist and the add-slack SKILL.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slack setup previously stopped after installing the adapter, leaving
users to manually discover /init-first-agent. When they DM'd the bot,
the channel-approval flow silently failed because no owner existed.
Now the Slack setup flow matches Discord/Telegram:
- Collects the operator's Slack member ID
- Opens a DM channel via conversations.open (requires im:write scope)
- Runs init-first-agent to establish ownership, wiring, and welcome DM
- Updates post-install note to focus on webhook URL (the only remaining step)
The welcome DM is delivered via chat.postMessage (outbound), which works
before Event Subscriptions are configured. The user sees the greeting
immediately; inbound replies require webhooks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`bash nanoclaw.sh` can now offer Signal as a channel choice, scan the
signal-cli link QR in the terminal, and wire up the first agent end to
end — mirroring the WhatsApp and Telegram flows.
Pieces:
- setup/add-signal.sh — non-interactive installer. Fetches
src/channels/signal.ts + signal.test.ts from the channels branch,
appends the self-registration import, installs qrcode (for the
setup-flow QR render), and builds. Idempotent and standalone-runnable.
- setup/signal-auth.ts — step runner. Spawns `signal-cli link --name
NanoClaw`, watches stdout for the `sgnl://linkdevice?…` (or legacy
`tsdevice://`) URL, emits SIGNAL_AUTH_QR with it. On exit 0, runs
`signal-cli -o json listAccounts` and reports the new account via
SIGNAL_AUTH STATUS=success. Pre-check via listAccounts returns
STATUS=skipped if an account is already linked.
- setup/channels/signal.ts — interactive driver. Probes for signal-cli
(offering `brew install signal-cli` on macOS or linking GitHub
releases on Linux if missing), runs add-signal.sh, renders each
SIGNAL_AUTH_QR block as a terminal QR inside a clack spinner,
persists SIGNAL_ACCOUNT to .env + data/env/env, restarts the
service, then wires the first agent via init-first-agent.
- setup/index.ts: register `signal-auth` in the STEPS map.
- setup/auto.ts: add 'signal' to ChannelChoice, import the driver,
add it to the channel picker (after WhatsApp, hint "needs signal-cli
installed"), branch the dispatch, and map channelDmLabel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slack: interactive driver walks through app creation, validates the
bot token via auth.test, installs the adapter, and prints a
post-install checklist for the webhook URL + Event Subscriptions
config. No welcome DM since Slack needs a public URL before inbound
events work — the driver's own "finish in Slack" note replaces the
outro "check your DMs" banner.
iMessage: picks local (macOS) vs remote (Photon) mode. Local mode
opens the node binary's directory in Finder so the user can drag it
into Full Disk Access. Remote mode prompts for Photon URL + API key.
Asks for the operator's phone/email, then wires the first agent
including a welcome iMessage.
Both marked "(experimental)" in the askChannelChoice picker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Container step: duration hint + 3-line rolling output window with
60s stall detector that offers "keep waiting" vs "ask Claude"
- First chat: reframed as a try-out with sandbox-model explainer
(wakes on message, sleeps when idle, context persists)
- Timezone: auto-detected non-UTC zones now get an explicit
confirm from the user instead of silent persist
- Outro: added always-on warning + prominent "check your DM" banner
when a channel was configured; directive last line
- Discord: always show token-location reminder even when user says
they have one; new "do you have a server?" branch walks through
server creation if not
- All select prompts: custom brightSelect renderer keeps inactive
option labels at full brightness (was dim gray); adds @clack/core
as a direct dep
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two NanoClaw installs on the same host used to fight over the shared `com.nanoclaw` launchd label / `nanoclaw.service` systemd unit and the `nanoclaw-agent:latest` docker tag — the second install silently rewrote the service pointer and rebuilt the image out from under the first. Introduces a deterministic per-checkout slug (sha1(projectRoot)[:8]) and namespaces everything off it:
- Service: `com.nanoclaw-v2-<slug>` / `nanoclaw-v2-<slug>.service`
- Image: `nanoclaw-agent-v2-<slug>:latest` (base), `nanoclaw-agent-v2-<slug>:<agentGroupId>` (per-group)
New shared helpers: src/install-slug.ts (host) + setup/lib/install-slug.sh (bash). Both compute the same slug so verify/probe/add-*.sh/build.sh/container-runner all agree. Any v1 `com.nanoclaw` service left on the host stays untouched and can coexist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Post-install was a bare instruction block: "DM the bot, run
/manage-channels". Replace it with an explicit Done/Stuck-style
select backed by the handoff mechanism — Claude takes over, tails
logs/nanoclaw.log for the inbound, inspects data/v2.db for the
auto-created messaging_group row, runs scripts/init-first-agent.ts
with the discovered platform_id + AAD user id, and verifies end-to-end.
Operators who want to drive it themselves pick "I'll do it myself"
and get the same terse instructions as before. Default is the
handoff (recommended hint).
Why Teams and not the other channels: Telegram/Discord/WhatsApp
already have synchronous platform IDs we capture during setup, so
init-first-agent runs inline. Teams platform IDs only exist after
the first real inbound, so the wiring is necessarily deferred — and
that deferred work is exactly what the handoff handles best.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Teams is the most complex channel NanoClaw supports — no "paste a
token" shortcut exists. Operators walk through ~6 Azure portal steps
(app registration, client secret, Azure Bot resource, messaging
endpoint, Teams channel, manifest sideload). The driver makes each
step as guided as possible and gives the operator an explicit
escape to interactive Claude whenever they get stuck.
Handoff mechanism (reusable across channels):
- setup/lib/claude-handoff.ts: offerClaudeHandoff(ctx) spawns
`claude --append-system-prompt <context> --permission-mode acceptEdits`
with stdio: 'inherit', returns when Claude exits so the driver can
re-offer the same step. Context captures channel, current step,
completed steps, collected values (secrets redacted), and file refs.
- validateWithHelpEscape / isHelpEscape: wrap clack text/password
prompts so typing '?' triggers the handoff mid-paste.
- Parallel to the existing claude-assist.ts (which is failure-triggered
and runs claude -p for a one-shot command suggestion). This is the
user-initiated, interactive counterpart.
Teams driver (setup/channels/teams.ts):
- 6-step walkthrough, each a clack note + paste prompts + stepGate
select ("Done / Stuck — hand me off to Claude / Show me again").
- Collects TEAMS_APP_ID / TEAMS_APP_TENANT_ID / TEAMS_APP_PASSWORD /
TEAMS_APP_TYPE plus the operator's public HTTPS URL (advisory —
no tunnel automation yet).
- Emits the full Azure CLI invocation alongside the portal steps for
operators who prefer scripted creation.
- UUID/password prompts accept '?' as a help escape; select prompts
have an explicit 'Stuck' option that triggers the handoff.
Manifest generator (setup/lib/teams-manifest.ts):
- Builds data/teams/teams-app-package.zip in-process: manifest.json
(schema v1.16) with app ID injected, a 32×32 outline icon, a
192×192 brand-blue color icon, bundled with the system `zip`.
- Minimal hand-rolled PNG encoder (CRC32 table + zlib deflate) so we
don't need ImageMagick or vendored binary blobs.
- ~2.5KB zip, validates with `unzip -l`; icons verify as valid PNGs.
Installer (setup/add-teams.sh):
- Non-interactive mirror of add-discord.sh. Validates the four env
vars, copies adapter from origin/channels, installs
@chat-adapter/teams@4.26.0, upserts creds to .env + data/env/env,
restarts the service.
auto.ts: Teams option in askChannelChoice with 'complex setup' hint,
dispatch to runTeamsChannel.
Deferred (known limitation, operator instructed to finish manually):
- Wait-for-first-DM pairing to capture the auto-generated Teams
platform_id. Teams platform IDs are only discoverable after the
first inbound activity. The driver installs the adapter and stops
there; the operator DMs the bot, NanoClaw auto-creates the
messaging group, and they wire an agent via /manage-channels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously init-first-agent auto-granted global owner to the first
user wired through it and left every subsequent user as nothing — no
role, no membership. That worked for the bootstrap path but broke the
second channel's welcome DM: the access gate saw no role + no
membership and dropped the message with accessReason='not_member'.
Make the role explicit:
- scripts/init-first-agent.ts accepts --role owner|admin|member
(default: owner). Role drives the grant:
owner -> global owner (agent_group_id=null)
admin -> admin scoped to this agent group
member -> no role row, just membership
Idempotent via getUserRoles pre-check — safe on re-runs. addMember
runs unconditionally (INSERT OR IGNORE) so the access gate has a
row even for users who'd otherwise pass via role alone.
- setup/lib/role-prompt.ts — shared askOperatorRole(channel) prompt
with owner as the default pick. Self-host single-operator is the
dominant case, so the user's fingers default to Enter.
- Telegram / Discord / WhatsApp drivers all call askOperatorRole
before resolving the agent name and pass --role <choice> through.
Captured in progression log via setupLog.userInput('<channel>_role').
Summary output drops the fragile "promoted on first owner" hint in
favor of a dedicated role: line ("owner (global)" / "admin (scoped to
<ag-id>)" / "member") so re-runs make the current grant legible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a setup step fails — whether hard via fail() or soft via the
"What's left" / "Skipping the first chat" notes — offer to ask Claude
to diagnose. On consent, spawn `claude -p --output-format stream-json`
with a scrolling 3-line action window ("Reading x", "Running y") so
the 1–4 minute investigations feel active rather than hung. No hard
timeout: debugging can take time, Ctrl-C is the escape hatch.
The prompt is minimal: one-paragraph framing, failed step name + msg +
hint, and a list of file references (not contents). Claude's Read/Grep
tools fetch what they need. A per-step map in claude-assist.ts gives
the most relevant files per step; the rest is README + auto.ts +
logs/setup.log + the per-step raw log.
Claude responds with REASON + COMMAND lines. We show the reason in a
clack note, prefill the command via setup/run-suggested.sh (bash 4+
readline, 3.x fallback to Enter-to-run), and eval on the user's
confirm.
When the user runs a fix, fail() now offers to retry the failing step
rather than aborting. setup/logs.ts tracks successfully-completed step
names in-memory; fail() threads those as NANOCLAW_SKIP on a spawnSync
retry, so the child picks up exactly where the parent left off — no
rebuilding containers or reinstalling OneCLI.
Other polish in this change:
- fitToWidth + dimWrap in lib/theme.ts to prevent long spinner labels
from soft-wrapping (each terminal row stacks a stale copy otherwise).
- Shorter container step label ("Preparing your assistant's sandbox…")
so it fits on narrow terminals.
- Wordmark anchored in the clack intro line on every run.
- All 25 existing fail() call sites updated to await fail(...) since
fail is now async.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WhatsApp (community/Baileys) joins the setup:auto channel picker, with
the same clack-native UX discipline as Telegram and Discord:
- setup/channels/whatsapp.ts — driver. Collects auth method (QR terminal
or pairing code), runs the auth step, renders QR blocks in-place with
ANSI cursor-rewind on rotation so the terminal doesn't fill up with
stale codes, reads creds.me.id for the bot phone, restarts the service,
asks for the operator's personal phone (defaulting to the authed
number), writes ASSISTANT_HAS_OWN_NUMBER=true when they differ
(dedicated mode), and hands off to init-first-agent.
- setup/whatsapp-auth.ts — forked standalone auth step. Channels-branch
version had a browser-QR path with an HTTP server + <canvas> QR
renderer; stripped entirely (headless/SSH users hit dead ends too
often, and the extra deps complicate install). The remaining terminal
QR emits raw QR strings in WHATSAPP_AUTH_QR blocks so the parent
driver owns the rendering. Pairing-code path retained. Status blocks
now use the runner's vocabulary (success/skipped/failed) so spawnStep
sets ok correctly; WhatsApp-specific UI text ("WhatsApp linked", "You
chat") lives in the driver.
- setup/add-whatsapp.sh — non-interactive installer, mirror of
add-telegram.sh. Fetches the adapter + groups step from the channels
branch (whatsapp-auth.ts stays local, pair-telegram.ts pattern),
installs pinned baileys/qrcode/pino, registers the steps in
setup/index.ts's STEPS map. No service restart (adapter factory
returns null until creds exist).
Cross-channel fixes bundled:
- scripts/init-first-agent.ts: always addMember(user, agentGroup) for
the target user so subsequent wirings (not the first) pass the access
gate. Telegram wiring first → Discord/WhatsApp second was dropping
every inbound with accessReason='not_member' because only the first
user gets owner. namespacedPlatformId also passes through JID-format
raws (contains '@') so WhatsApp's bare <phone>@s.whatsapp.net matches
what the adapter stores.
- setup/service.ts: launchctl unload-then-load instead of bare load (bare
load errors 'already loaded' when a prior plist was cached, keeping
launchd on the OLD ProgramArguments even after the file on disk
changed). systemctl start → restart (start is a no-op on an active
unit, swallowing unit-file edits).
- setup/add-telegram.sh: removed the in-script open "tg://resolve"
block. The driver (setup/channels/telegram.ts) now owns the deep-link,
gated on a p.confirm so the browser can't steal focus unexpectedly.
- setup/channels/discord.ts + setup/channels/telegram.ts: every browser
open goes through confirmThenOpen (new shared helper in
setup/lib/browser.ts) — operator presses Enter before their browser
takes focus. Telegram switched from tg://resolve?domain= to
https://t.me/<bot> which works everywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror of the Telegram flow but without a pairing step — Discord
exposes enough via the bot token that we only need one paste from the
operator, with every other identity field derived:
GET /users/@me → bot username (sanity check)
GET /oauth2/applications/@me → application id, verify_key
(public key), owner {id, username}
POST /users/@me/channels → DM channel id
After confirming "Is @<owner_username> your Discord account?" the flow
invites the bot to a server (OAuth URL + open + confirm, gating so the
welcome DM can actually reach the operator), installs the adapter, opens
the DM channel, and hands off to init-first-agent with
--channel discord --platform-id discord:@me:<dmChannelId>. The existing
init-first-agent welcome-over-CLI-socket path delivers the greeting
through the normal adapter pipeline — no Discord-specific code in the
welcome logic.
Fallbacks: if the app is team-owned (no owner object) or the operator
declines the confirmation, a Dev Mode walkthrough + user-id paste prompt
takes over.
Adds:
- setup/add-discord.sh (non-interactive installer, mirror of
add-telegram.sh minus pair-step registration)
- setup/channels/discord.ts (operator-facing flow)
- setup/auto.ts: Discord option in askChannelChoice + dispatch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Content pass: every user-facing line is rewritten from the perspective
of someone trying NanoClaw for the first time. Phase labels and devops
framing are gone. Examples:
"Environment OK" → "Your system looks good."
"Container image ready" → "Sandbox ready."
"OneCLI installed" → "OneCLI vault ready."
"Anthropic credential" → "Claude account"
"Mount allowlist in place" → "Access rules set."
"Service installed/running" → "NanoClaw is running."
"Wiring the terminal agent" → "Setting up your terminal chat…"
"Setup complete" → "You're ready! Enjoy NanoClaw."
Long-running steps get a one-sentence "why" that teaches a NanoClaw
differentiator while the user waits:
bootstrap → "NanoClaw is small and runs entirely on your machine.
Yours to modify."
container → "Your assistant lives in its own sandbox. It can only
see what you explicitly share."
onecli → "Your assistant never gets your API keys directly. The
vault adds them to approved requests as they leave the
sandbox."
OneCLI is now named explicitly and framed as "your agent's vault" in
the install step, the paste-auth save step, the subscription-auth
banner, and their associated failure hints.
Auth split (option b: explicit step name on fail): the auth-method
choice moves from the bash menu in register-claude-token.sh into a
clack select. Only the subscription path still breaks out to the
interactive TTY for `claude setup-token`; paste-based OAuth tokens and
API keys stay in clack via p.password() and register directly via
`onecli secrets create`. register-claude-token.sh is scoped down to
the subscription flow only.
nanoclaw.sh: dropped the "Phase 1 / Phase 2" labels. The wordmark and
subtitle now print bash-side so setup:auto skips repeating them and
the flow reads as one continuous sequence. Bootstrap label is
"Installing the basics" with a dim gutter-line "why" preamble. pnpm's
`> nanoclaw@X setup:auto` preamble is suppressed via --silent.
Em-dash pass on user-facing copy: every em-dash that functions as an
em-dash in a user-visible string is replaced with period, semicolon,
comma, or parens. Code comments and JSDoc are untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
auto.ts had grown to 923 lines with ~10 interleaved responsibilities.
Split into three focused modules, keeping auto.ts as a pure step
sequencer:
- setup/lib/runner.ts (325 lines) — spawn + stream-parse + spinner-wrap
primitives. Exports: spawnStep, spawnQuiet, runQuietStep,
runQuietChild, runUnderSpinner (internal), StatusStream, types
(Fields, Block, StepResult, SpinnerLabels, QuietChildResult),
writeStepEntry, summariseTerminalFields, dumpTranscriptOnFailure,
fail(), ensureAnswer().
- setup/lib/theme.ts (39 lines) — brand palette (brand, brandBold,
brandChip) with USE_ANSI / TRUECOLOR gating, so both auto.ts and
channel flows can render the NanoClaw cyan without duplicating the
detection.
- setup/channels/telegram.ts (277 lines) — runTelegramChannel(displayName)
owns the full flow: BotFather instructions, token paste + validation
(via getMe), install script, pair-telegram streaming UI (code card +
attempt checkpoints), agent-name prompt, init-first-agent wiring.
auto.ts drops to 376 lines. main() reads as a clean sequence of
`if (!skip.has(X)) await Xstep(...)` blocks.
fail() now takes the step name explicitly — no module-level
failingStep state. Every call site is grep-friendly and self-contained
(fail('container', msg, hint)).
Typechecks clean. Smoke-tested end-to-end: intro, mounts step,
progression log, and outro all render the same as before the split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>