Commit Graph

3 Commits

Author SHA1 Message Date
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
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
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