Corepack with no version pin pulls latest pnpm (currently 11.0.8), which
silently stops honoring `only-built-dependencies[]=` in `.npmrc` for
global installs. The allowlist file ends up correctly written but
ignored, so:
- `@anthropic-ai/claude-code`'s postinstall — which downloads the
platform-native Claude binary — never runs. Agents then crash at
runtime with "claude native binary not installed... postinstall did
not run."
- `agent-browser`'s postinstall, which chmods the linux-arm64 binary,
is also skipped, so the binary fails with EPERM the first time it's
invoked.
Pin the container's pnpm to 10.33.0 (the same version host's
package.json already pins via `packageManager`). Keep the two in
lockstep so a host bump triggers a deliberate container bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace "full E.164, e.g. +15551234567" with plain-language guidance
mirroring the WhatsApp setup card: "start with + and your country code,
no spaces or dashes" plus a worked example. "E.164" is the technical
name for the format and means nothing to non-telecom users; the
explanation it stands in for is one sentence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Teams setup is 6+ Azure steps over 30+ minutes. Today, every
"Done / Stuck / Show again" gate forces continuation; the only escape
is Ctrl-C, which kills setup entirely. Add a fourth option at each gate
that returns to the channel picker so a stuck operator can pick a
different channel without losing the rest of setup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first-keyword check (`WITH` → SELECT path) was wrong for CTEs that
precede mutations (e.g. `WITH stale AS (...) DELETE FROM t WHERE ...`).
These would be routed through `db.prepare().all()` instead of executing
the mutation.
Use better-sqlite3's `stmt.reader` property, which asks SQLite's own
parser whether the statement returns data. Single mutations go through
`stmt.run()`; compound statements (which `prepare()` rejects) fall back
to `db.exec()`.
Add a regression test for WITH...DELETE.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Setup deliberately avoids the sqlite3 CLI (`setup/verify.ts:5` calls
this out: "Uses better-sqlite3 directly (no sqlite3 CLI)") and never
installs or probes for the binary. Despite that, 13 skills shelled out
to `sqlite3 ...` directly, breaking on hosts where the CLI isn't
preinstalled — the same root cause as #2191 but spread across the
skill surface.
Add `scripts/q.ts`, a ~30-LOC wrapper over the `better-sqlite3` dep
that setup already installs and verifies. Default output matches
`sqlite3 -list` (pipe-separated, no header) so existing skill text
reads identically — only the binary changes. SELECT/WITH queries go
through `db.prepare().all()`; everything else (INSERT/UPDATE/DELETE,
including compound statements) goes through `db.exec()`.
Migrate every in-tree caller:
- 17 hardcoded invocations across 8 SKILL.md files (init-first-agent,
add-deltachat, add-signal, add-emacs, add-whatsapp, add-ollama-provider,
debug, add-parallel) plus add-deltachat/VERIFY.md.
- `manage-channels/SKILL.md` shows canonical SQL but never prescribed
a tool, so the assistant defaulted to `sqlite3` and silently failed.
Add a one-line wrapper hint above the SQL block.
- `migrate-v2.sh` schema/count probes (was the original #2191 case).
Replace `.tables` with `SELECT name FROM sqlite_master`.
- Document the wrapper convention in root `CLAUDE.md` under "Central DB".
Add `scripts/q.test.ts` with 6 vitest cases covering both modes,
NULL rendering, empty-result, compound mutations, and arg validation.
Wire `scripts/**/*.test.ts` into `vitest.config.ts`.
Out of scope (flagged for follow-up):
- `debug` and `add-parallel` still reference the v1-only path
`store/messages.db`. Routing through the wrapper now produces a
cleaner "no such file" error, but the surrounding sections are
v1-era throughout — a v1-content cleanup is its own PR.
- `cleanup-sessions.sh` is being addressed in #1889 (different style,
hard-fail rather than wrap); left untouched here to avoid stepping
on that author's work.
Closes#2191.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move the "Get started: …" URL above the numbered instructions and
render it in bright white so it pops against the brand-cyan body.
(Headless-only — interactive runs still auto-open the URL in a
browser, no card line.)
- Group the OAuth scope list vertically by family (im, channels,
groups, chat, users, reactions) instead of one comma-run wall.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The skill's "Assess Current State" step said only "query agent_groups,
messaging_groups, ..." without specifying columns. The `register` CLI
takes `--assistant-name "<name>"` (mentioned three times in the same
SKILL.md), but the schema column is `name`, not `assistant_name` — and
the SKILL.md never linked the two.
When the agent had to compose a SELECT against `agent_groups` from the
SKILL.md vocabulary alone, it extrapolated `--assistant-name` into a
column name and produced:
SELECT id, folder, assistant_name FROM agent_groups;
-> Error: in prepare, no such column: assistant_name
Replace the prose pointer with canonical SQL queries that match the
real schema. The `name AS assistant_name` alias preserves the familiar
term in the agent's output.
Verified locally as a drop-in: `/manage-channels` runs clean from end
to end with this version, no further inference needed.
Closes#2289
SQLite TIMESTAMP columns store UTC without a zone marker. `Date.parse`
treats timezoneless ISO strings as local time, so on any non-UTC host
every claim and processAfter looks (TZ offset) hours stale. That makes
fresh claims trip the kill-claim path on the first sweep tick — every
container gets killed within seconds of spawn.
Two affected sites in host-sweep.ts:
- decideStuckAction reads claim.status_changed and computes claimAge.
On a TZ=Europe/Madrid host (UTC+2), a claim made 5s ago looks
7205s old and exceeds CLAIM_STUCK_MS (60s).
- The orphan retry loop reads msg.processAfter and skips messages
rescheduled into the future. On the same host, future timestamps
look (TZ offset) hours in the past, so the skip is missed and
tries gets bumped on every tick.
Fix: introduce parseSqliteUtc(s) which appends "Z" only when no zone
marker is present, then call it from both sites. Behavior under
TZ=UTC is unchanged.
Verified on a production v2 install on TZ=Europe/Madrid: with the
patch applied, an idle container survived 30+ minutes without being
killed (previously: killed within 60s of spawn).
Tests: 5 new cases covering the bare/Z/+offset/invalid input matrix
and a TZ-independence check. All 19 host-sweep tests pass and tsc
clears against main.
migrate-v2.sh probes ${ONECLI_URL_CHECK}/health (with ONECLI_URL_CHECK
defaulting to http://127.0.0.1:10254, the OneCLI web port). That path
returns 404, so the detection branch never matches an already-running
OneCLI instance and the script falls through to the install path.
The web app's health endpoint is /api/health
(apps/web/src/app/api/health/route.ts) and has been since the OneCLI
repo was made public. /health was never exposed by the web on :10254
nor by the gateway on :10255 (the gateway uses /healthz).
Verified against a running OneCLI v1.21.0:
GET :10254/api/health -> 200 {"status":"ok","version":"1.21.0",...}
GET :10254/health -> 404 (Next.js fallback HTML)
GET :10255/healthz -> 200
GET :10255/health -> 400 (gateway parses non-/healthz as CONNECT)
Closes#2285
PR #2259 (Baileys v6→v7) was merged into the channels branch instead of
main. PR #2260 was merged into main 28s later assuming v7 was already
in place. The v6 pin survived in three sites while the WhatsApp adapter
copied from origin/channels at install time was already on the v7 LID
API, breaking every fresh migrate-v2.sh run at 2c-install-whatsapp with
TS errors on remoteJidAlt/participantAlt/lid-mapping.update.
Bumps the pin to 7.0.0-rc.9 (the version v1 has been running on for
months) in:
- setup/install-whatsapp.sh
- setup/add-whatsapp.sh
- .claude/skills/add-whatsapp/SKILL.md (install instruction)
package.json + pnpm-lock.yaml are not touched here — install-whatsapp.sh
mutates them at runtime via pnpm install with the corrected pin.
Closes#2283
Today's copy says "Check that signal-cli is installed (we'll guide
you if not)" but the auto-install PR (#2281) makes that misleading —
we don't guide, we just install. Update the intro list to match what
will actually happen, and add a "no input needed for any of it" lead
so users know to expect a hands-off run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a user picks Signal in setup and signal-cli isn't on PATH, today
NanoClaw bails with a GitHub releases link and tells them to re-run.
That's a hard wall for non-technical users — GitHub releases pages
are intimidating, and the Linux native build / Java decision isn't
obvious.
Replace the bail-out with a real install: a new install-signal-cli.sh
script that does `brew install signal-cli` on macOS or downloads the
native Linux release into ~/.local/bin (no Java, no sudo). Wired into
ensureSignalCli with a spinner; probe again after, fall back to the
original manual-install copy if anything fails.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
openInboundDb() always opened /workspace/inbound.db which doesn't exist
in CI. In test mode, return a thin wrapper over the in-memory singleton
that delegates prepare/exec but no-ops close(), so callers' try/finally
cleanup doesn't destroy the shared DB mid-test.
One flag (_testMode), no monkey-patching, no saved-close bookkeeping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test wrapper forwards the in-memory outDb as the writable handle,
avoiding the filesystem reopen that fails in CI. The function stays
private — the optional writableOutDb param is an internal detail, not
a public API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WhatsApp's mobile UI calls the menu "You" on iOS and "Settings" on
Android (depending on platform/version). Both QR-scan and pairing-code
captions only mentioned "Settings", so iOS users had to figure out the
iOS-specific path on their own.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>