Commit Graph

15 Commits

Author SHA1 Message Date
Gabi Simons
c5d0243417 fix(setup): add Interactivity & Shortcuts step to Slack setup
Slack interactive buttons (channel approval cards) require Interactivity
to be enabled in the app settings. Without it, button clicks silently
fail to reach the host. Added the step to both the setup wizard
post-install checklist and the add-slack SKILL.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 12:19:44 +00:00
Gabi Simons
c36f0c6b36 fix(setup): wire Slack agent during setup like Discord/Telegram
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>
2026-04-28 11:35:51 +00:00
gavrielc
3fa001409e feat(setup): wire Signal into the auto setup flow
`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>
2026-04-23 23:20:47 +03:00
gavrielc
3101f65a72 feat(setup): add Slack and iMessage channel flows (experimental)
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>
2026-04-23 13:26:06 +03:00
gavrielc
4ff4cc75b9 Merge remote-tracking branch 'origin/main' into setup-feedback-fixes
# Conflicts:
#	setup/auto.ts
#	setup/channels/whatsapp.ts
2026-04-23 10:39:35 +03:00
gavrielc
56ef5b4461 feat(setup): clarify setup flow from user-feedback session
- 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>
2026-04-23 10:35:12 +03:00
gavrielc
7a9401ddf2 feat(setup): per-checkout service name and docker image tag
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>
2026-04-23 10:10:09 +03:00
gavrielc
390e09597a feat(setup): hand off to Claude for Teams finish-wiring
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>
2026-04-22 14:31:32 +03:00
gavrielc
a70e41856b feat(setup): Microsoft Teams wiring with Claude handoff
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>
2026-04-22 14:27:29 +03:00
gavrielc
596035be09 feat(setup): operator role prompt per channel, owner by default
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>
2026-04-22 12:57:57 +03:00
gavrielc
4859d8fb2d feat(setup): Claude-assisted error recovery with resume-at-step retry
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>
2026-04-22 12:42:44 +03:00
gavrielc
dfcbab5364 feat(setup): optional WhatsApp wiring + cross-channel UX polish
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>
2026-04-22 12:39:48 +03:00
gavrielc
9b6e5b24a1 feat(setup): optional Discord wiring in setup:auto
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>
2026-04-22 10:45:05 +03:00
gavrielc
7d2081660b feat(setup): rewrite copy for first-time users + split auth flow
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>
2026-04-22 02:57:20 +03:00
gavrielc
9b7d4d50e4 refactor(setup): split auto.ts into runner + theme + telegram channel
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>
2026-04-22 02:26:50 +03:00