Commit Graph

996 Commits

Author SHA1 Message Date
gavrielc e64bdb3016 refactor(claude-md): split shared base into module fragments, inject name at runtime
Move every agent-specific instruction out of the shared container/CLAUDE.md
so the base is genuinely universal. Persona/identity now comes from the
system-prompt addendum (buildSystemPromptAddendum now takes assistantName
and prepends "# You are {name}"). Per-module instructions live alongside
each MCP tool source:

  container/agent-runner/src/mcp-tools/core.instructions.md
  container/agent-runner/src/mcp-tools/scheduling.instructions.md
  container/agent-runner/src/mcp-tools/self-mod.instructions.md

composeGroupClaudeMd() scans that directory and emits `module-<name>.md`
fragments as symlinks to /app/src/mcp-tools/<name>.instructions.md (valid
via the existing RO source mount). Skill fragments renamed to
`skill-<name>.md` for naming consistency with `module-*` and `mcp-*`.

Mount tightening so composer-managed files can't be clobbered by agent
writes: nested RO mounts for /workspace/agent/CLAUDE.md and
/workspace/agent/.claude-fragments/. CLAUDE.local.md (per-group memory)
stays RW as the only writable CLAUDE.md-family file.

.gitignore: ignore CLAUDE.local.md, .claude-shared.md, .claude-fragments/
everywhere, and simplify groups/ rules to ignore the whole tree (per-
installation state, not tracked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:14:51 +03:00
gavrielc 95e74d8383 docs(onecli): expand secrets section; correct stale admin-roles refs
Document the selective-mode gotcha for auto-created OneCLI agents
(no secrets injected by default) with the CLI commands to inspect
and fix it. Note that approval policies are not configurable via
the SDK or `onecli@1.3.0` CLI — web UI only.

Replace stale `NANOCLAW_ADMIN_USER_IDS` / `src/access.ts` references
across CLAUDE.md, docs/architecture.md, docs/checklist.md, and
docs/module-contract.md. Admin gating now runs host-side in
src/command-gate.ts against `user_roles`; approver picks live in
src/modules/approvals/primitive.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:46:17 +03:00
gavrielc 202ee71311 feat(setup): auto-detect timezone after CLI agent step
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>
2026-04-22 16:44:53 +03:00
gavrielc 0ed00b3358 docs(claude.md): add v1-merge STOP banner directing to migrate-v2.sh
Prepend a Claude-addressed banner so that when an upgrader (or Claude on
their behalf) runs `git pull` / `git merge` from v1 and hits merge
conflicts, Claude aborts the merge and routes the user to
`bash migrate-v2.sh` instead of trying to resolve the rewrite by hand.
Fresh clones are explicitly told to ignore the banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:21:28 +03:00
gavrielc 3db66c0ced fix: forward ONECLI_API_KEY to OneCLI SDK for authenticated container config
Ports the v1 fix from PR #1777 (originally 8b5b581 by @johnnyfish).
Cherry-pick did not apply cleanly because v2 reformatted the surrounding
code and split OneCLI usage into two sites — manual port was needed.

v2-specific adaptations:
- Also forward apiKey at the second OneCLI call site in
  src/modules/approvals/onecli-approvals.ts (v2 split the approvals
  module out of container-runner).
- Skipped the companion test-mock commit (38163bc) — it patches
  src/container-runner.test.ts, which no longer exists in v2 (tests
  consolidated into host-core.test.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: johnnyfish <jonathanfishner11@gmail.com>
2026-04-22 15:16:59 +03:00
gavrielc 5371c76c14 docs(readme): mark translations as pre-v2 pending update
The Chinese and Japanese READMEs still describe the v1 architecture
(setup flow, channel list, "main channel" concept, etc.). Add a notice
at the top of each pointing readers to the English README for current
content until the translations are refreshed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:10:17 +03:00
gavrielc fe942dd3dd chore: bump to 2.0.0 with v2 CHANGELOG entry
Major version for the v2 rewrite. CHANGELOG documents the breaking
changes users will hit on upgrade: new entity model, two-DB session
split, `bash nanoclaw.sh` as default install, channels/providers
relocated to sibling branches, three-level isolation, Apple Container
removed from default setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:10:10 +03:00
gavrielc 8e1c8f8f61 style: apply prettier formatting to touched files
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:57:09 +03:00
gavrielc 8662f21e8f docs(readme): update for v2 + scripted setup default
Rewrite install flow around `bash nanoclaw.sh`, update What It Supports to
reflect the three-level isolation model and the real channel roster, fix
the Philosophy section (AI-native hybrid, skills over features via
channels/providers branches, Codex/OpenCode/Ollama as drop-in providers),
and replace the pre-v2 architecture diagram and key-files list with the
two-DB session split (`inbound.db` / `outbound.db`) and current src layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:53:57 +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 a541e7542c Merge pull request #1914 from qwibitai/v2-with-refactors
V2 with refactors
2026-04-22 13:49:41 +03:00
gavrielc fb82c1babb Delete docs/shared-src.md 2026-04-22 13:46:28 +03:00
exe.dev user 7da24b166d fix(agent-runner): remove thread_id filter and fix processing ack on empty result
The concurrent poll in processQuery filtered out messages with
mismatched thread_ids, causing a deadlock when the initial batch
(e.g. a host-generated welcome trigger with null thread_id) completed
but follow-ups arrived with a different thread_id (e.g. a Discord DM).
The query stayed open waiting for matching-thread pushes that never
came, blocking the poll loop indefinitely.

Thread routing is the router's concern — per-thread sessions already
isolate threads into separate containers; shared sessions intentionally
merge everything. Removed the filter.

Also fixed processing_ack: a result event (with or without text) means
the turn is done, but markCompleted only ran when event.text was truthy.
When the agent responded via MCP send_message (empty result text), the
initial batch stayed in 'processing' for the query's lifetime, creating
false stuck signals in the host sweep. Now marks completed on any result
event.

Belt-and-suspenders: init-first-agent welcome trigger now sets threadId
to the DM platform_id instead of null.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 10:42:56 +00:00
gavrielc c8fc1da719 refactor(claude-md): compose per-group CLAUDE.md from shared base + fragments
Replace the per-group "written once at init, owned by the group" CLAUDE.md
with a host-regenerated entry point that imports:

  - a shared base (`container/CLAUDE.md` mounted RO at `/app/CLAUDE.md`)
  - optional per-skill fragments (skills that ship `instructions.md`)
  - optional per-MCP-server fragments (inline `instructions` field in
    `container.json`)
  - per-group agent memory (`CLAUDE.local.md`, auto-loaded by Claude Code)

Principle: RW = per-group memory, RO = shared content. Source/skills/base
are shared; personality, config, working files, and Claude state stay
per-group.

Key changes:

  - New `src/claude-md-compose.ts` — per-spawn composition +
    `migrateGroupsToClaudeLocal()` one-time cutover.
  - New `container/CLAUDE.md` — shared base, seeded verbatim from the
    former `groups/global/CLAUDE.md`.
  - `src/container-runner.ts` — swap `/workspace/global` mount for RO
    `/app/CLAUDE.md`; call `composeGroupClaudeMd()` after
    `initGroupFilesystem()`.
  - `src/group-init.ts` — drop `.claude-global.md` symlink + initial
    `CLAUDE.md` write; seed `CLAUDE.local.md` from `opts.instructions`.
  - `src/index.ts` — call `migrateGroupsToClaudeLocal()` at startup.
  - `src/container-config.ts` — add optional `instructions` field to
    `McpServerConfig` (inline per-MCP guidance fragment).
  - `container/Dockerfile` — drop dead `/workspace/global` mkdir.
  - Remove obsolete `scripts/migrate-group-claude-md.ts`.

Migration (runs once at host startup, idempotent):

  - Delete `.claude-global.md` symlinks in each group.
  - Rename each `groups/<folder>/CLAUDE.md` → `CLAUDE.local.md`
    (preserves existing per-group content as memory).
  - Delete `groups/global/` directory.

Design docs: `docs/claude-md-composition.md` and `docs/shared-source.md`
(the latter is the sibling design discussion this refactor builds on).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 12:58:43 +03:00
exe.dev user 8a12fa61ac refactor: shared source — replace per-group agent-runner copies with single RO mount
Replace the per-group agent-runner-src copy model with a single shared
read-only mount. Source and skills are now RO + shared; personality,
config, working files, and Claude state stay RW + per-group.

Key changes:
- Mount container/agent-runner/src/ RO at /app/src (all groups share one copy)
- Mount container/skills/ RO at /app/skills; per-group skill selection via
  symlinks in .claude-shared/skills/ based on container.json "skills" field
- Mount container.json as nested RO bind on top of RW group dir
- Move all NANOCLAW_* env vars to container.json (runner reads at startup)
- New runner config.ts module replaces process.env reads
- Move command gate (filtered/admin) from container to host router
- Dockerfile: remove source COPY, split CLI installs (claude-code last),
  move agent-runner deps above CLIs for better layer caching
- Add writeOutboundDirect for router denial responses
- Design doc at docs/shared-src.md

Not included (follow-up): DB migration to drop agent_provider columns,
cleanup of orphaned agent-runner-src directories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 12:58:43 +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 72b7a72cbb feat(setup): ping agent before chat, detect stale service, auto-install Claude
Round-trip confirmation before first chat. After cli-agent wires up the
Terminal Agent, send `chat ping` through the CLI socket under a spinner
with 30s timeout (shared helper in setup/lib/agent-ping.ts, also used by
verify). Only after a real reply do we show "Your assistant is ready."
and enter the chat loop. Ping failures surface a targeted note
(socket_error vs no_reply) and skip the prompt — so users never type
into the void.

Checkout-mismatch detection. verify resolves the running service PID's
script path via `ps -p <pid> -o command=` and compares to projectRoot.
If the service is running from a sibling clone (common for developers
with multiple checkouts), SERVICE comes back as running_other_checkout
instead of running, AGENT_PING is skipped, and the failure note tells
the user exactly which bootout + bootstrap pair to run.

Native Claude Code install on demand. Only the subscription auth path
needs `claude`; the paste-token and paste-API-key paths don't. So
register-claude-token.sh now runs setup/install-claude.sh when `claude`
is missing (curl -fsSL https://claude.ai/install.sh | bash), then
prepends ~/.local/bin to PATH in-process so the rest of the script can
see the fresh binary.

Gutter-safe wrapping. wrapForGutter + dimWrap in lib/theme.ts hard-wrap
text to `process.stdout.columns - gutter` on word boundaries, measuring
visible length (ANSI-stripped). dimWrap applies the dim envelope per
line because clack resets styling at each line break when rendering
multi-line log content — a single outer dim() only colors the first
line. Applied to the long "why" notes before container + onecli, the
channel-skip info, the ping-failure note, and the checkout-mismatch
remediation.

Wordmark anchoring. printIntro always includes the NanoClaw wordmark in
the clack intro line, whether or not nanoclaw.sh already printed one in
bash. Worth ~1 line of redundancy so the brand stays visible at the top
of the clack session after bootstrap output scrolls out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 11:07:35 +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 1e0f7d631d Merge pull request #1884 from ssimeonov/fix/sdk-binary-resolution
fix(container): v2 -- point SDK to pnpm-installed Claude Code binary
2026-04-22 09:55:24 +03:00
gavrielc a263da3e53 feat(setup): prompt to install Homebrew on factory macOS
install-node.sh and install-docker.sh both require brew on macOS. On a
fresh Mac there's no brew, so the bootstrap spinner would bail with a
cryptic "Homebrew not installed" error. Move the prompt to nanoclaw.sh
as a pre-flight, so the user sees a clear ask — including the heads-up
that Xcode Command Line Tools come along for the ride (~5-10 min) —
before the spinner starts and brew's own sudo/CLT prompts appear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:17:19 +03:00
gavrielc 1858ef35f0 Merge pull request #1908 from qwibitai/setup-auto
feat(setup): scripted branded setup flow (nanoclaw.sh)
2026-04-22 03:06:30 +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
gavrielc 416fe01855 refactor(setup): drop CLI-bonus wiring from init-first-agent
init-first-agent used to double-wire the CLI channel to every new DM
agent as a convenience for `pnpm run chat`, gated by --no-cli-bonus.
With the /new-setup-2 flow gone and a dedicated scratch CLI agent
created earlier in setup:auto, that bonus just stomps on CLI routing
the user already set up. Remove the CLI_CHANNEL/CLI_PLATFORM_ID
constants, ensureCliMessagingGroup, the --no-cli-bonus flag, and the
cli-bonus wiring block.

Pass the paired user's identity through to the welcome delivery so
the sender resolver sees the real owner (e.g. telegram:<id>) instead
of cli:local. Extend the CLI channel's admin-transport payload to
accept optional sender/senderId overrides — falls back to the old
cli/cli:local defaults when omitted, so existing callers are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 02:13:22 +03:00
gavrielc 5269edada4 feat(setup): three-level output (clack UI / progression log / raw per-step)
Documents and implements the output contract from docs/setup-flow.md:

  Level 1: clack UI — branded, concise, product content
  Level 2: logs/setup.log — append-only, linear, structured entries for
           humans + AI agents reviewing a run
  Level 3: logs/setup-steps/NN-name.log — full raw stdout+stderr per step

Every scripted sub-step, including bootstrap, emits at all three levels.
Bootstrap now runs under a bash-side clack-alike spinner with live elapsed
time; its apt/pnpm output is captured to 01-bootstrap.log and summarised
as a progression entry. setup.sh's legacy log() routes to the raw log
instead of contaminating the progression log.

Telegram install becomes fully branded: setup/auto.ts owns the BotFather
instructions (clack note), token paste (clack password with format
validation), and getMe check (clack spinner). add-telegram.sh drops to a
non-interactive installer that reads TELEGRAM_BOT_TOKEN from env, logs to
stderr, and emits a single ADD_TELEGRAM status block on stdout.

The Anthropic credential flow is the one intentional break — register-
claude-token.sh still inherits the TTY for claude setup-token's browser
dance; it logs as an 'interactive' progression entry with the method.

setup/logs.ts centralises the level 2/3 formatting: reset, header, step,
userInput, complete, abort, stepRawLog. User answers (display name, agent
name, channel choice, telegram_token preview) log as their own entries so
the setup path is reconstructable from the progression log alone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 02:02:13 +03:00
gavrielc 6e0d742a7f feat(setup): brand setup:auto with @clack/prompts + brand palette
Wraps the scripted setup flow in a branded, friendly UI. Each step runs
under a clack spinner with elapsed time; child stdout/stderr is captured
quietly and dumped only on failure. Interactive children (token paste,
Anthropic OAuth) bypass the spinner and inherit the TTY.

- intro: NanoClaw wordmark + brand-cyan accent chip, truecolor with
  kleur fallback and NO_COLOR / non-TTY awareness
- pair-telegram: emits PAIR_TELEGRAM_CODE / _ATTEMPT status blocks only;
  auto.ts renders clack notes + "received X — doesn't match" checkpoints
- streaming status-block parser handles mid-step events without waiting
  for the child to exit
- terminal-block detection now finds any block with a STATUS field
  (handles MOUNTS emitting CONFIGURE_MOUNTS, etc.) and treats 'skipped'
  as a success variant with an optional friendlier label

Also fixes a latent bash bug where `$VAR…` (unbraced followed by a
multi-byte Unicode character) pulled ellipsis bytes into the variable
name lookup and tripped `set -u`. Braced `${VAR}` in add-telegram.sh
and register-claude-token.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 01:09:26 +03:00
gavrielc e24ecbf8b0 refactor(setup): own pair-telegram.ts in this branch with clean output
Previously setup:auto parsed pair-telegram's machine-readable status
blocks and rendered a banner on top. Fork the script instead: check
in setup/pair-telegram.ts with a focused 4-digit banner, a short
wrong-attempt line, and a single final PAIR_TELEGRAM status block
(kept so the parent driver still picks up PLATFORM_ID and
PAIRED_USER_ID via parseStatus).

Drop pair-telegram.ts from add-telegram.sh's copy list so the local
version isn't overwritten on re-runs. The other adapter files
(telegram.ts, telegram-pairing.ts, etc.) still come from the channels
branch.

Also fix a latent bug: auto.ts was reading ADMIN_USER_ID from the
success block, but the actual field name is PAIRED_USER_ID —
init-first-agent would have been called with --user-id "".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:27:43 +03:00
gavrielc 356a4d0a9f feat(setup): render Telegram pairing code in a focused banner
The pair-telegram step emits PAIR_TELEGRAM_ISSUED / _NEW_CODE /
_ATTEMPT blocks meant for /setup skill parsing — dumping them raw in
setup:auto left the operator squinting at key/value clutter. Intercept
the stream line-by-line, suppress the block framing, and print just
the 4-digit code inside a box with a short instruction. Wrong-code
attempts and the final success block also get short human lines.
parseStatus still runs on the full buffered output at close so
PLATFORM_ID / ADMIN_USER_ID flow through unchanged to init-first-agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:22:53 +03:00
gavrielc 5a472c4155 fix(setup): print bot URL alongside the deep-link attempt
Headless / SSH / WSL users won't have \`open\` or \`xdg-open\` wired up,
so the deep-link fails silently and they have no clue where to go.
Always print https://t.me/<username> so the URL is at least clickable
or copy-pasteable from the terminal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:19:23 +03:00
gavrielc e7d798b00d feat(setup): validate Telegram token via getMe and deep-link to bot
After the token is in .env, call
https://api.telegram.org/bot<TOKEN>/getMe — if ok, extract the bot's
username and \`open tg://resolve?domain=<username>\` so the Telegram
desktop app lands on the bot chat. When pair-telegram prints the
4-digit code a moment later, the user just types it into the already-
open chat instead of hunting for their bot.

Falls back to https://t.me/<username> if the tg:// scheme isn't
registered, and just warns-and-continues if getMe fails (network
hiccup shouldn't block setup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:17:42 +03:00
gavrielc 92c28a956d feat(setup): run init-first-agent after Telegram pairing
pair-telegram only identifies the chat and operator — it returns
PLATFORM_ID and ADMIN_USER_ID but doesn't create the agent group,
grant owner, or send the welcome. scripts/init-first-agent.ts does
that, matching the pattern the /new-setup skill already uses for
channel wiring.

Also prompts for the agent's own name (default: Nano), overridable
via NANOCLAW_AGENT_NAME. displayName is hoisted out of the cli-agent
block so both cli-agent and channel wiring share the value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:11:35 +03:00
gavrielc 9c7e1d02af feat(setup): optional Telegram wiring in setup:auto
After cli-agent, prompt the user to connect a messaging app. For now
only Telegram is offered; "skip" falls through to the existing CLI
flow.

setup/add-telegram.sh runs the scriptable half of /add-telegram: fetch
the channels branch, copy the adapter + pair-telegram files, append
the self-registration import, install @chat-adapter/telegram@4.26.0
(pinned to match the skill), rebuild, collect TELEGRAM_BOT_TOKEN via
silent paste, write .env + data/env/env, and kick the service so the
new adapter is live. Idempotent throughout.

setup:auto then runs the existing `pair-telegram` step with
--intent main. The step emits the 4-digit code in its status stream,
which is already forwarded to stdout by runStep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:04:14 +03:00
gavrielc c87cd250b2 feat(verify): end-to-end agent ping via CLI channel
Verify now runs \`pnpm run chat ping\` silently and checks for a reply.
Emits AGENT_PING=ok|no_reply|socket_error|skipped; skipped when the
service isn't running or no groups are wired (those already fail the
verify via other checks). Kills the child after 90s so a wedged
container can't hang setup (chat.ts's own 120s timeout is too long
here). setup:auto surfaces AGENT_PING!=ok in its failure summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:52:51 +03:00
gavrielc 85faa3eab0 fix(setup): rephrase display-name prompt
"Your agents" — the name is stored on the operator's user row and
applies to every future agent they wire up, not just this scratch CLI
one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:49:28 +03:00
gavrielc d02001e144 feat(setup): prompt for display name, hardcode agent persona
Before the cli-agent step, ask the operator what the agent should
call them (defaults to \$USER). The agent's own persona name is
hardcoded to "Terminal Agent" — this is the scratch CLI agent, not
one of the operator's real personas. NANOCLAW_DISPLAY_NAME still
skips the prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:46:49 +03:00
gavrielc 81838bbb34 fix(setup): clarify silent-paste prompt
Explicitly tell the user that nothing appears on screen as they paste
and that a single Enter submits. "(Input is hidden for safety.)" was
ambiguous — users kept waiting for a visible confirmation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:43:43 +03:00
gavrielc 1c748f1f2b refactor(setup): drop timezone step from setup:auto chain
The timezone step blocked the scripted flow on headless servers where
the resolved TZ was UTC (interactive /setup confirms, setup:auto had
to bail). Drop it from the chain — host TZ defaults to whatever the
OS reports. Users who need an explicit override run the step on
demand: `pnpm exec tsx setup/index.ts --step timezone -- --tz <zone>`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:18:38 +03:00
gavrielc e49cbce429 Merge pull request #1883 from ssimeonov/fix/claude-code-version-bump
fix(container): v2 -- bump Claude Code to 2.1.116 and Agent SDK to ^0.2.116
2026-04-21 21:56:26 +03:00
gavrielc a9a488eb27 Merge pull request #1900 from davekim917/fix/discord-truncation-and-session-persist
fix: persist SDK session_id on init + split long outbound messages
2026-04-21 21:36:32 +03:00
Gabi Simons 52a9ab5179 feat(add-wechat): personal WeChat channel via Tencent iLink Bot API
New channel skill for personal WeChat, using Tencent's official iLink
Bot API (the same protocol @tencent-weixin/openclaw-weixin uses).
Region-restricted to mainland 微信 accounts — international WeChat
clients can't complete the QR flow.

Skill contents:
- Install steps copy the adapter from the `channels` branch (same
  pattern as other /add-<channel> skills) and register it in
  src/channels/index.ts.
- Post-login wiring helper at scripts/wire-dm.ts — lists unwired
  WeChat messaging groups, prompts for an agent group, and inserts the
  messaging_group_agents row with sender policy `request_approval` by
  default (matches the router auto-create default so the admin gets an
  approval card on the next unknown-sender DM).
- Channel Info documents how /new-setup Claude captures the
  operator's user_id (from data/wechat/auth.json.operatorUserId) and
  the first DM's platform_id (from the adapter's "WeChat inbound" log).

Also adds WeChat as option 15 in /new-setup's channel list so setup
wires into the existing /add-<channel> flow automatically.

Addresses https://github.com/qwibitai/nanoclaw/issues/1901.

Co-Authored-By: ythx-101 <226337373+ythx-101@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:21:50 +00:00
exe.dev user be6cec59ad fix(setup): auto-recover from stale docker group mid-session
- container: install Docker via setup/install-docker.sh when missing,
  distinguish socket EACCES from daemon-down so we bail fast instead of
  polling 60s, and re-exec the step under `sg docker` when usermod hasn't
  reached the current shell.
- auto: after the container step, re-exec the whole driver under `sg
  docker` (with a NANOCLAW_REEXEC_SG guard) so onecli/service/verify also
  get docker-group access without a re-login. Surface the new
  docker_group_not_active error from the container step.
- service: when the systemd user manager has a stale group list, auto-
  apply \`sudo setfacl -m u:\$USER:rw /var/run/docker.sock\` so the service
  can start without waiting for the next login.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:55:04 +00:00
gavrielc e86d0d93dd feat(setup): wire CLI agent in setup:auto
Chains `cli-agent` (wraps scripts/init-cli-agent.ts) between service and
verify. Without this wiring, the socket at data/cli.sock accepts the
connection but there's no agent group routed to `cli/local`, so
`pnpm run chat` hangs waiting for a reply.

Defaults: display name from NANOCLAW_DISPLAY_NAME env, falling back to
\$USER then "Operator". Agent persona name from NANOCLAW_AGENT_NAME,
defaulting to the display name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 18:45:52 +03:00
exe.dev user fd2e404ba9 fix(setup): auto-install Node and bypass corepack prompt
Node check now triggers setup/install-node.sh when missing/too old, and
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 prevents the first-use prompt from
hanging the script when stdout is redirected to the log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:05:52 +00:00
gavrielc 264849da6c feat(setup): add nanoclaw.sh entry point
Single command end-to-end: `bash nanoclaw.sh` runs setup.sh for
bootstrap and hands off to `pnpm run setup:auto` on success. Passes
through NANOCLAW_TZ, NANOCLAW_SKIP, SECRET_NAME, HOST_PATTERN via env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:45:04 +03:00
gavrielc b0cae1ba4c feat(setup): chain register-claude-token.sh into setup:auto
Runs after the OneCLI install step and before mounts/service. Skips
silently when `onecli secrets list` already reports an Anthropic
secret, so re-running setup:auto on a configured install is a no-op.
Child process uses stdio:inherit so the menu + browser sign-in flow
work normally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:41:29 +03:00
gavrielc ee5995ae16 feat(setup): add register-claude-token.sh
Bash helper that registers an Anthropic credential in OneCLI via three
paths: Claude subscription (runs `claude setup-token` under script(1)
for PTY capture), paste an existing sk-ant-oat… OAuth token, or paste
an sk-ant-api… API key.

On bash 4+ the `claude setup-token` command is pre-filled in the
readline buffer so Enter submits it. On bash 3.2 (macOS default
/bin/bash) we fall back to a plain confirmation prompt. Token
extraction strips ANSI + TTY-wrap line breaks and anchors on
sk-ant-oat…AA with a length cap (via perl; BSD grep caps {n,m} at 255).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:38:43 +03:00
gavrielc 3ce4101cd9 feat(setup): chain OneCLI install in setup:auto
The install half of the OneCLI step is fully scriptable (the gateway
and CLI install themselves via `curl | sh`, PATH + api-host + .env
updates are idempotent). Register the Anthropic secret is still
interactive — the auto driver leaves that for `/setup` §4 to handle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:13:39 +03:00