Documents and implements the output contract from docs/setup-flow.md:
Level 1: clack UI — branded, concise, product content
Level 2: logs/setup.log — append-only, linear, structured entries for
humans + AI agents reviewing a run
Level 3: logs/setup-steps/NN-name.log — full raw stdout+stderr per step
Every scripted sub-step, including bootstrap, emits at all three levels.
Bootstrap now runs under a bash-side clack-alike spinner with live elapsed
time; its apt/pnpm output is captured to 01-bootstrap.log and summarised
as a progression entry. setup.sh's legacy log() routes to the raw log
instead of contaminating the progression log.
Telegram install becomes fully branded: setup/auto.ts owns the BotFather
instructions (clack note), token paste (clack password with format
validation), and getMe check (clack spinner). add-telegram.sh drops to a
non-interactive installer that reads TELEGRAM_BOT_TOKEN from env, logs to
stderr, and emits a single ADD_TELEGRAM status block on stdout.
The Anthropic credential flow is the one intentional break — register-
claude-token.sh still inherits the TTY for claude setup-token's browser
dance; it logs as an 'interactive' progression entry with the method.
setup/logs.ts centralises the level 2/3 formatting: reset, header, step,
userInput, complete, abort, stepRawLog. User answers (display name, agent
name, channel choice, telegram_token preview) log as their own entries so
the setup path is reconstructable from the progression log alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the scripted setup flow in a branded, friendly UI. Each step runs
under a clack spinner with elapsed time; child stdout/stderr is captured
quietly and dumped only on failure. Interactive children (token paste,
Anthropic OAuth) bypass the spinner and inherit the TTY.
- intro: NanoClaw wordmark + brand-cyan accent chip, truecolor with
kleur fallback and NO_COLOR / non-TTY awareness
- pair-telegram: emits PAIR_TELEGRAM_CODE / _ATTEMPT status blocks only;
auto.ts renders clack notes + "received X — doesn't match" checkpoints
- streaming status-block parser handles mid-step events without waiting
for the child to exit
- terminal-block detection now finds any block with a STATUS field
(handles MOUNTS emitting CONFIGURE_MOUNTS, etc.) and treats 'skipped'
as a success variant with an optional friendlier label
Also fixes a latent bash bug where `$VAR…` (unbraced followed by a
multi-byte Unicode character) pulled ellipsis bytes into the variable
name lookup and tripped `set -u`. Braced `${VAR}` in add-telegram.sh
and register-claude-token.sh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously setup:auto parsed pair-telegram's machine-readable status
blocks and rendered a banner on top. Fork the script instead: check
in setup/pair-telegram.ts with a focused 4-digit banner, a short
wrong-attempt line, and a single final PAIR_TELEGRAM status block
(kept so the parent driver still picks up PLATFORM_ID and
PAIRED_USER_ID via parseStatus).
Drop pair-telegram.ts from add-telegram.sh's copy list so the local
version isn't overwritten on re-runs. The other adapter files
(telegram.ts, telegram-pairing.ts, etc.) still come from the channels
branch.
Also fix a latent bug: auto.ts was reading ADMIN_USER_ID from the
success block, but the actual field name is PAIRED_USER_ID —
init-first-agent would have been called with --user-id "".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pair-telegram step emits PAIR_TELEGRAM_ISSUED / _NEW_CODE /
_ATTEMPT blocks meant for /setup skill parsing — dumping them raw in
setup:auto left the operator squinting at key/value clutter. Intercept
the stream line-by-line, suppress the block framing, and print just
the 4-digit code inside a box with a short instruction. Wrong-code
attempts and the final success block also get short human lines.
parseStatus still runs on the full buffered output at close so
PLATFORM_ID / ADMIN_USER_ID flow through unchanged to init-first-agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Headless / SSH / WSL users won't have \`open\` or \`xdg-open\` wired up,
so the deep-link fails silently and they have no clue where to go.
Always print https://t.me/<username> so the URL is at least clickable
or copy-pasteable from the terminal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the token is in .env, call
https://api.telegram.org/bot<TOKEN>/getMe — if ok, extract the bot's
username and \`open tg://resolve?domain=<username>\` so the Telegram
desktop app lands on the bot chat. When pair-telegram prints the
4-digit code a moment later, the user just types it into the already-
open chat instead of hunting for their bot.
Falls back to https://t.me/<username> if the tg:// scheme isn't
registered, and just warns-and-continues if getMe fails (network
hiccup shouldn't block setup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pair-telegram only identifies the chat and operator — it returns
PLATFORM_ID and ADMIN_USER_ID but doesn't create the agent group,
grant owner, or send the welcome. scripts/init-first-agent.ts does
that, matching the pattern the /new-setup skill already uses for
channel wiring.
Also prompts for the agent's own name (default: Nano), overridable
via NANOCLAW_AGENT_NAME. displayName is hoisted out of the cli-agent
block so both cli-agent and channel wiring share the value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After cli-agent, prompt the user to connect a messaging app. For now
only Telegram is offered; "skip" falls through to the existing CLI
flow.
setup/add-telegram.sh runs the scriptable half of /add-telegram: fetch
the channels branch, copy the adapter + pair-telegram files, append
the self-registration import, install @chat-adapter/telegram@4.26.0
(pinned to match the skill), rebuild, collect TELEGRAM_BOT_TOKEN via
silent paste, write .env + data/env/env, and kick the service so the
new adapter is live. Idempotent throughout.
setup:auto then runs the existing `pair-telegram` step with
--intent main. The step emits the 4-digit code in its status stream,
which is already forwarded to stdout by runStep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verify now runs \`pnpm run chat ping\` silently and checks for a reply.
Emits AGENT_PING=ok|no_reply|socket_error|skipped; skipped when the
service isn't running or no groups are wired (those already fail the
verify via other checks). Kills the child after 90s so a wedged
container can't hang setup (chat.ts's own 120s timeout is too long
here). setup:auto surfaces AGENT_PING!=ok in its failure summary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Your agents" — the name is stored on the operator's user row and
applies to every future agent they wire up, not just this scratch CLI
one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before the cli-agent step, ask the operator what the agent should
call them (defaults to \$USER). The agent's own persona name is
hardcoded to "Terminal Agent" — this is the scratch CLI agent, not
one of the operator's real personas. NANOCLAW_DISPLAY_NAME still
skips the prompt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Explicitly tell the user that nothing appears on screen as they paste
and that a single Enter submits. "(Input is hidden for safety.)" was
ambiguous — users kept waiting for a visible confirmation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The timezone step blocked the scripted flow on headless servers where
the resolved TZ was UTC (interactive /setup confirms, setup:auto had
to bail). Drop it from the chain — host TZ defaults to whatever the
OS reports. Users who need an explicit override run the step on
demand: `pnpm exec tsx setup/index.ts --step timezone -- --tz <zone>`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- container: install Docker via setup/install-docker.sh when missing,
distinguish socket EACCES from daemon-down so we bail fast instead of
polling 60s, and re-exec the step under `sg docker` when usermod hasn't
reached the current shell.
- auto: after the container step, re-exec the whole driver under `sg
docker` (with a NANOCLAW_REEXEC_SG guard) so onecli/service/verify also
get docker-group access without a re-login. Surface the new
docker_group_not_active error from the container step.
- service: when the systemd user manager has a stale group list, auto-
apply \`sudo setfacl -m u:\$USER:rw /var/run/docker.sock\` so the service
can start without waiting for the next login.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chains `cli-agent` (wraps scripts/init-cli-agent.ts) between service and
verify. Without this wiring, the socket at data/cli.sock accepts the
connection but there's no agent group routed to `cli/local`, so
`pnpm run chat` hangs waiting for a reply.
Defaults: display name from NANOCLAW_DISPLAY_NAME env, falling back to
\$USER then "Operator". Agent persona name from NANOCLAW_AGENT_NAME,
defaulting to the display name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Node check now triggers setup/install-node.sh when missing/too old, and
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 prevents the first-use prompt from
hanging the script when stdout is redirected to the log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single command end-to-end: `bash nanoclaw.sh` runs setup.sh for
bootstrap and hands off to `pnpm run setup:auto` on success. Passes
through NANOCLAW_TZ, NANOCLAW_SKIP, SECRET_NAME, HOST_PATTERN via env.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runs after the OneCLI install step and before mounts/service. Skips
silently when `onecli secrets list` already reports an Anthropic
secret, so re-running setup:auto on a configured install is a no-op.
Child process uses stdio:inherit so the menu + browser sign-in flow
work normally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bash helper that registers an Anthropic credential in OneCLI via three
paths: Claude subscription (runs `claude setup-token` under script(1)
for PTY capture), paste an existing sk-ant-oat… OAuth token, or paste
an sk-ant-api… API key.
On bash 4+ the `claude setup-token` command is pre-filled in the
readline buffer so Enter submits it. On bash 3.2 (macOS default
/bin/bash) we fall back to a plain confirmation prompt. Token
extraction strips ANSI + TTY-wrap line breaks and anchors on
sk-ant-oat…AA with a length cap (via perl; BSD grep caps {n,m} at 255).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The install half of the OneCLI step is fully scriptable (the gateway
and CLI install themselves via `curl | sh`, PATH + api-host + .env
updates are idempotent). Register the Anthropic secret is still
interactive — the auto driver leaves that for `/setup` §4 to handle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`pnpm run setup:auto` chains the deterministic setup steps (environment
→ timezone → container → mounts → service → verify) by spawning the
existing per-step CLI and parsing its status blocks. Config via env:
NANOCLAW_TZ, NANOCLAW_SKIP.
Credentials + channel install + /manage-channels stay interactive —
verify reports what's left and exits 0 rather than failing the driver.
Also have the container step try to start Docker when it's installed
but not running (open -a Docker on macOS, sudo systemctl start docker
on Linux) and poll `docker info` for up to 60s before giving up. Both
/setup and setup:auto pick this up automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apple Container is no longer supported — the runtime abstraction in
src/container-runtime.ts is already Docker-only. Remove the remaining
setup-time branches that probed for it: the Apple Container runtime
option in the container build step, the APPLE_CONTAINER field emitted
by the environment check, and the `command -v container` probe in
verify. `--runtime docker` still parses for backwards compatibility
with the /setup skill.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapses the two-phase setup into a single linear skill: steps 1-6
(prereqs through end-to-end CLI ping) run straight through, steps 7-13
(naming, timezone, channel wiring, mounts, QoL, done) are skippable.
Drops the "chat now vs. continue" branch point — after the ping the
flow emits "Test Agent success, proceeding with setup" and continues
directly into the naming questions.
Also updates stale `/new-setup-2` header comments in setup/install-*.sh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the unknown-channel approval flow completes, seed a /welcome task
into the newly-wired session so the agent greets the new user on first
contact. The replayed /start (Telegram's default first-message) is filtered
by the agent-runner's command-command filter, so without an explicit
onboarding trigger the first interaction produced nothing.
Pin the destination by its local_name from agent_destinations to avoid the
agent picking the wrong named destination (previously it greeted the owner,
whose DM is in CLAUDE.md).
Also guard dispatchResultText against echoing trailing status lines when
the agent has already sent messages explicitly via send_message. Otherwise
a task-triggered flow that calls send_message then emits "welcome message
sent" produces a duplicate chat to the recipient.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapses the two-phase setup into a single linear skill: steps 1-6
(prereqs through end-to-end CLI ping) run straight through, steps 7-13
(naming, timezone, channel wiring, mounts, QoL, done) are skippable.
Drops the "chat now vs. continue" branch point — after the ping the
flow emits "Test Agent success, proceeding with setup" and continues
directly into the naming questions.
Also updates stale `/new-setup-2` header comments in setup/install-*.sh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The welcome DM used to be handed to the running service over the CLI
admin socket, which stamped `cli:local` as the sender. On a strict
messaging group (any fresh DM wired by init-first-agent), that tripped
the unknown-sender approval gate — the operator's own bootstrap script
ended up requesting its own approval. Fix by writing the welcome
directly into the session's inbound.db with a `System` sender; the
running service's host-sweep wakes the container on its next pass.
Also drop `--no-cli-bonus`. Now that init-cli-agent always wires
cli/local to the scratch CLI agent, every caller of init-first-agent
had to pass --no-cli-bonus to avoid double-wiring; the flag has become
mandatory, so it's just removed. The cli-bonus branch goes with it.
Skill docs updated in lockstep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parseUserId now falls back to user.kind when the id prefix isn't a
registered adapter — Teams uses `29:` rather than `teams:`, so the
literal prefix wouldn't resolve the channel adapter for cold DMs.
init-cli-agent no longer claims the first-owner slot on `cli:local`.
The CLI identity is scratch; owner promotion belongs to
init-first-agent once the real channel user is wired.
Ports the emacs channel skill to match the other channel skills:
copy src/channels/emacs.ts + emacs/nanoclaw.el from the channels branch,
append the self-registration import, enable via EMACS_ENABLED, and wire
through the register setup step. Documents the v2 entity model (single
messaging group, platform_id="default") and drops the v1 auto-register /
symlink behavior that the old adapter did.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These shipped with the old v1 architecture and are no longer needed:
- add-reactions, add-voice-transcription, add-image-vision, add-pdf-reader,
use-local-whisper — Chat SDK channels handle these natively now;
the WhatsApp native (Baileys) adapter on the channels branch covers
attachments and reactions out of the box.
- add-compact — no longer needed.
- add-telegram-swarm — Chat SDK Teams adapter handles multi-bot identity.
- channel-formatting — Chat SDK does per-channel formatting natively.
- add-gmail — was built on a legacy MCP server; deprecated.
add-emacs and use-native-credential-proxy are kept and will be ported
to the current architecture in follow-up commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-commit hook reflowed imports on files changed in the previous commit.
Unrelated format drift on other files intentionally left unstaged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- InboundEvent gains an optional replyTo; router stamps the row's address
fields from it when set, so replies can route to a different channel than
the one the inbound came in on.
- ChannelSetup adds onInboundEvent for admin-transport adapters that build
the full event themselves.
- CLI wire format accepts {text, to, reply_to}. Routed messages go through
onInboundEvent and do not evict an active chat client.
- init-first-agent hands the DM welcome to the running service via
data/cli.sock — synchronous wake, no sweep wait. Fails loudly if the
service is down; no silent fallback.
- Split the CLI scratch-agent bootstrap into scripts/init-cli-agent.ts;
init-first-agent is DM-only.
Agents cannot set replyTo: it lives only on the inbound/router seam and is
consumed once when writing messages_in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Twelve idempotent install scripts matching install-telegram.sh's shape,
one per channel in /new-setup-2's pick list (except Emacs, which uses
a git-merge install flow). Each bundles preflight + fetch + copy +
register + pnpm install + build so a single allowlisted bash call can
replace a chain of permission prompts. Linear's also patches
chat-sdk-bridge.ts for catchAll forwarding; Matrix's runs the
post-install ESM-extension dist patch; WhatsApp-native's covers the
deterministic install portion only — QR/pairing auth still lives in
/add-whatsapp.
Scripts only; new-setup-2/SKILL.md integration deferred pending a
decision on whether to generalize the set-env pattern from 712a0e1
across the Chat SDK channels (each /add-<channel>/SKILL.md's
Credentials section has a similar unapprovable shell chain).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Telegram clients send /start when a user first DMs a bot (and when they
tap "Start" on a bot profile). It's a platform handshake, not a
user-intended prompt — forwarding it to the agent wastes a turn and
produces a confused response.
Matches the existing filter pattern for /help, /login, /logout,
/doctor, /config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three allowlist-friendly setup helpers so /new-setup and /new-setup-2
don't hit unmatchable commands during a fresh install:
- setup/install-node.sh — idempotent Node 22 install wrapper (macOS via brew,
Linux via NodeSource + apt). Replaces the raw `curl | sudo -E bash -` flow
whose stdin-consuming `bash -` segment can't be pre-approved.
- setup/install-docker.sh — same pattern for Docker (brew --cask on macOS,
get.docker.com on Linux + usermod).
- setup/set-env.ts — generic `--step set-env` that writes KEY=VALUE to .env
(and optionally syncs to data/env/env) so channel-install flows don't
invent `grep && sed && rm` pipelines, which split at each && and can't be
tightly allowlisted.
new-setup-2's Telegram path now uses set-env for TELEGRAM_BOT_TOKEN and
explicitly skips /add-telegram's Credentials section. new-setup step 1 and
step 2 now call the install wrappers; the raw curl/apt entries are gone from
the allowed-tools list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Timezone and host-mount prompts now go through AskUserQuestion for a
cleaner UI; channel selection stays plain-prose but is numbered (14
options exceeds the 4-option AskUserQuestion cap).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inserts a Timezone step (new step 3) that runs --step timezone and, if
the resolver lands on UTC, asks the user to confirm before leaving UTC
in .env; re-runs with --tz <answer> if they give a real IANA zone.
Renumbers subsequent steps accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch Bash(bash setup/install-telegram.sh) to a prefix match so trailing
flags or redirections don't fall through to approval prompts. Add the
common read-only coreutils (tail, head, grep) the model reaches for to
cap noisy build output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the /add-telegram preflight + install commands into
setup/install-telegram.sh so /new-setup-2 can run the adapter install
programmatically when the user picks Telegram, then hand off to
/add-telegram for credentials and pairing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the router sees a mention or DM on a messaging group that isn't wired
to any agent, it now escalates to an owner for approval instead of silently
dropping. Mirrors the existing unknown-sender approval pattern (ACTION-ITEMS
item 22).
Schema (migration 012):
- `messaging_groups.denied_at TEXT NULL` — timestamp set on deny so future
mentions stop escalating. ALTER TABLE ADD COLUMN, FK-safe (unlike the
rebuild that bit migration 011).
- `pending_channel_approvals` — PK on `messaging_group_id` gives free
in-flight dedup. One card per channel, no spam on rapid retries.
Router:
- New hook `setChannelRequestGate(mg, event) => Promise<void>`, invoked
from the no-wirings branch when the message was addressed to the bot
(isMention=true). Hook is fire-and-forget.
- Checks `mg.denied_at` before escalating — denied channels drop silently
and do not re-prompt.
- The two "no-wirings" branches (fresh auto-create and existing mg with
no agents) are consolidated into one escalation path that calls the
gate once. Without the module, behavior is log + record (no regression).
Permissions module:
- `channel-approval.ts::requestChannelApproval` — MVP picker: target
agent is `getAllAgentGroups()[0]`, card names it explicitly ("Wire it
to <Andy>?"). Approver via existing `pickApprover` + `pickApprovalDelivery`
primitives.
- Response handler: same click-auth pattern as sender-approval (clicker
must be the designated approver OR have admin privilege over the
target agent group).
- Approve defaults per the feature spec:
engage_mode = 'mention-sticky' for groups, 'pattern' + '.' for DMs
sender_scope = 'known'
ignored_message_policy = 'accumulate'
session_mode = 'shared'
DM vs group inferred from the original event's threadId (non-null →
group) because the auto-created mg has a placeholder is_group=0 until
the adapter fills it in.
- Triggering sender is auto-added to agent_group_members so sender_scope=
'known' doesn't bounce the replayed message into a sender-approval
cascade.
- Deny: stamps messaging_groups.denied_at, clears pending row.
- Failure modes — no owner, no agent groups, no reachable DM — log and
drop without creating a pending row, letting a future attempt try
again (same as sender-approval).
9 new integration tests cover every branch: mention triggers card, DM
triggers card, dedup, approve creates correct wiring + admits sender +
replays, approve-on-DM uses pattern/'.' defaults, deny sets denied_at
and future mentions drop silently, unauthorized clicker rejected,
no-owner drops, no-agent-groups drops.
168 tests pass (was 159; +9).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>