Move the MIME/type-to-extension maps and derivation helpers out of
session-manager.ts into a dedicated attachment-naming module — keeps
session-manager focused on session lifecycle and gives the helpers
a natural home for unit tests alongside the existing attachment-safety
module.
Two small fixes alongside the extraction:
- extForMime now guards `typeof mime !== 'string'` before .split, so a
buggy bridge passing `mimeType: { ... }` (object) no longer crashes
the inbound write loop.
- deriveAttachmentName computes Date.now() once per call instead of
twice, and tightens the explicit-name check to a string-and-truthy
guard so non-string values fall through to derivation.
Adds attachment-naming.test.ts with 11 cases covering MIME normalization
(case + parameters), Telegram type fallback, the non-string defensive
guard, and the bare-timestamp fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removing the "Not logged in · Please run /login" detection and
substitution from this PR — narrowing scope to just the OneCLI
gateway transient-retry change. The login-message handling will be
addressed separately.
Reverts:
- AgentProvider.isAuthRequired / authRequiredMessage
- ClaudeProvider auth-required regex, classifier, and remediation text
- poll-loop writeAuthRequiredMessage helper + call sites
- claude.test.ts (auth-only test file)
OneCLI/wakeContainer changes (the remaining content of the PR) are
unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a paired `authRequiredMessage()` method to AgentProvider so
per-provider auth-failure remediation can differ. Claude returns the
Anthropic/`claude` instruction; future providers (Codex, OpenCode, …)
can return their own remediation text. The poll-loop calls
`provider.authRequiredMessage?.()` and falls back to a generic message
if a provider implements `isAuthRequired` without supplying its own
remediation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- wakeContainer now never throws — returns Promise<boolean>, catches
internally. Closes the regression risk for the 5 awaited callers in
agent-to-agent, interactive, and approvals/response-handler that the
previous version left unwrapped. Router uses the boolean to stop the
typing indicator on transient failure; host-sweep just awaits.
- Tighten AUTH_REQUIRED_RE: anchor to start-of-string with the specific
`·` (U+00B7) separator the CLI uses, so an agent that quotes the
banner mid-sentence in a normal reply doesn't trip the classifier.
- Log a one-line note from writeAuthRequiredMessage so substitutions
are visible when debugging "user got the credentials message but I
don't see why."
- Add unit tests for ClaudeProvider.isAuthRequired covering both banner
variants, trailing content, mid-sentence quoting, leading-prose
quoting, alternate separators, and unrelated text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related fixes for the case where credentials aren't usable:
1. Replace Claude Code's "Not logged in / Invalid API key · Please run
/login" output with a host-aware message. The user can't run /login
from chat, so the raw text is unhelpful. Provider gains an optional
isAuthRequired() classifier; the poll-loop substitutes the message
on both result-text and error paths.
2. Treat OneCLI gateway failure as a transient hard error instead of
spawning a credential-less container. The catch in container-runner
now propagates; router and host-sweep wrap wakeContainer to log and
leave the inbound row pending so the next 60s sweep tick retries.
Router also stops the typing indicator on failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- getDelay indexed by attempt (1-based) into a 0-indexed array, so the
leading 0 was unreachable and every "after a crash" delay was shifted
up one slot. Use attempt - 1 so the documented schedule (0s → 0s →
10s → 30s → 2min → 5min → 15min cap) actually holds.
- enforceStartupBackoff runs before initDb (which creates DATA_DIR), so
on a fresh checkout fs.writeFileSync hit ENOENT. write() now
mkdirSync's DATA_DIR first.
- shutdown() didn't run resetCircuitBreaker if teardownChannelAdapters
threw, so a graceful exit with a teardown error would be counted as a
crash on the next start. Wrap teardown in try/finally.
- Adds src/circuit-breaker.test.ts: state transitions, full schedule
(parameterized), reset-window expiry, malformed file, and the
fresh-install path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slipped through during the #2035 rebase resolution — both #2030's import
and ours landed in the merge. TypeScript dedups by symbol so it didn't
fail the typecheck, but it's noise and would've eventually tripped a
linter rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original approach passed ANTHROPIC_AUTH_TOKEN into the container
as an env var and disabled the proxy for the custom host (NO_PROXY) —
which works, but bypasses OneCLI entirely for that credential. The
container holds the raw secret, the gateway loses audit/rotation, and
we lose the rest of the vault's protections for this cohort.
OneCLI-native version: store the token as a generic secret with header
injection (--header-name Authorization --value-format 'Bearer {value}'
+ host-pattern matching the base URL hostname). The container only
needs ANTHROPIC_BASE_URL plus a placeholder ANTHROPIC_AUTH_TOKEN — the
proxy rewrites the Authorization header on the wire.
setup/lib/setup-config.ts — adds --anthropic-auth-token alongside the
existing --anthropic-base-url.
setup/auto.ts — runAuthStep short-circuits the auth-method prompt when
both NANOCLAW_ANTHROPIC_BASE_URL and NANOCLAW_ANTHROPIC_AUTH_TOKEN are
set: creates the OneCLI generic secret, writes ANTHROPIC_BASE_URL to
.env (so the runtime reads it), and appends `import './claude.js';` to
src/providers/index.ts (so the provider only registers when the user
has configured a custom endpoint — no branching for everyone else).
src/providers/claude.ts — drops ANTHROPIC_AUTH_TOKEN/NO_PROXY
passthrough. Reads ANTHROPIC_BASE_URL from .env, sets a placeholder
ANTHROPIC_AUTH_TOKEN in container env so the SDK includes an
Authorization header for OneCLI to overwrite.
src/providers/index.ts — removes the unconditional import; setup
appends it on demand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
Adds /add-gcal-tool — a sibling of /add-gmail-tool that installs
@cocal/google-calendar-mcp with the same OneCLI stub-file pattern. Skill
applies the Dockerfile + TOOL_ALLOWLIST changes at install time; trunk
stays clean so users who never run the skill don't carry the calendar
MCP in their image.
Dropped the Phase 5 dry-run section since it hardcoded a per-install
image tag slug and duplicated Phase 4's live agent test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of the SKILL.md already contains the Dockerfile + TOOL_ALLOWLIST
edit instructions with an "ALREADY APPLIED" short-circuit. Keeping those
edits out of trunk means users who never run /add-gmail-tool don't carry
the Gmail MCP package in their image.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`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>
Signal adapter source (src/channels/signal.ts + signal.test.ts) now
lives on the `channels` branch alongside all other channel adapters,
per the trunk/channels split documented in CLAUDE.md and CONTRIBUTING.md
("Trunk does not ship any specific channel adapter"). The /add-signal
skill fetches the file from origin/channels like every other channel.
This PR to main therefore carries only:
- .claude/skills/add-signal/{SKILL,VERIFY,REMOVE}.md — the skill itself
- scripts/init-first-agent.ts — unrelated infra fix that benefits any
native-ID channel (Signal, WhatsApp) by skipping the channel-prefix
on platform IDs that already have their own format
The fixed adapter source + tests were pushed to the channels branch in
a parallel commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getAskQuestionRender used to hardcode the card title and option labels
for pending_channel_approvals and pending_sender_approvals in the
DB-access layer, duplicating wording that already lived in the approval
modules. That caused a visible drift between the initial card title —
picked per event in channel-approval.ts ("📣 Bot mentioned in new chat"
vs. "💬 New direct message") — and the post-click render, which
always showed the constant "📣 Channel registration".
Mirror the pattern already used by pending_approvals: add title /
options_json columns on both pending_*_approvals tables via migration
013, have the approval modules write them at creation time, and let
getAskQuestionRender just SELECT.
- Migration 013 ALTERs the two tables to add title + options_json.
- PendingChannelApproval / PendingSenderApproval types and their
create functions grow the two fields.
- channel-approval.ts / sender-approval.ts normalize options once
and pass both title and options_json into the insert.
- getAskQuestionRender drops the hardcoded render objects and reads
the stored values.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Correctness fixes:
- parseSignalStyles now uses a recursive walker so nested styles (e.g.
**bold with `code` inside**) produce correct offsets against the final
plain text. Previous impl recorded styles against intermediate text and
didn't reindex when later passes stripped prefix characters.
- *single-asterisk* maps to ITALIC (was BOLD, divergent from standard
Markdown). _underscore_ also maps to ITALIC.
- EchoCache keys on (platformId, text) so an outbound "hi" to Alice no
longer drops a real "hi" inbound from Bob.
- On TCP socket close, flip adapter connected=false and log a warning so
operators see lost daemon connections instead of silently failing sends.
- signalTcpCheck clears its 5s timeout on success so successful checks
don't leak a setTimeout handle.
Config hygiene:
- Rename SIGNAL_HTTP_HOST/PORT to SIGNAL_TCP_HOST/PORT (transport is TCP
JSON-RPC, not HTTP) and add SIGNAL_CLI_PATH for non-PATH installs.
- Remove unused readFileSync import.
- Log a warning in deliver() when outbound files are dropped (native
adapter doesn't forward attachments to signal-cli yet).
Tests:
- Nested style offset correctness
- *italic* and _italic_ ITALIC mapping
- Cross-recipient echo isolation
- Same-recipient echo still suppressed
- isConnected() flips on socket close
- Outbound-files warn-and-drop path
SKILL.md realigned to the add-telegram / add-whatsapp template: fetches
from the `channels` branch (not a `skill/*` branch), lists pre-flight
idempotency checks, adds Features / Troubleshooting sections. Added
VERIFY.md and REMOVE.md siblings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior instruction told users to append "@openai/codex@${CODEX_VERSION}" to
a single combined `pnpm install -g` block. That block no longer exists on
main — the Dockerfile splits each global CLI (vercel, agent-browser,
claude-code) into its own RUN layer for cache granularity. Update the skill
to add a standalone RUN block for Codex that matches the existing pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the /add-opencode and /add-ollama-provider pattern. Copies the
add-codex SKILL.md from the providers branch onto trunk so the skill is
discoverable without a manual branch copy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>