Picking the wrong messaging channel during setup left users with no way
to bail out — they had to either complete the chosen flow or kill setup
and start over. This adds a Back option to the first prompt of three
channel sub-flows that share the same simple shape (one leading
brightSelect that's easy to extend).
Mechanics:
- New `setup/lib/back-nav.ts` exports a BACK_TO_CHANNEL_SELECTION
sentinel and ChannelFlowResult type.
- `setup/auto.ts` wraps the channel dispatch in a while-loop; channels
return BACK_TO_CHANNEL_SELECTION to bounce back to the chooser
without restarting setup. Channels not yet wired return void and the
loop exits after one pass, so the change is backwards compatible.
- Discord, WhatsApp, iMessage each add a `← Back to channel selection`
option to their first prompt.
Telegram, Slack, Teams, and Signal will follow as separate PRs — they
each need a slightly different shape (extra prompt insertions, gating
inside multi-step flows, etc.) and are easier to review independently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The old migration flow (detect → validate → db → groups → env →
channel-auth → channels → tasks) ran inside `bash nanoclaw.sh` via
setup/auto.ts. Replaced by the standalone `bash migrate-v2.sh` flow.
Deleted:
- setup/migrate-v1.ts (orchestrator)
- setup/migrate-v1/{detect,validate,db,env,groups,channel-auth,channels,tasks}.ts
Kept:
- setup/migrate-v1/shared.ts (used by new migrate-v2/ steps)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- verify: remove the CLI ping; cli-agent step earlier in setup already
proved the round-trip works, and the test agent gets cleaned up before
verify runs — so the ping was guaranteed to fail on installs that wired
a messaging app instead of staying CLI-only. Status now collapses to
service-running ∧ credentials ∧ ≥1 wired group.
- agent-ping: catch Claude Code's "Please run /login" / "Not logged in" /
"Invalid API key" banners so a successfully-spawned agent that has no
credentials no longer reports as 'ok'.
- auth paste: validate the full sk-ant-oat…AA shape; when the cleaned
input is under 90 chars, surface a truncation-specific hint pointing at
terminal wrap as the likely cause. Strip internal whitespace at both
validate and assignment so multi-line pastes that survive clack also
go through cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolve import conflict in setup/auto.ts — keep runMigrateV1 import,
deduplicate runWindowedStep and getLaunchdLabel/getSystemdUnit imports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Routes the post-ping `_ping-test` cleanup through `spawnQuiet` +
`setupLog.step` so a non-zero exit from `delete-cli-agent.ts` lands
in `logs/setup-steps/cleanup-cli-agent.log` and the progression log,
and prints a one-line warn to the user. Previously the spawnSync was
fire-and-forget with `stdio: 'ignore'`, leaving an orphan agent group
silently if cleanup failed.
Restores the original copy on the cli-agent step labels, the ping
explainer paragraph, and the post-ping spinner stop line — those
copy changes are out of scope for this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `fmtDuration(ms)` helper in `setup/lib/theme.ts` that returns
`47s` under a minute and `1m 34s` from 60s onward, then routes every
elapsed-time spinner suffix in the setup flow through it. Replaces
the inline `Math.round((Date.now() - start) / 1000)` + `(${elapsed}s)`
pattern at every site.
Format is consistent past 60s — `1m 0s` over `1m` — so the live
spinner doesn't change shape at every whole-minute crossing.
Sites updated: setup/auto.ts, setup/lib/{runner,tz-from-claude,
claude-assist}.ts, and setup/channels/{signal,whatsapp,telegram,
discord,slack}.ts. Pre-allocated suffix budgets in `fitToWidth`
calls bumped from `' (999s)'` to `' (99m 59s)'` so long-running
steps don't blow past the reserved width.
Remove the grouped detectExistingEnv() block that asked "reuse all or
start fresh" at the top of setup. Each channel step now reads credentials
directly from .env on disk via readEnvKey() and offers to reuse them
individually at the point of use.
- Add readEnvKey() helper in setup/environment.ts
- Remove ENV_KEY_GROUPS, ExistingEnvGroup, detectExistingEnv from auto.ts
- Move detectRegisteredGroups skip to right before cli-agent step
- Switch all channel files (telegram, discord, slack, teams, imessage)
from process.env to readEnvKey()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When maybeReexecUnderSg() re-launches setup:auto under `sg docker`,
the new process had no memory of completed steps — it re-prompted the
welcome menu, re-ran environment and container checks, and then failed
on onecli because the earlier run's state was lost.
Pass NANOCLAW_SKIP with completedStepNames() so the re-exec'd process
skips already-finished steps, suppress the welcome menu and existing-env
prompts on re-exec since the user already answered them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Scratch agent uses fixed folder `_ping-test` so it can never collide
with a real agent on re-runs
- Added --folder flag to init-cli-agent.ts and cli-agent step wrapper
- Delete always targets `_ping-test` exactly — no re-derivation needed
- Removed normalizeName coupling and FOLDER status field (no longer needed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- delete-cli-agent.ts discovers tables with agent_group_id dynamically
instead of hardcoding a list
- cli-agent step emits FOLDER in its status block so setup/auto.ts
reads it from the step result instead of re-deriving via normalizeName
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "Terminal Agent" created for the connection test is now silently
deleted after a successful ping. If the user chooses to chat, a new
agent is auto-created as "{name}'s Terminal" — no name prompt needed.
Condensed the three-line ping section into a single "Connection verified."
status line.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds an `accentGreen` helper (#3fba50) with the same TTY/NO_COLOR/
truecolor gating as the rest of the palette, then wraps the word
"you" in the "What should your assistant call you?" prompt so the
operator parses at a glance who the question is about — the user,
not the assistant. The mirror prompt that asks for the assistant's
name ("What should your assistant be called?") is left for a
follow-up.
Customize `brightSelect`'s render function so the focused option's
label paints in brand cyan during selection and the submitted answer
paints in dim cyan after the user moves on. Inactive options keep
their default rendering — only the cursor and submitted state pick
up the color, matching the body-text emphasis added in #2101.
Also migrate the one remaining `p.select` call site (the "What next?"
prompt after the first chat) to `brightSelect` so every menu in the
setup flow goes through the same render path. The shape of the call
matches what `brightSelect` already supports — message + options
with value/label/hint — so no feature is lost in the swap.
Reuses `brandBody` from #2101 for the cyan, so the prompt highlight
and the body prose share one definition of the brand body color.
Adds a `brandBody` helper in setup/lib/theme.ts that wraps prose in
brand cyan (#2BB7CE), with the same TTY/NO_COLOR/truecolor gating used
by `brand`/`brandBold`/`brandChip`. The helper splits multi-line input
and colors each line independently so the SGR sequence doesn't bleed
across clack's gutter prefix.
Routing:
- `note()` (the un-dim card wrapper from #2095) now passes
`brandBody` as its `format` callback, so card bodies render
cyan line-by-line.
- Every prose `p.log.{message,info,success,step,warn}` call in the
setup flow wraps its body argument in `brandBody`. Calls whose
body is explicitly `k.dim(...)` (failure transcript tails, log
paths, claude-assist response previews) are left alone — those
are the "preview/debug" cases the dim-policy comment in
theme.ts already carves out.
- Spinner-finish lines in windowed-runner / claude-assist color
only the message portion; the `(5s)` elapsed suffix stays dim.
Brand cyan accents (chips, wordmark, inline emphasis) are unchanged.
This PR only adds the body color.
A follow-up will add OSC 11 dark/light detection so light-mode
terminals get a brand blue (#2b6fdc) variant — opt-in upgrade with
no regression for the dark-mode default.
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 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>
setup/auto.ts spawned register-claude-token.sh via runInheritScript, which
inherits the parent Node process's PATH. When OneCLI was installed earlier
in the same setup run, its installer wrote the binary to ~/.local/bin and
appended a PATH line to the user's shell rc — but rc updates do not reach
an already-running process. The script's first guard, `command -v onecli`,
failed instantly (~3ms), and the auth step reported "Couldn't complete the
Claude sign-in" even though the real blocker was OneCLI not on PATH.
Patch process.env.PATH at the top of main() so every subsequent shell-out
sees ~/.local/bin. Idempotent — no-op if already present. Also drops a
duplicate `pollHealth` import that was lurking in the import block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slipped through during the #2035 rebase resolution — both #2030's import
and ours landed in the merge. TypeScript dedups by symbol so it didn't
fail the typecheck, but it's noise and would've eventually tripped a
linter rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original approach passed ANTHROPIC_AUTH_TOKEN into the container
as an env var and disabled the proxy for the custom host (NO_PROXY) —
which works, but bypasses OneCLI entirely for that credential. The
container holds the raw secret, the gateway loses audit/rotation, and
we lose the rest of the vault's protections for this cohort.
OneCLI-native version: store the token as a generic secret with header
injection (--header-name Authorization --value-format 'Bearer {value}'
+ host-pattern matching the base URL hostname). The container only
needs ANTHROPIC_BASE_URL plus a placeholder ANTHROPIC_AUTH_TOKEN — the
proxy rewrites the Authorization header on the wire.
setup/lib/setup-config.ts — adds --anthropic-auth-token alongside the
existing --anthropic-base-url.
setup/auto.ts — runAuthStep short-circuits the auth-method prompt when
both NANOCLAW_ANTHROPIC_BASE_URL and NANOCLAW_ANTHROPIC_AUTH_TOKEN are
set: creates the OneCLI generic secret, writes ANTHROPIC_BASE_URL to
.env (so the runtime reads it), and appends `import './claude.js';` to
src/providers/index.ts (so the provider only registers when the user
has configured a custom endpoint — no branching for everyone else).
src/providers/claude.ts — drops ANTHROPIC_AUTH_TOKEN/NO_PROXY
passthrough. Reads ANTHROPIC_BASE_URL from .env, sets a placeholder
ANTHROPIC_AUTH_TOKEN in container env so the SDK includes an
Authorization header for OneCLI to overwrite.
src/providers/index.ts — removes the unconditional import; setup
appends it on demand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a single config registry that drives both CLI flags and an opt-in
advanced-settings screen, so power users can override defaults like
remote OneCLI host/token or alt Anthropic endpoints without burdening
the standard linear flow with extra prompts.
Why: advanced configurations didn't fit cleanly into the existing
sequenced setup. PR #2030 took the "add another prompt step" route for
remote OneCLI; this approach instead routes those overrides through a
single source of truth so adding the next knob (alt endpoint, custom
host pattern, …) doesn't mean another prompt-or-skip decision.
setup/lib/setup-config.ts — schema (typed entry list with surface
'flag' | 'flag+ui'), name derivation (camelCase → NANOCLAW_UPPER_SNAKE
+ --kebab-case), seeded with --onecli-api-host, --onecli-api-token,
--anthropic-base-url, plus existing NANOCLAW_SKIP / NANOCLAW_DISPLAY_NAME
as flag-only entries.
setup/lib/setup-config-parse.ts — argv parser (--key value, --key=value,
--no-bool, -- terminator), env reader, applyToEnv() bridge that writes
resolved values back to process.env so existing step code keeps reading
env vars unchanged. Also --help printer.
setup/lib/setup-config-screen.ts — interactive menu loop. Entries
render with current value as hint; selecting one opens the right prompt
type (text / password for secrets / confirm / brightSelect for enums);
"Done" returns to the main flow.
setup/auto.ts — parses argv first (--help short-circuits before any
render), folds env+flags into process.env, then offers a welcome menu:
"Standard setup" (default) vs "Advanced". The onecli step branches on
NANOCLAW_ONECLI_API_HOST: if set, skips the local-vs-fresh prompt
entirely, runs pollHealth pre-flight, then calls runQuietStep with
--remote-url. Token, when provided, writes through to ONECLI_API_KEY in
.env. Welcome copy tightened (drops the duplicate wordmark/tagline) so
the bash → clack handoff reads as one flow.
setup/onecli.ts — cherries the --remote-url implementation from PR
run()) and generalizes writeEnvOnecliUrl into a writeEnvVar helper so
ONECLI_API_KEY follows the same upsert path.
nanoclaw.sh — forwards "$@" to setup:auto so flags reach the parser;
trims the redundant "Setting up your personal AI assistant" subtitle
and the bootstrap teach line so the pre-clack section isn't competing
with the clack intro for the same role.
Token plumbing only fires in --remote-url mode; local installs are
unauthenticated against localhost and don't need it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allow connecting to an OneCLI gateway running on another host instead
of installing one locally. Adds a third choice ('Connect to a remote
OneCLI') alongside reuse/fresh in the setup wizard, prompts for the
remote URL, validates reachability before proceeding, and passes
--remote-url to the onecli step.
In onecli.ts: extracts installOnecliCliOnly() for the remote path
(installs the CLI binary but skips the gateway), exports pollHealth
for use by auto.ts, and handles --remote-url to configure api-host
and write ONECLI_URL to .env without running the full gateway install.
`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>
`bash nanoclaw.sh` detects a v1 install before channel pairing and does a
best-effort automated port of operationally important state. Hands off to
a new `/migrate-from-v1` skill for owner seeding and fork customizations.
Between the timezone and channel steps, `setup/auto.ts` calls
`runMigrateV1()` which orchestrates these registered sub-steps (each a
separate entry in the progression log with its own raw log + status
block — failures never abort the chain):
- **migrate-detect** — scans siblings of the v2 checkout + common $HOME
locations; `$NANOCLAW_V1_PATH` overrides authoritatively. Relaxed
`package.json` check lets forks + partial installs still match; DB
presence is the strongest signal.
- **migrate-validate** — asserts v1 DB shape (tables + required
columns); writes `schema-mismatch.json` on failure. Subsequent steps
short-circuit their DB-dependent parts but still run.
- **migrate-db** — seeds `agent_groups` + `messaging_groups` +
`messaging_group_agents` from v1's `registered_groups`. JID
decomposition (`dc:123` → `channel_type='discord'`,
`platform_id='discord:123'`); `trigger_pattern` + `requires_trigger`
→ `engage_mode` + `engage_pattern` (mirrors migration 010 backfill).
Users + user_roles are NOT seeded — the skill does that with an owner
interview. Idempotent: existing rows reused, not duplicated.
- **migrate-groups** — rsync group folders. v1 `CLAUDE.md` → v2
`CLAUDE.local.md` (v2 composes `CLAUDE.md` at container spawn); v1
`container_config` JSON → `.v1-container-config.json` sidecar for the
skill to translate. Tight v1-pattern scan (`/workspace/ipc/tasks`,
`store/messages.db`, `[PR_CONTEXT:`, etc.) flags files referencing
v1-specific infrastructure — content is NOT modified, just flagged in
the handoff.
- **migrate-env** — merges v1 `.env` into v2 `.env`, never overwriting
existing v2 keys.
- **migrate-channel-auth** — per-channel registry tracks v1 env keys,
v2 required keys (with source-of-key instructions — e.g. Discord
needs `DISCORD_PUBLIC_KEY` which v1 never stored), and candidate
on-disk auth state paths (Baileys keystore, matrix sync state,
etc.). Missing required v2 keys surface as actionable followups and
flip the step to `partial`.
- **migrate-channels** — runs `setup/install-<channel>.sh` for each
detected channel in non-interactive mode. Install-script output is
captured to `logs/setup-migration/install-<channel>.log` sidecars
(silent under the parent spinner). Channels with no v2 adapter get
a `not_supported` followup but don't degrade status.
- **migrate-tasks** — v1 `scheduled_tasks` → `messages_in` rows with
`kind='task'` in each session's `inbound.db`. `schedule_type`
mapping (cron / interval / once → v2 cron). Idempotent: skips v1
task ids already present. Inactive rows dumped to
`inactive-tasks.json` for reference.
Everything writes to `logs/setup-migration/handoff.json` — the source
of truth the skill consumes.
`.claude/skills/migrate-from-v1/SKILL.md`:
- **Phase A** (always): owner seeding + v1 access policy flip
(`unknown_sender_policy` public/strict) via `AskUserQuestion`. Pulls
sender candidates from v1's `messages` table as hints.
- **Phase B** (if followups exist): walks
`handoff.followups` — translates `.v1-container-config.json`
sidecars, handles `not_supported` channels, fills in missing
required keys with instructions on where to get them.
- **Phase C** (fork-aware): `git log <upstream>..HEAD` in v1. Empty →
"no customizations to port." Non-empty → scope choice (mechanical /
full interview / reference-only). Portable categories
(`container/skills/*`, `.claude/skills/*`, docs) scan+copy with
`scanForV1Patterns`. Non-portable (`src/*`,
`container/agent-runner/src/*`) stash to `docs/v1-fork-reference/`
— explicit "don't translate v1 infra to v2" warning because v1's
IPC file queue / single DB don't exist in v2.
Clearly marked in README, CLAUDE.md, SKILL.md header, and via a `p.warn`
that fires once per run when v1 is detected. Users with no v1 install
see a silent skip — no prompts, no noise.
Verified end-to-end against a live v1 install (300 discord + 1
discord-supervisor groups, fork with ~15 commits of PR-factory work):
- Detect → validate → db (301 rows seeded) → groups (301 CLAUDE.local.md
+ 178 other files + 1 container_config sidecar) → env (4 keys copied)
→ channel-auth (flagged missing `DISCORD_APPLICATION_ID` +
`DISCORD_PUBLIC_KEY`) → channels (discord installed, discord-supervisor
→ not_supported) → tasks (0 rows, skipped)
- Idempotent re-run: 0 rows created, 903 rows reused; tasks skip if
id already present
- Fresh-user case: silent skip, no prompts, straight to "You're ready!"
- Schema-mismatch case: recorded to `schema-mismatch.json`, chain
continues
- Unit tests for the pure transforms (`parseJid`,
`inferChannelType`, `triggerToEngage`, `scanForV1Patterns`,
`looksLikeV1Install`)
- Validate `requiredV2Keys` for telegram/slack/matrix/teams/webex/
resend/linear against the actual Chat SDK packages (Discord was
verified from real error output)
- Widen candidate auth file paths for WhatsApp/Matrix/iMessage based
on real non-Discord v1 installs once we have some
See docs/v1-to-v2-changes.md for the v1 → v2 architecture diff.
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>
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>
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>
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>
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>