Align the environment check with the v2 setup flow so existing wired agent groups are detected from data/v2.db instead of the retired v1 store. This prevents setup from reporting no registered groups on valid v2 installs and adds regression coverage for both v2 and pre-migration state.
Co-Authored-By: Claude Opus 4.7 <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>
Two installs on the same host could trash each other's containers: the
reaper used `docker ps --filter name=nanoclaw-`, a substring match that
picked up every install's containers. A crash-looping peer (e.g. a legacy
v1 plist respawning ~6k times) would call cleanupOrphans on every boot and
kill the healthy install's session containers within seconds of spawn.
- Stamp `--label nanoclaw-install=<slug>` onto every spawned container.
- cleanupOrphans filters by that label; healthy peers are left alone.
- Setup preflight enumerates `com.nanoclaw*` launchd plists / nanoclaw
user systemd units, probes state/runs, and unloads any that are
crash-looping (state != running AND runs > 10) before installing
this install's service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three UX tweaks after watching a user walk through setup:
1. Claude-assist "Run this command?" now defaults to Yes. After Claude has already been asked to diagnose + explained the fix, the vast majority of users want to run it — the No-default added friction without proportional safety.
2. claude-assist persists its session across failures in one setup run. First invocation captures session_id from the stream-json init event; subsequent invocations pass --resume <id>. Claude sees prior failures as conversation history instead of treating each hiccup as a blank-slate ticket.
3. First-chat flow no longer drops the user into a free-text chat loop by default. Instead: explain what the ping/pong check is doing, wait for the pong, then offer "Continue with setup" (recommended, default) or "Pause here and chat with your agent from the terminal" (opt-in). The free-text loop is still reachable, just not the default path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream onecli.sh/cli/install script resolves the latest release via
api.github.com/repos/onecli/onecli-cli/releases/latest — anonymous callers
get throttled to 60 req/hour per IP, and once exhausted the installer dies
with "curl: (56) 403 / Error: could not determine latest release". Shared
IPs (corporate NAT, public Wi-Fi) hit this without ever running the
installer themselves. Reproduced locally: rate_limit remaining=0 → upstream
installer returns the exact user error.
Fallback path when upstream fails:
1. Resolve version via `curl -fsSL -o /dev/null -w '%{url_effective}' \
https://github.com/onecli/onecli-cli/releases/latest`. That endpoint
302s to /tag/vX.Y.Z — parses the version without an API call.
2. If the redirect probe also fails, install a pinned fallback version
(ONECLI_CLI_FALLBACK_VERSION, currently 1.3.0).
3. Download the archive from /releases/download/vX.Y.Z/… directly (the
CDN path isn't API-throttled), extract, and install to /usr/local/bin
or ~/.local/bin mirroring upstream's install-dir logic.
Gateway install (onecli.sh/install, docker-compose based) is untouched —
it doesn't hit the API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dimmed explanatory prose blocks were hard to read against dark terminals. Shift the weight ladder up a notch:
- dimWrap() no longer dims. Multi-line prose (the step-intro copy, etc.) renders at the terminal's regular weight.
- Spinner outcome labels (done/failed/skipped) are now bold via runUnderSpinner, so each step's headline reads stronger than the body copy around it.
- Un-dim two command-hint blocks in auto.ts (docker-group setfacl + service restart; the socket-error remediation commands) — those are commands the user may need to type.
Dim is still used where it helps — (Ns) spinner timings, URLs, short inline parentheticals — and for the preview/debug blocks dim is explicitly reserved for: dumpTranscriptOnFailure tail and claude-assist streams.
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>
Before: setup/onecli.ts ran `curl -fsSL onecli.sh/install | sh` unconditionally. For users with OneCLI already running and bound to a specific listener (host-accessible, shared with other apps), re-running the installer rebound the gateway and broke those consumers.
Now: auto.ts probes for an existing install (`onecli version` + `onecli config get api-host`). If detected, clack asks: use the existing instance (recommended) or install a fresh one. The new --reuse flag in the onecli step skips the installer, reads the configured api-host, writes ONECLI_URL to .env, and moves on without touching the running gateway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forks that keep the upstream nanoclaw repo under a non-origin remote name (typically `upstream`, with `origin` pointing at the user's fork) hit "git fetch origin channels failed" when adding a channel, because the fork doesn't carry the channels branch. New setup/lib/channels-remote.sh walks `git remote -v` for a url matching qwibitai/nanoclaw, auto-adds `upstream` if none matches, and honors NANOCLAW_CHANNELS_REMOTE as an override. Wired into the four add-*.sh scripts that setup:auto invokes (discord, telegram, whatsapp, teams).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shared bash + node emitter in setup/lib/diagnostics.{sh,ts} reads/writes data/install-id so every event from a single install shares one distinct_id — bash-side setup_launched/setup_start, node-side auto_started, per-step started/completed, auth_method_chosen, channel_chosen, first_chat_ready/failed, setup_incomplete, setup_aborted, setup_completed. Opt-out via NANOCLAW_NO_DIAGNOSTICS=1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a timezone step between cli-agent and channel wiring in setup:auto.
Autodetect via --step timezone; if it resolves to UTC or fails, confirm
with the user and accept either an IANA zone or a free-text description
(e.g. "New York"). Free-text falls through to a headless `claude -p`
call that returns a single IANA string, gated on the claude CLI being
on PATH.
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>
Round-trip confirmation before first chat. After cli-agent wires up the
Terminal Agent, send `chat ping` through the CLI socket under a spinner
with 30s timeout (shared helper in setup/lib/agent-ping.ts, also used by
verify). Only after a real reply do we show "Your assistant is ready."
and enter the chat loop. Ping failures surface a targeted note
(socket_error vs no_reply) and skip the prompt — so users never type
into the void.
Checkout-mismatch detection. verify resolves the running service PID's
script path via `ps -p <pid> -o command=` and compares to projectRoot.
If the service is running from a sibling clone (common for developers
with multiple checkouts), SERVICE comes back as running_other_checkout
instead of running, AGENT_PING is skipped, and the failure note tells
the user exactly which bootout + bootstrap pair to run.
Native Claude Code install on demand. Only the subscription auth path
needs `claude`; the paste-token and paste-API-key paths don't. So
register-claude-token.sh now runs setup/install-claude.sh when `claude`
is missing (curl -fsSL https://claude.ai/install.sh | bash), then
prepends ~/.local/bin to PATH in-process so the rest of the script can
see the fresh binary.
Gutter-safe wrapping. wrapForGutter + dimWrap in lib/theme.ts hard-wrap
text to `process.stdout.columns - gutter` on word boundaries, measuring
visible length (ANSI-stripped). dimWrap applies the dim envelope per
line because clack resets styling at each line break when rendering
multi-line log content — a single outer dim() only colors the first
line. Applied to the long "why" notes before container + onecli, the
channel-skip info, the ping-failure note, and the checkout-mismatch
remediation.
Wordmark anchoring. printIntro always includes the NanoClaw wordmark in
the clack intro line, whether or not nanoclaw.sh already printed one in
bash. Worth ~1 line of redundancy so the brand stays visible at the top
of the clack session after bootstrap output scrolls out.
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>
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>
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>
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>
- 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>
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>
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>