Remove ALL Payload imports, withPayload wrapper, and (payload)
routes. Blog index + changelog are now static data arrays.
Blog post at /blog/peer-messaging-claude-code is static TSX.
Payload CMS stays as a dev dependency for future local admin
but has zero presence in the production build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Final working pattern: withPayload via require() for build
compatibility, admin page replaced with redirect (no RootPage
import = no React #130), payload packages externalized from
turbopack bundle. Blog/changelog use server-side getPayload().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Turbopack tries to parse esbuild's native binary as JS, causing
build failure. Externalize all Payload-related packages so they
resolve at runtime, not bundled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
withPayload crashes ALL routes with React #130 in standalone
output — even with admin page replaced by redirect. The wrapper
injects a client-side ConfigProvider that fails hydration.
Removed: withPayload wrapper, entire (payload) route group.
Kept: payload.config.ts, migrations, blog/changelog server-side
queries with graceful DB fallback.
Payload admin runs on local dev only (add withPayload back in
next.config when running pnpm dev). Production content via
static TSX pages or future API-based publishing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep withPayload (needed for build compilation) but replace the
admin RootPage with a redirect. The RootPage's ConfigProvider
causes React #130 in standalone output. Blog/changelog use
server-side getPayload() which works fine.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The withPayload wrapper injects a client-side ConfigProvider that
crashes hydration on every route when the Payload admin can't
initialize in standalone output. Blog/changelog pages use server-
side getPayload() which works without the wrapper.
Payload admin at /payload is disabled until standalone server
init is implemented. All user-facing content works.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replicate working cuidecar Payload setup:
- require() instead of ESM import for withPayload
- routes.admin = "/payload" to avoid /admin conflicts
- (payload)/payload/ route group with own layout + importMap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Payload CMS integration crashes the entire production app — the
withPayload wrapper + admin routes break when DB tables don't
exist and the layout conflicts with i18n routing.
Keeping: payload.config.ts, blog/changelog pages with graceful
DB fallback, static blog post page. Payload admin will be added
back once properly integrated with a dedicated route group that
doesn't inherit the main app layout.
The blog post at /blog/peer-messaging-claude-code is static TSX
and works without Payload runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Static TSX page at /blog/peer-messaging-claude-code while Payload
admin is not yet configured in production. Full 1100-word post on
protocol, dev-channels, prompt-injection, and next steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Production containers get DATABASE_URL (postgres) — Payload
creates tables in a 'payload' schema. Local dev falls back to
SQLite file for zero-config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Production has no SQLite — Payload pages now catch connection
errors and render empty state instead of crashing with React #130.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove Payload's /api/[...slug] route that conflicts with existing
/api/[...route]. Blog/changelog pages use Payload's local API.
Includes cli install.ts backup + assertNoMcpLoss guards (from
worktree agent).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Payload CMS v3.81 withPayload() requires Next.js >=16.1.0 for
production turbopack builds. Upgrade resolves the build failure.
Reverts the dev-only withPayload workaround — now loads normally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Payload CMS v3.81 withPayload() injects a turbopack config key
that Next.js 16.0.10 rejects in production builds (needs >=16.1).
Load withPayload only in dev; production gets a pass-through.
Payload admin works locally; production serves blog/changelog
as regular Next.js pages querying the Payload API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js 16.0.10 fails production builds with turbopack config
present (needs >=16.1.0). Gate it behind NODE_ENV !== production.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cover encryptDirect/decryptDirect with three scenarios (happy path,
wrong recipient, tampered ciphertext) and invite link parsing with
round-trip, expiry rejection, and malformed input handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The private setStatus(ConnStatus) conflicted with the public
setStatus("idle"|"working"|"dnd") method, causing TS2393 under strict
typecheck. Rename the private one to setConnStatus and update all
internal call sites.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three surgical edits for credibility:
Hero subheadline: remove WhatsApp/Slack/phone promises (roadmap,
not shipped), replace "reachable from anywhere you are" (vague)
with concrete value prop: E2E encrypted, delivered mid-turn as
<channel> reminders, broker never sees plaintext. Change "Free
and open-source. Forever." → "Open-source CLI. Free during
public beta." to match the pricing section.
Logo bar: remove Vercel/Linear/Stripe/Supabase/Shopify/Figma
(not actual customers). Replace with tech stack labels: Claude
Code, MCP, libsodium, Bun, TypeScript, MIT.
FAQ: fix "Is claudemesh free?" to match beta pricing. Fix "How
do I get started?" to reference the real curl installer instead
of nonexistent npx claudemesh init. Fix "Which Claude Code
versions?" to name actual install + launch flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Landing page showed \`curl -fsSL claudemesh.sh/install | bash\` but
the domain didn't resolve, so anyone copy-pasting got a DNS error.
Ship:
- apps/web/src/app/install/route.ts: GET returns an auditable bash
installer (Node preflight, npm install -g claudemesh-cli, runs
claudemesh install, prints next steps, colored output). No Node
auto-install — fails clean if missing with a pointer.
- apps/web/src/proxy.ts: exclude /install from the i18n matcher so
Next.js returns the shell script unmangled.
- hero.tsx + features.tsx: swap claudemesh.sh → claudemesh.com.
Test: curl http://localhost:3000/install | bash -n → OK.
Content-Type: text/x-shellscript; charset=utf-8.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 6-tier grid was selling features that don't exist yet:
- Pro \$12/mo: dashboard, peer registry, message history (not built)
- Plus \$24/mo: Tailscale mesh (already default), MCP bridge (free),
audit log (not built)
- Team \$99/mo: \"self-hosted broker\" AND \"25 peers\" AND
\"unlimited peers\" — three contradictions in one tier
- Business \$499/mo: multi-region, retention, Slack/Linear (roadmap)
- Enterprise: claimed \"SOC 2 pack\" without certification
Replaced with a single Public-Beta card:
- Free, no card required
- Two columns: Shipping today (verified against source) + Roadmap
v0.2–v0.3 (clearly labeled)
- Promise: \"Beta users keep the free plan for life\"
Non-additive rewrite of a shipped section. Authorized by user
explicitly; required because the prior pricing created refund +
legal risk.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Running \`claudemesh\` with no args now detects install state and
prints context-appropriate guidance: suggests \`install\` if MCP
not registered, \`join\` if no meshes, \`launch\` if ready.
Replaces the static HELP dump with a first-run wizard that meets
users where they are.
Static HELP still available via --help.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three Tier-2 polish commands for debugging + discoverability:
- claudemesh --version / -v: print CLI version (baked from
package.json at build time via Bun JSON import).
- claudemesh status: WS-probe each joined mesh's broker, report
reachability per mesh. Exit 1 if any broker unreachable.
- claudemesh doctor: run 6 preconditions — Node>=20, claude on PATH,
MCP registered, hooks registered, config file parses + chmod 0600,
mesh keypairs validate. Each check has a pass/fail + fix hint.
Exit 0 if all pass.
Help text now leads with version (\"claudemesh v0.1.3 —\").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Additive NEWS entry pointing to the new public repo
github.com/alezmad/claudemesh-cli and the launch command.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds `claudemesh launch [args]` that spawns Claude Code with
--dangerously-load-development-channels server:claudemesh so peer
messages arrive as <channel> system reminders mid-turn instead of
pull-only via check_messages. Windows uses shell:true to resolve
claude.cmd from PATHEXT.
Prints an info banner before spawning that explains the channel's
scope (peer text injection only), the trust model (treat as
untrusted input), and that existing tool-approval prompts remain
the safety net. --quiet skips the banner.
Install output now mentions `claudemesh launch` as the recommended
launch path; plain `claude` still works for pull-only mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The push handler previously fell through to base64-decoding the
raw ciphertext whenever decryptDirect() returned null. For direct
(crypto_box) messages that produces garbage binary which surfaces
as garbled bytes in Claude's <channel> reminder. Limit the base64
fallback to legacy broadcast/channel messages (no senderPubkey),
and emit a clearer "⚠ message from <pubkey> failed to decrypt"
warning when direct decryption fails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same bug as cuidecar/cccb43a: \`overscroll-behavior: none\` applied to
the universal selector killed wheel events on every overflow:hidden
container on the page — hero, demo-dashboard, cta, surfaces, anything
with rounded cards. Combined with the mesh-stream overflow-y-auto
(fixed in 701516b) this was double-trapping the wheel.
Move the rule from \`*\` to \`html\`, change to \`overscroll-behavior-y\`.
Still prevents rubber-band chaining at the document level, but lets
wheel events propagate naturally through nested overflow containers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The demo-dashboard embedded MeshStream with a fixed min-h-[480px] grid
+ overflow-y-auto on the message <ol>. Browsers capture every wheel
event that fires over a scrollable container — so hovering the demo
section froze page scroll until the user moved the cursor off.
Landing demo has only 6 messages, never needs internal scroll. The
fixed viewport only makes sense in the live dashboard where envelope
count can exceed the box.
Added `scrollable?: boolean` prop to MeshStream (default false).
- demo-dashboard (landing): no prop → intrinsic height, no overflow,
wheel events propagate to the page
- live-stream-panel (/dashboard/meshes/[id]/live): scrollable → keeps
the chat-style fixed viewport with scroll
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full parity with claude-peers:
1. Push-injection (the "tap on shoulder" UX)
- MCP server now declares experimental.claude/channel capability
- BrokerClient onPush handlers emit server.notification({
method: "notifications/claude/channel",
params: { content, meta: {from_id, from_name, mesh_slug,
mesh_id, priority, sent_at, delivered_at, kind}}
})
- Claude Code injects each push as <channel source="claudemesh">
system reminder, so the receiver session sees inbound messages
WITHOUT calling check_messages manually
- Updated MCP instructions with the "RESPOND IMMEDIATELY" framing
(adapted from claude-peers)
2. Status hooks in install (default-on, --no-hooks to opt out)
- new apps/cli/src/commands/hook.ts: reads stdin JSON (Claude Code
hook payload), extracts cwd+session_id, POSTs /hook/set-status
to every joined mesh's broker in parallel with process.ppid +
1s timeout per POST. Silent fail, fire-and-forget.
- install.ts: writes to ~/.claude/settings.json registering
`claudemesh hook idle` on Stop + `claudemesh hook working` on
UserPromptSubmit. Idempotent, preserves other hook entries.
- uninstall.ts: removes both hook entries + MCP entry; leaves
unrelated hook/MCP entries alone.
- dedupes by brokerUrl (multiple meshes on same broker → one POST)
3. CLI surface
- new subcommand: `claudemesh hook <status>` (internal, but
exposed so Claude Code can invoke it via the hook shell command)
- `install --no-hooks` for users who want bare MCP registration
- --help updated
Coexistence with claude-peers: both tools register Stop and
UserPromptSubmit hooks, each POSTs to its own broker. Claude Code
fires multiple hooks per event without conflict.
npm version 0.1.0 → 0.1.1 (patch).
Verified:
- install with hooks → 2 entries added to settings.json ✓
- install --no-hooks → "Hooks skipped" ✓
- uninstall → both MCP entry + 2 hook entries removed ✓
- `echo '{...}' | claudemesh hook idle` with no joined meshes →
silent no-op ("no joined meshes, nothing to do") ✓
- MCP initialize response includes experimental.claude/channel ✓
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs caught via devtools on live site:
**1. CSP 'font-src 'self' data:' violation × 3 per landing load.**
BaseLayout was loading Geist + Geist_Mono via next/font/google. In
prod builds Next.js self-hosts those under /_next/static, but the
generated CSS still references `--font-sans: "Geist", …` which some
browsers resolve by re-requesting fonts.gstatic.com. Since we ship
Anthropic Sans/Serif/Mono self-hosted already (/fonts/*.woff2 via
@font-face in globals.css), the Geist dependency was pure overhead.
Removed `next/font/google` imports entirely. Added a `.cm-root`
class on <html> that remaps the Tailwind `--font-sans/--font-mono`
tokens to our `--cm-font-sans/--cm-font-mono` vars — so every
Tailwind `font-sans` / `font-mono` utility now resolves to Anthropic
families. No Google Fonts fetch, no CSP violation.
**2. /pricing 401 on public visit.**
`<Plan>` calls `useCustomer()` → `GET /api/billing/customer` which
needs auth. Unauthed visitor on /pricing → 401 in devtools + wasted
round trip. Gated `useCustomer` on `authClient.useSession()` —
query `enabled: !!session?.user`. Public visitors now skip the fetch
entirely; signed-in users still get their customer record.
**3. Residual "Welcome back! 👋" on /auth/login (both locales).**
Emoji sweep (e91fc80) missed the i18n translation files. Removed 👋
from en/auth.json + es/auth.json login header titles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clickable HTTPS invite URLs replace the raw ic://join/<token> as the
primary share format. Someone receiving a link in Slack now lands on
a friendly page with install instructions, not a dead-end.
Backend:
- createMyInvite returns a new joinUrl field
(https://claudemesh.com/join/<token>) alongside the existing
ic://join/<token> inviteLink and raw token. Schema + Hono route
updated. ic:// scheme stays — CLI parses both.
- New GET /api/public/invite/:token in packages/api/src/modules/public/
(unauthed). Decodes the base64url payload, verifies ed25519
signature against owner_pubkey using the same canonicalInvite()
contract the broker enforces on join, then joins mesh/invite/user
to return the shape needed by the landing page. Does NOT mutate
usedCount — this is a read-only preview.
- Error taxonomy: malformed | bad_signature | expired | revoked |
exhausted | mesh_archived | not_found. Each returned with any
metadata we CAN surface (meshName, inviterName, expiresAt) so the
error page can be specific ("ask Jordan for a new one").
- cache-control: public max-age=30 on valid invites, no-store on
errors (reasons flip as state changes).
Frontend:
- New public route /[locale]/join/[token] (no auth). Server
Component fetches the preview endpoint, branches on valid/invalid,
renders a minimal landing-design-language shell (wordmark header,
clay accents, serif headlines, mono commands).
- Valid-invite view: "You're invited to {meshName}", inviter +
role + member-count lede, install-toggle component.
- Invalid-invite view: per-reason error copy + inviter name when
available + link back to /.
- InstallToggle client component: three-way state
(unknown/yes/no). Asks "first time / already set up?", then shows
either the 3-step install+init+join path with per-step copy
buttons, or the single claudemesh join <token> command for users
who have the CLI. Every code block has copy-to-clipboard.
- Security footer: "ed25519 keypair generated locally, you keep
your keys, broker sees ciphertext only, leave anytime with
claudemesh leave <mesh-slug>".
Invite generator (/dashboard/meshes/[id]/invite):
- QR code now encodes the HTTPS joinUrl instead of ic:// (phone
cameras land on the web page → friendly path).
- Primary CTA copies the HTTPS URL. Secondary "Copy CLI command"
for fast-path users. Footer explanation updated.
CLI coordination note: dispatched to broker/db lane — claudemesh CLI
needs to accept BOTH ic://join/<token> AND
https://claudemesh.com/join/<token> (extract <token> from pathname).
Server side already returns both.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pairs with claudemesh-2's new /join/[token] landing page. Users can
now paste a clickable HTTPS URL instead of the dev-only ic:// scheme.
apps/cli/src/invite/parse.ts — new extractInviteToken() handles
four input formats before handing the raw base64url token to the
existing parseInviteLink pipeline:
- https://claudemesh.com/join/<token> (primary, clickable)
- https://claudemesh.com/<locale>/join/<token> (i18n prefix)
- ic://join/<token> (still supported, dev)
- <raw-token> (last resort: bare base64url)
User-facing strings updated to the HTTPS form:
- cli help: "join <url>"
- install success message
- list (no-meshes) hint
- MCP server "no meshes" error
- README.md primary example
- docs/QUICKSTART.md Path A + Path B
Verified extractInviteToken() on all 4 formats — each returns the
same base64url token → same broker /join lookup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ScrollContainer — the wrapper under every dashboard/admin SidebarInset
— had zero horizontal padding on its scroll child, so pages rendered
edge-to-edge against the viewport. On wide screens content also
stretched to whatever width the sidebar left over (no max-width).
Single-point fix: wrap the scroll child in
<div class="mx-auto w-full max-w-[var(--cm-max-w)] px-4 py-6 md:px-8 md:py-8">
Hits every route under SidebarInset in one change:
- /dashboard
- /dashboard/meshes + /new + /[id] + /[id]/invite + /[id]/live
- /dashboard/invites
- /dashboard/settings (+ billing, security)
- /admin + /admin/users, /organizations, /customers, /meshes,
/sessions, /invites, /audit
px-4 → md:px-8 matches the marketing sections' gutter rhythm.
max-w-[var(--cm-max-w)] (90rem) caps content on ultra-wide.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>