Commit Graph

142 Commits

Author SHA1 Message Date
exe.dev user cb15e606c3 feat(setup): move URL fallback into the open-browser prompt
On GUI devices the URL was previously rendered dim inside the
instructional `note(...)` card, then `confirmThenOpen` printed
its prompt below: read the card, see the URL, then a separate
"Press Enter to open the X" prompt with no link near it. Two
visual moments for what's really one decision.

This PR pulls the URL out of the card on GUI devices and
relocates it directly under the action line of the confirm
prompt, separated only by a dim "If browser does not appear,
please visit: <url>" line:

    │
    ◆  Press Enter to open the Developer Portal
    │  If browser does not appear, please visit: …  (dim)
    │  ● Yes / ○ No
    │

Action and fallback live as one prompt block — the user sees
both at the same time, no need to scroll back up to grab the
URL if the auto-open misses.

Headless behavior is unchanged: `formatNoteLink` still emits
"Get started: <url>" inside the card on headless devices (per
#2146), and `confirmThenOpen` still no-ops on headless (per
#2145). The only thing that changed for headless is the leading
`\n` in the helper output, which acts as a visual separator from
the steps above.

Five call sites adjusted (Discord ×3, Slack ×1, Telegram ×1) to
use `.filter((line) => line !== null)` so the now-nullable
`formatNoteLink` cleanly drops out of GUI-rendered cards.
2026-04-30 16:46:29 +03:00
exe.dev user 6863e0f63b feat(setup): label headless URL fallback with "Get started:"
When a card's auto-open is gated on `confirmThenOpen`, the URL also
appears inside the surrounding `note(...)` as a copy-paste fallback —
rendered dim because on a GUI device the auto-open is doing the
heavy lifting and the printed URL is just an incidental backup.

On headless devices the auto-open doesn't run (per #2145), so the
URL inside the note is the user's *only* path forward. A dim URL
reads as "incidental reference" exactly when it should be reading
as "this is the action."

Adds `formatNoteLink(url)` to setup/lib/browser.ts:
  - GUI device → `k.dim(url)` (unchanged from today)
  - Headless device → `Get started: <url>` at full strength

Replaces five call sites (Discord ×3, Slack ×1, Telegram ×1).
Single helper, atomic switch via the same `isHeadless()` plumbing
introduced in #2145, so the headless behavior across all five
flows stays in sync.
2026-04-30 16:46:16 +03:00
exe.dev user 4d42bb95fb feat(setup): skip browser-open prompts on headless devices
Wires the existing `isHeadless()` from setup/platform.ts into
`confirmThenOpen`. When the helper detects a headless device
(Linux without `DISPLAY`/`WAYLAND_DISPLAY`), both the
"Press Enter to open your browser" prompt and the actual
`openUrl(...)` call are skipped — there's no browser to launch
and the user can't usefully press Enter to summon one.

Why this is enough — the surrounding flow already supports the
headless path implicitly:

  - Every `confirmThenOpen` call site sits beneath a `note(...)`
    that prints the URL and the steps the user needs to take.
    The URL is already visible to copy-paste onto another
    device.

  - Every site is followed by an explicit confirmation prompt
    ("Got your bot token?", "Done with the X?", etc.) that
    naturally serves as the headless user's "I finished the
    thing on my other device" signal.

So the headless branch becomes: read the note, do the thing,
answer the next prompt — without a useless "Press Enter to
open your browser" detour in between.

Coverage rationale (~95% accurate for the cases that actually
cause user confusion today):

  - Linux + no `DISPLAY`/`WAYLAND_DISPLAY` → headless. Catches:
      • Raspberry Pi headless installs
      • Bare-metal Linux servers
      • SSH'd into Linux without X11 forwarding
      • CI environments on Linux
      • Linux containers (which have no display)
  - macOS → never headless. Even SSH'd Macs can usually still
    open URLs through the local user's session, so treating
    them as GUI-capable is the right default.
  - Windows → never headless (effectively always GUI in
    practice).

The remaining ~5% are edge cases (someone manually unset
`DISPLAY` on a desktop Linux session, etc.) that almost never
happen accidentally and recover gracefully — the URL is still
visible in the surrounding note.

Six call sites in channel adapters (Discord ×3, Slack ×1,
Telegram ×1, Teams ×1) all change behavior atomically through
the single helper. No per-site copy changes needed; consistency
is enforced by the central wiring.
2026-04-30 16:45:59 +03:00
exe.dev user a66cd545d5 feat(setup): switch elapsed-time suffixes to "Xm Ys" past 60s
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.
2026-04-30 16:45:21 +03:00
gabi-simons 5be15be139 fix: prevent telegram pairing spinner from flooding the terminal
The spinner label exceeded terminal width, breaking clack's cursor-up
redraw and causing each animation tick to print a new line instead of
updating in-place. Wrap with fitToWidth() like other setup spinners.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 12:07:53 +00:00
gavrielc 9a919f4148 Merge branch 'main' into setup-assistant-green 2026-04-29 15:36:14 +03:00
exe.dev user 4608836953 feat(setup): paint "assistant" green in the agent-name prompt
Wraps the word "assistant" in `accentGreen` (#3fba50, added in #2103)
across the six channel adapters that ask "What should your assistant
be called?" — Discord, iMessage, Signal, Slack, Telegram, WhatsApp.
Mirrors the green emphasis on "you" in the display-name prompt: the
green word names the subject of the question (assistant vs operator)
so the operator parses it at a glance.
2026-04-29 12:32:25 +00:00
gavrielc 0044bba0e5 Merge branch 'main' into setup-pronoun-green 2026-04-29 15:25:02 +03:00
exe.dev user 26594d2c54 feat(setup): paint "you" green in the display-name prompt
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.
2026-04-29 12:16:15 +00:00
gavrielc 3742165708 Merge branch 'main' into setup-color-choices 2026-04-29 15:07:00 +03:00
exe.dev user 4c791a41b2 feat(setup): cyan highlight on active and submitted choices
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.
2026-04-29 12:01:35 +00:00
gavrielc 7d153df710 Merge branch 'main' into setup-color-body 2026-04-29 14:58:02 +03:00
exe.dev user ab2d509671 feat(setup): paint card and log bodies in brand cyan
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.
2026-04-29 11:43:30 +00:00
gavrielc 9f564650c6 Merge branch 'main' into setup-token-headless 2026-04-29 14:02:45 +03:00
Daniel M b7f099db96 Merge branch 'main' into setup-token-headless 2026-04-29 13:59:24 +03:00
gavrielc c8e960314a Merge remote-tracking branch 'upstream/main' into fix/setup-reuse-existing-env
# Conflicts:
#	setup/channels/imessage.ts
#	setup/channels/telegram.ts
2026-04-29 13:58:21 +03:00
Gabi Simons d4868a5e01 Merge branch 'main' into fix/password-clear-on-error 2026-04-29 13:35:48 +03:00
Gabi Simons a014a67556 fix password fields not clearing after validation error
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>
2026-04-29 10:34:58 +00:00
Gabi Simons aa390b3fd0 detect existing .env and credentials on setup re-run
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>
2026-04-29 10:20:54 +00:00
exe.dev user 9c8f680ca8 fix: stop dimming setup card bodies
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.
2026-04-29 10:20:10 +00:00
exe.dev user 93be2d15f0 fix claude setup-token flow for headless/remote systems
Use script(1) to capture PTY output and extract OAuth token when
browser-based auth isn't available, with fallback code-paste flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 10:18:38 +00:00
exe.dev user 89738917ae offer to install and authenticate Claude CLI before diagnosis
When setup fails and claude-assist kicks in, instead of silently
skipping when the CLI is missing or unauthenticated, interactively
offer to install it (via install-claude.sh) and sign in (via
claude setup-token) so the user can get diagnostic help immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:18:29 +00:00
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 b44bcf5dcf Merge branch 'main' into fix/custom-anthropic-base-url 2026-04-27 00:38:53 +03:00
gavrielc be86bd3c2d fix(setup): remove duplicate pollHealth import in auto.ts
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>
2026-04-27 00:35:55 +03:00
gavrielc 6591062fbb refactor: route custom Anthropic endpoint through OneCLI vault
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>
2026-04-27 00:34:31 +03:00
gavrielc 7693a20970 feat(setup): validate onecli api token starts with oc_
Matches the OneCLI CLI's own format expectation ("oc_... format" per
`onecli auth login --help`) so a malformed token gets caught at setup
time rather than at first vault call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:13:07 +03:00
gavrielc e706dcac00 feat(setup): default OneCLI remote URL to https://app.onecli.sh
Replaces the example.internal placeholder with the hosted gateway URL
so the advanced screen and --help suggest the canonical destination
out of the box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:13:07 +03:00
gavrielc f048447ec5 feat(setup): authenticate onecli CLI for remote vault setup
Without `onecli auth login`, setup-time CLI calls (e.g. `secrets list`
inside anthropicSecretExists, `secrets create` in runPasteAuth) hit a
secured remote vault unauthenticated and fail silently — the auth step
sees no existing Anthropic credential and prompts the user to add one
even when it's already in the remote vault.

Two auth surfaces matter here: the CLI's persistent store via
`onecli auth login --api-key`, and ONECLI_API_KEY in .env that the
runtime SDK reads at request time. We need both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 00:13:07 +03:00
gavrielc efdd05a7ef feat(setup): advanced settings registry with remote OneCLI support
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>
2026-04-27 00:13:07 +03:00
Emmanuel Venisse 6b431c195d feat(setup): add remote OneCLI option in setup flow
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.
2026-04-26 18:33:19 +02:00
grtwrn fc375ca72b fix(register): wire channels with correct engage fields, skip prefix for native IDs
setup/register.ts had two bugs that prevented new channels from being
registered via `/manage-channels`:

1. createMessagingGroupAgent was called with the legacy field names
   `trigger_rules` and `response_scope`. The SQL INSERT expects
   `engage_mode` / `engage_pattern` / `sender_scope` / `ignored_message_policy`
   (migration 010). Every register call failed with
   `RangeError: Missing named parameter "engage_mode"` after the agent
   and messaging group were partially created — leaving an orphaned pair.

   Now mirrors scripts/init-first-agent.ts:wireIfMissing:
   - Groups (is_group=1) default to engage_mode='mention' (bot only
     responds when addressed).
   - DMs (is_group=0) default to engage_mode='pattern' with '.' (respond
     to every message).
   - An explicit --trigger overrides the pattern regex.

2. The "normalize platform_id" block unconditionally prefixed
   "<channel>:" even for native IDs like WhatsApp JIDs
   ("120363408974444974@g.us"), iMessage emails ("user@example.com"),
   or Signal phones ("+15551234567") / Signal groups ("group:abc"). But
   the router (src/router.ts:158) looks up messaging_groups by the raw
   event.platformId from the adapter, which for these native adapters
   never has a prefix. So the prefixed row was never matched — the
   message was silently dropped with no "Message routed" log.

   Extracted scripts/init-first-agent.ts:namespacedPlatformId into
   src/platform-id.ts so both setup paths use the same heuristic (skip
   the prefix for IDs containing '@', starting with '+', or starting
   with 'group:'). Prevents future drift between the two paths.

Tested by: re-running `setup/index.ts --step register` for a WhatsApp
group JID, confirming the row is created with correct engage fields
and matching platform_id, then sending a test message and observing
"Message routed" with the right agent group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:06:10 +03:00
gavrielc 41162517d9 Merge pull request #1960 from shock99-samantha/fix/register-engage-mode-schema
fix(setup): register step uses engage_mode columns dropped by migration 010
2026-04-24 15:35:35 +03:00
gavrielc 9bb416c157 Merge branch 'main' into codex/detect-auth-errors-in-setup 2026-04-24 15:27:35 +03:00
glifocat 9fd694c763 chore(setup): minimize verify diff 2026-04-24 11:49:04 +02:00
glifocat 4fc2c4275c test(setup): cover CLI-only verify status 2026-04-24 11:44:58 +02:00
glifocat 1de5a0356b fix(setup): accept CLI-only verify success 2026-04-24 11:44:35 +02:00
Pankaj Garg f41c162009 detect setup auth ping failures 2026-04-24 09:23:18 +02:00
Samantha d0c608c751 fix(setup): register step uses engage_mode columns dropped by migration 010
Migration 010-engage-modes (replace trigger_rules + response_scope with
engage_mode/engage_pattern/sender_scope/ignored_message_policy) updated
the schema and the production code paths, but missed setup/register.ts.

The step still constructed a payload with the dropped columns. On any
fresh v2 install, attempting to register a channel via:

  pnpm exec tsx setup/index.ts --step register -- --platform-id ...

fails with: `Missing named parameter "engage_mode"`. This affects every
flow that calls the register step — the /add-<channel> skills,
/manage-channels, and the setup auto driver.

Map old → new:
- trigger_rules.pattern (string) → engage_mode='pattern',
  engage_pattern=<pattern>
- requiresTrigger=false (no pattern) → engage_mode='pattern',
  engage_pattern='.' (the "always" sentinel from migration 010)
- requiresTrigger=true (no pattern) → engage_mode='mention'
- response_scope='all' → sender_scope='all',
  ignored_message_policy='drop' (conservative default matching the
  migration backfill rule)

Tested by registering three Telegram channels (one DM, two groups) on a
fresh v2 install — all succeeded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 19:44:47 -04: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
cheats1314 539af750d4 fix(setup): detect registered groups from v2 central db
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>
2026-04-23 22:22:18 +08: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 73c931594a Merge branch 'main' into fix/scoped-container-reaper 2026-04-23 12:52:32 +03:00
Lazer Cohen 2383bde80f fix(container): scope orphan reaper by install label so peers don't kill each other
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>
2026-04-23 12:12:30 +03:00
gavrielc dee7e0be32 feat(setup): Yes-default + session-persist on claude-assist, quieter first-chat
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>
2026-04-23 11:23:37 +03:00
gavrielc 990d243dbd fix(setup): bypass rate-limited GitHub API when installing onecli CLI
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>
2026-04-23 11:10:30 +03:00
gavrielc 910342fd80 style(setup): lift text weight — prose becomes regular, outcomes bold
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>
2026-04-23 10:59:12 +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