Universe page aggregates unread topic_message rows per mesh for the
viewing user. Counts messages newer than topic_member.last_read_at
(or all messages if the viewer never opened the topic) and excludes
anything the viewer authored. One JOIN-grouped query, not N+1.
Mesh card surfaces the count as a clay-rounded badge to the left of
the role chip — matches the per-topic badge style on the mesh detail
page so unread is the same visual idiom across the dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PATCH /v1/topics/:name/read upserts topic_member.last_read_at for the
api key's issuing member. The chat panel calls it on mount and on
every inbound SSE message (5s debounce so we don't hammer it).
GET /v1/topics now returns unread per topic — counts messages newer
than last_read_at and not authored by the viewer. Mesh detail page
shows a clay-rounded badge next to each topic name with the count
(99+ ceiling).
AuthedApiKey gains issuedByMemberId so endpoints can attribute
side-effects to the minting member. Required because external api
keys aren't tied to a specific peer member; only dashboard- and
CLI-minted keys carry one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GET /v1/topics/:name/stream opens an SSE firehose, polled server-side
every 2s and streamed as `message` events. Forward-only — clients
hit /messages once for backfill, then live from connect-time onward.
Heartbeats every 30s keep the connection through proxies.
Web chat panel reads the stream via fetch + ReadableStream so the
bearer token stays in the Authorization header (EventSource can't
set custom headers, which would force token-in-URL leaks). Auto-
reconnect with exponential backoff. setInterval polling removed.
Vercel maxDuration bumped to 300s on the catch-all API route so
streams aren't cut at the 10s default.
drizzle migrations/meta/ deleted — superseded by the filename-
tracked custom runner in apps/broker/src/migrate.ts (c2cd67a).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch release on top of 1.6.0:
- Revoke-by-id-prefix bug fix (broker.revokeApiKey now returns
structured status; CLI surfaces not_found / not_unique). Pasting
the 8-char prefix from `apikey list` output now works as users
expect, instead of silently no-op'ing with a misleading "✔
revoked" message. Already deployed to broker.
- whoami falls back to local mesh-config view when no web session
is signed in. Users who joined via invite (and never ran
`claudemesh login`) now see their member ids and pubkey prefixes
per mesh, instead of a "Not signed in" dead end.
- README updated: REST surface lives at claudemesh.com/api/v1/*
(web app), NOT ic.claudemesh.com/api/v1/* (broker). Surfaced
during CLI-only smoke test against prod when curl on the broker
host returned 404.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claudemesh apikey revoke <id> reported success even when the input
didn't match any row in mesh.api_key. The CLI's `apikey list` shows
truncated 8-char prefixes; users naturally paste those; broker did
exact-id match against meshApiKey.id; UPDATE affected 0 rows; old
revokeApiKey returned void so the CLI couldn't tell. Discovered via
end-to-end CLI smoke test against prod (roadmap validation pass).
Three-part fix:
- broker.revokeApiKey now returns
{ status: "revoked"|"not_found"|"not_unique"; id?, matches? } and
accepts either the full id or a unique prefix (>=6 chars). Prefix
matching is bounded to the caller's mesh and only succeeds if
exactly one row matches; ambiguous prefixes return not_unique so
we never silently revoke the wrong key.
- New WSApiKeyRevokeResponseMessage carries the structured status
back to the CLI. Old apikey_revoke_ok type removed before being
released — never shipped to users. The error path is no longer
used for not_found/not_unique cases; the unified response carries
both outcomes.
- CLI's apiKeyRevoke now resolves with { ok, id } | { ok: false,
code, message }. runApiKeyRevoke surfaces the code/message and
exits non-zero on failure (NOT_FOUND for missing, INVALID_ARGS
for ambiguous prefix).
Net effect: pasting `claudemesh apikey revoke vq0fwjdX` now actually
revokes the key whose id starts with vq0fwjdX (or fails loud if 0
or >1 keys match). Verified against prod via the new branch's CLI
binary before commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Web-first owners had no mesh.member row because the broker only ever
created one on first WS hello (CLI flow). The topic chat page server
component requires that row to issue a dashboard apikey
(issuedByMemberId is a FK to mesh.member), so visiting the chat for a
web-only mesh hit notFound() on the owner's own room.
Forward fix: createMyMesh now generates a fresh ed25519 peer keypair,
inserts a mesh.member row with role=admin and dashboardUserId=userId,
and subscribes the owner to the auto-created #general topic as 'lead'.
The peer secret key is intentionally discarded — web users don't sign
anything in v0.2.0 (no DMs, base64 plaintext on topics). If the same
user later runs the CLI, the broker mints a separate member row from
its own keypair; both work for their respective surfaces.
Backfill: apps/broker/scripts/backfill-owner-members.ts walks every
non-archived mesh whose owner has no member row, generates real
ed25519 keypairs via libsodium, inserts the rows in a transaction,
and subscribes each as 'lead' on #general. Already run against prod
— 13 owner rows minted, ddtest verified end-to-end via playwriter
(send → poll → render round-trip ok).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that the filename-tracked runner is in place and prod is bootstrapped,
BROKER_SKIP_MIGRATE=1 is no longer needed. Removed from Coolify env;
the comment is updated to reflect that the flag is a break-glass for
ops, not the steady-state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
drizzle's _journal.json drifted to idx=11 while the file system had 25
.sql files; the prod drizzle.__drizzle_migrations table was further
behind with 3 rows. The runtime migrator silently skipped anything
outside the journal, so every new schema change required psql -f by
hand.
The new runner tracks applied files in mesh.__cmh_migrations
(filename PK + sha256 + applied_at). On startup it bootstraps the
tracking table inline, lists migrations/*.sql lexicographically,
filters out already-applied files, and runs the rest in transaction
order under the existing pg_advisory_lock. SHA mismatches on
already-applied files emit a warning but don't fail (cosmetic edits
are common); production drift detection lives elsewhere.
Bootstrap script at apps/broker/scripts/bootstrap-cmh-migrations.ts
computes file hashes and seeds the tracking table — already run
against prod with all 25 current files registered as applied. Future
deploys pick up only truly new migrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The web chat surface needed a guaranteed landing room — a topic that
exists for every mesh from creation onward so the dashboard always has
somewhere to drop the user. #general is the convention; ephemeral DMs
remain ephemeral (mesh.message_queue) so agentic privacy is unchanged.
Three hooks plus a backfill:
- packages/api/src/modules/mesh/mutations.ts — createMyMesh now calls
ensureGeneralTopic() right after the mesh insert. New helper is
idempotent via the unique (mesh_id, name) index.
- apps/broker/src/index.ts — handleMeshCreate (CLI claudemesh new)
inserts #general + subscribes the owner member as 'lead' in the
same handler.
- apps/broker/src/crypto.ts — invite-claim flow auto-subscribes the
newly minted member to #general as 'member', defensively ensuring
the topic exists if predates this change.
- packages/db/migrations/0024_general_topic_backfill.sql — one-shot
backfill: creates #general for every active mesh that doesn't have
one, subscribes every active member, and marks the mesh owner as
'lead' based on owner_user_id == member.user_id. Idempotent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two UX wins for the v0.2.0 chat surface:
- Mesh cards on /dashboard now show topic count alongside members and
tier ("3 MEMBERS · 2 TOPICS · FREE"). Active topics render in clay,
zero in tertiary. One aggregate query, not N+1.
- Mesh detail page replaces the CLI-hint empty state with an inline
CreateTopicForm. Non-empty topic lists get a compact "+ new topic"
pill in the section header. Server action validates name format
(lowercase letters/digits/dashes, 1-50 chars), inserts via the
unique (meshId, name) index, auto-subscribes the creator as topic
lead, then redirects into the chat.
Sidebar audit — kept platform/manage/dev structure as is. Topics are
mesh-scoped so a top-level "topics" entry would have nothing to land
on without a mesh chosen first. Discoverability lives on the mesh
cards instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit against peer-graph-panel, live-stream-panel, state-timeline-panel,
and resource-panel showed the chat used generic shadcn Card chrome
instead of the established panel pattern. Refactor swaps the wrapper
to the canonical idiom:
- rounded-[var(--cm-radius-lg)] + border-[var(--cm-border)] + bg-[var(--cm-bg)]
- mono header strip with clay-pulse fetch dot, 11px label, 10px metadata
- mono 9px footer status bar (mesh slug · poll cadence · key expiry)
- Anthropic Mono via var(--cm-font-mono) on chrome, sans on message body
- compose textarea uses cm-bg-elevated + cm-border-hover focus state
- error line in cm-fig (#c46686) instead of generic destructive
No behavior change — only chrome. Polling, send path, decode logic
unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New dashboard route at /dashboard/meshes/[id]/topics/[name] gives signed-in
users a thin chat client over the v0.2.0 REST surface. The mesh detail page
now lists topics with one-click links into the chat. Backend layout:
- packages/api/src/modules/mesh/api-key-auth.ts — exports
createDashboardApiKey() that mints a 24h read+send key scoped to a single
topic for the caller's member id. The page server component calls this on
every render and embeds the secret in the props of the client component;
the secret never touches sessionStorage so a tab close = key effectively
abandoned (the row remains until expiresAt).
- apps/web/.../topics/[name]/page.tsx — server component, NextAuth gate,
resolves the user's meshMember.id, mints the key, renders the shell.
- apps/web/src/modules/mesh/topic-chat-panel.tsx — client component, polls
GET /v1/topics/:name/messages every 5s, sends via POST /v1/messages.
Encoding wraps base64(plaintext) into the ciphertext field — matches the
current broker contract until per-topic HKDF lands in v0.3.0.
The mesh detail page gains a Topics section with empty-state copy that
points users at the CLI verb (claudemesh topic create) for now; topic
creation from the web UI is a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v0.2.0 backend cut. Topics, API keys, REST /api/v1/*, and bridge
peers — all in one CLI release. Adds three new verb namespaces:
topic (channel pub/sub), apikey (REST client auth), bridge (cross-mesh
forwarding).
Also pins @claudemesh/sdk as a workspace devDependency so the bridge
implementation is bundled by Bun at build time and doesn't leak into
the npm tarball's runtime deps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A bridge holds memberships in two meshes and relays messages on a
single topic between them. Federation-lite without a broker-to-broker
protocol.
SDK additions:
- Bridge class (start, stop, EventEmitter for forwarded/dropped/error)
- MeshClient.joinTopic / leaveTopic / createTopic methods
- Loop prevention: plaintext hop counter prefix __cmh<n>: with maxHops
default 2; echo guard via senderPubkey == own session pubkey
CLI additions:
- claudemesh bridge run <config.yaml> long-lived process
- claudemesh bridge init prints config template
- Zero-dep YAML parser for the flat bridge config shape
The hop prefix is visible in message bodies — minor wart, fixed in
v0.3.0 by moving loop tracking into broker primitives.
SDK kept as devDependency since Bun bundles it into dist; no impact
on npm publish or runtime resolution.
Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issuance flow over WS for now (REST endpoints come next slice).
Plaintext secret returned ONCE on create — never recoverable.
- broker: 3 WS handlers (apikey_create/list/revoke), wire types in
union, audit log on issuance + revoke
- ws-client: apiKeyCreate/List/Revoke with resolver maps, response
dispatch
- CLI: claudemesh apikey create <label> [--cap a,b] [--topic c,d]
[--expires ISO]; list shows status, scope, last-used; revoke by id
- policy: apikey create + revoke prompt by default (issuing or
disabling a credential is meaningful)
Default capability set is "send,read" — least privilege for unscoped
keys (admin must explicitly opt-in).
Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation for v0.2.0 REST + external WS auth.
Bearer tokens stored as SHA-256 hashes; secrets are 256-bit CSPRNG so
Argon2 would waste cost without security gain.
Adds mesh.api_key table, migration 0023 applied manually to prod, and
helpers: createApiKey, listApiKeys, revokeApiKey, verifyApiKey.
Next slices: CLI apikey verbs and REST endpoints in apps/web router.
Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleSend's pre-flight check rejected #<topicId> sends because the
target wasn't matched by @group / * / pubkey, so it fell into the
"direct" branch and looked for a peer with that pubkey. Topic targets
need their own class — delivery happens via topic_member, not by
matching connected peers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Broker (apps/broker/src/index.ts)
- Unified disconnect/kick handler uses close code 1000 for disconnect
(CLI auto-reconnects) vs 4001 for kick (CLI exits, no reconnect).
- Ban now closes with code 4002.
- Hello handler: revoked members get a specific 'revoked' error with a
'Contact the mesh owner to rejoin' message, then ws.close(4002).
Previously banned users saw the generic 'unauthorized' error.
- list_bans handler returns { name, pubkey, revokedAt } for each
revoked member.
CLI (apps/cli)
- ws-client: close codes 4001 and 4002 set .closed = true and stash
.terminalClose so callers can surface a friendly message instead of
the low-level 'ws terminal close' error. Revoked error in hello is
also captured as a terminal close.
- withMesh catches terminalClose and prints:
4001 → 'Kicked from this mesh. Run claudemesh to rejoin.'
4002 → the broker's 'Contact the mesh owner to rejoin.' message
- kick.ts now exports runDisconnect + runKick with clear hints:
'disconnect' → 'They will auto-reconnect within seconds.'
'kick' → 'They can rejoin anytime by running claudemesh.'
- cli.ts adds 'disconnect' dispatch; HELP updated.
Semantics:
disconnect: session reset, no DB state, auto-reconnects
kick : session ends, no DB state, user must manually rejoin
ban : session ends + revokedAt set, cannot rejoin until unban
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote CLI from 1.0.0-alpha.42 to stable 1.0.0 so
`npm i -g claudemesh-cli` installs the current release without
needing the @alpha dist-tag.
Both dist-tags now point at 1.0.0 — `@alpha` kept as an alias for
continuity so existing docs, install scripts, and scheduled upgrade
commands keep working.
upgrade + doctor commands updated to prefer the `latest` dist-tag
(falling back to `alpha`) and to suggest `npm i -g claudemesh-cli`
without the @alpha suffix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three bugs compounding when multiple peers share a display name:
1. list_peers (MCP + CLI) truncated pubkey to 12 hex chars with an
ellipsis. A truncated pubkey cannot be used as a routing key, so
the caller had no way to disambiguate visually.
2. send_message required the full 64-hex pubkey and refused prefix
input, forcing callers to rely on --json output to get a full key.
3. Name-based resolution returned the first exact match without
filtering the caller's own session — so "send to <my-own-name>"
would bounce against the broker's self-send guard when another
session of the same user was the intended target.
Fixes:
- list_peers now prints 16-char pubkey prefix labelled "pubkey: …"
(MCP) and appends it to CLI output
- send_message accepts any 8–64 hex-char prefix and resolves against
live peer lists across joined meshes; unique match routes, multi-
match returns a disambiguation error listing each candidate's
displayName + pubkey + cwd
- Name matches now skip the caller's own session pubkey; multiple
same-named matches fail loudly with a copy-pasteable pubkey
disambiguation hint instead of silently picking one
- Full 64-char pubkeys without a live match still queue at the
broker (preserves offline-delivery semantics)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /dashboard landing that surfaces meshes and invitations-to-you
in one view. Replaces the simple mesh grid at /dashboard (preserved
at /dashboard/legacy).
Backend additions:
- GET /api/my/invites/incoming — pending_invite rows addressed to
the authed user's email, joined with invite for role + expiry and
user/mesh for display. Unaccepted + unrevoked + unexpired only.
- DELETE /api/my/invites/incoming/:id — dismiss a pending invite
(revokes the pending_invite row only; underlying invite code stays
valid so the inviter can re-send).
Web additions (all under apps/web/src/modules/dashboard/universe/):
- welcome.tsx — editorial serif header with mesh + invite counts
- invitations.tsx — client card with Accept (→ /i/:code claim flow)
and optimistic Decline
- meshes-grid.tsx — hero card + compact grid, linked to mesh detail
- reveal.tsx — fade-up motion matching marketing _reveal.tsx
Styling uses the existing claudemesh design tokens (--cm-clay,
--cm-bg-elevated, Anthropic Sans/Serif/Mono) — nothing redefined.
Onboarding redirect (0 meshes → /meshes/new?onboarding=1) preserved,
now gated on 0 invitations too so users with pending invites still
land on the dashboard.
Sidebar icon switched to Atom for the "universe" concept.
Standalone prototype saved at prototypes/live-dashboard.html for
reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If the arg isn't a URL and matches a mesh already in local config,
print a hint pointing at `launch --mesh <slug>` instead of treating
the slug as an invite code. Avoids the 501 invite_v2_disabled confusion
when users try to "enter" a mesh they already own.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The peers command opens its own WS to each mesh, which briefly appears
as a hostname-PID peer. Filter it out by session pubkey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Broker WS handlers:
- kick: disconnect peer(s) by name, --stale duration, or --all.
Authz: owner or admin only. Closes WS + marks presence disconnected.
- ban: kick + set revokedAt on mesh.member. Hello already rejects
revoked members, so ban is instant and permanent until unban.
- unban: clear revokedAt. Peer can rejoin with their existing keypair.
- list_bans: return all revoked members for a mesh.
Session-id dedup (previous commit): handleHello disconnects ghost
presences with matching (meshId, sessionId) before inserting the new
one. Eliminates duplicate entries after broker restarts.
CLI (alpha.37):
- claudemesh kick <peer|--stale 30m|--all>
- claudemesh ban/unban <peer>
- claudemesh bans [--json]
- Uses new sendAndWait() on ws-client for request-response pattern
over WS (generic _reqId resolver).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a client reconnects with the same session_id before the 90s
stale sweeper runs, the old ghost presence stays in the connections
map. Result: duplicate entries in list_peers for the same Claude
Code instance.
Now: handleHello iterates connections for matching (meshId, sessionId),
closes the old WS, deletes from map, marks disconnected in DB.
One session_id = one presence, always.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backwards compat shim (task 27)
- requireCliAuth() falls back to body.user_id when BROKER_LEGACY_AUTH=1
and no bearer present. Sets Deprecation + Warning headers + bumps a
broker_legacy_auth_hits_total metric so operators can watch the
legacy traffic drain to 0 before removing the shim.
- All handlers parse body BEFORE requireCliAuth so the fallback can
read user_id out of it.
HA readiness (task 29)
- .artifacts/specs/2026-04-15-broker-ha-statelessness-audit.md
documents every in-memory symbol and rollout plan (phase 0-4).
- packaging/docker-compose.ha-local.yml spins up 2 broker replicas
behind Traefik sticky sessions for local smoke testing.
- apps/broker/src/audit.ts now wraps writes in a transaction that
takes pg_advisory_xact_lock(meshId) and re-reads the tail hash
inside the txn. Concurrent broker replicas can no longer fork the
audit chain.
Deploy gate (task 30)
- /health stays permissive (200 even on transient DB blips) so
Docker doesn't kill the container on a glitch.
- New /health/ready checks DB + optional EXPECTED_MIGRATION pin,
returns 503 if either fails. External deploy gate can poll this
and refuse to promote a broken deploy.
Metrics dashboard (task 32)
- packaging/grafana/claudemesh-broker.json: ready-to-import Grafana
dashboard covering active conns, queue depth, routed/rejected
rates, grant drops, legacy-auth hits, conn rejects.
Tests (task 28)
- audit-canonical.test.ts (4 tests) pins canonical JSON semantics.
- grants-enforcement.test.ts (6 tests) covers the member-then-
session-pubkey lookup with default/explicit/blocked branches.
Docs (task 34)
- docs/env-vars.md catalogues every env var the broker + CLI read.
Crypto review prep (task 35)
- .artifacts/specs/2026-04-15-crypto-review-packet.md: reviewer
brief, threat model, scope, test coverage list, deliverables.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Broker (all need redeploy):
- sweepOrphanMessages: DELETE undelivered message_queue rows older
than 7 days; hourly sweep. Stops unbounded growth when a sender
typos a name (queued forever, never claimed).
- Per-member send rate limit: TokenBucket(60/min, burst 10) keyed on
memberId so reconnecting can't bypass. Surfaces as queued=false,
error='rate_limit: ...'.
- Pre-flight size cap: reject at handleSend if nonce+ciphertext+
targetSpec exceeds env.MAX_MESSAGE_BYTES with a clear error
instead of silent WSS frame-level kill.
- No-recipient reject: for direct sends, check any matching peer
is connected BEFORE queueing. Kills the self-send silent drop
(sending to your own pubkey when you only have one session
connected) and typo-to-offline-peer silent drops.
- WSAckMessage.error field added for structured failure reasons.
CLI:
- ws-client ack handler reads msg.queued and msg.error; surfaces
rate_limit / too_large / no_recipient to callers instead of
returning ok:true with a dummy messageId.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When Alice's session-A encrypts a direct message to Bob (target = Bob's
stable member pubkey) and Bob's session-B receives it, Bob has BOTH an
ephemeral session secret key and the member secret key. The old code
only tried session_sk, then silently failed with '⚠ message from
<sender> failed to decrypt' even though the message was valid —
just encrypted to the member key.
Now: try session first, fall back to member on null. Matches the
sender side's choice freedom (encrypt using either key).
Repros when: user opens multiple Claude Code sessions (all use the
same member key but each generates its own session key), and one
session sends to another by display-name resolution which returns
the member pubkey.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a user opens multiple Claude Code instances on one laptop they
all share the same memberPubkey (one identity, one config.json). The
broker was broadcasting each Claude Code start/stop to every OTHER
session of the same user — showing as 'peer agutierrez left / joined'
spam in every active claude terminal.
Now: skip broadcast to presences whose memberPubkey equals the joining
or leaving presence's memberPubkey. Other actual peers on the mesh
still see the event.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs that combined to make Claude's peer-send look successful even
when the recipient didn't exist:
1. resolveClient fell through to 'let the broker try' when a single
mesh was joined and the name didn't match any peer. The broker
queued the message against the literal unknown string, matched no
peer in fan-out, but returned a messageId — so the CLI reported
'✓ lezg → msgId' for a peer that was never there.
Now: refuse to send, list the known peer names.
2. list_peers showed the same pubkey multiple times with different
display_names (one per live session) without hinting that they
were the same member — so Claude treated them as distinct people.
Now: annotate with '[shares key with N other session(s)]' so the
caller understands one pubkey = one identity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Older CLIs sometimes called POST /cli/mesh/create without a pubkey,
and the broker stored the string 'pending' as peer_pubkey on the
owner's mesh.member row. Every subsequent hello from the real CLI
failed the membership lookup silently, leaving the connection in
'reconnecting' forever with no useful log line.
Now: validate pubkey is 64 hex chars before creating the owner
member row. Existing 'pending' rows on prod were patched manually.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- info/inbox commands → unified render.ts
- install route: drop in-memory counter, rely on PostHog + structured logs
- docs: roadmap, CLAUDE.md reflect alpha.31 state
- tests workflow now also builds + smoke-tests the CLI bundle
- homebrew tap bootstrap kit in packaging/homebrew-tap-bootstrap/
(README + copy of the formula template for dropping into the tap repo)
- upstream Claude Code issue draft for rich <channel> UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- apps/cli/ is now the canonical CLI (was apps/cli-v2/).
- apps/cli/ legacy v0 archived as branch 'legacy-cli-archive' and tag
'cli-v0-legacy-final' before deletion; git history preserves it too.
- .github/workflows/release-cli.yml paths updated.
- pnpm-lock.yaml regenerated.
Broker-side peer-grant enforcement (spec: 2026-04-15-per-peer-capabilities):
- 0020_peer-grants.sql adds peer_grants jsonb + GIN index on mesh.member.
- handleSend in broker fetches recipient grant maps once per send, drops
messages silently when sender lacks the required capability.
- POST /cli/mesh/:slug/grants to update from CLI; broker_messages_dropped_by_grant_total metric.
- CLI grant/revoke/block now mirror to broker via syncToBroker.
Auto-migrate on broker startup:
- apps/broker/src/migrate.ts runs drizzle migrate with pg_advisory_lock
before the HTTP server binds. Exits non-zero on failure so Coolify
healthcheck fails closed.
- Dockerfile copies packages/db/migrations into /app/migrations.
- postgres 3.4.5 added as direct broker dep.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
alpha.28-30 binaries all reported 'v1.0.0-alpha.27' from a hardcoded
constant in src/constants/urls.ts — my bump sed only matched
package.json's 'version' key, not the TypeScript literal.
build.ts now reads package.json version and injects it via Bun's
`define` (source-text replacement, equivalent to esbuild --define).
urls.ts reads the injected symbol with a runtime fallback for `bun
src/...` dev mode. Version drift can't recur.
+ peers + status migrated to the render.ts unified renderer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Channel messages now render as '<sender>: <body>' with priority
+ broadcast badges in Claude Code's <channel> reminders, so the inbox
reads as a chat thread rather than bare lines.
[URGENT] alice: deploy is blocking release
bob (broadcast): team sync 15min
charlie: pr #42 lgtm
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CLI source (242 files, ~14k lines) was gitignored during the
earlier cli→cli-v2 reorg so only the published npm package carried it.
That blocks the GitHub Actions release workflow (release-cli.yml),
which clones the repo fresh on each runner and needs the source to
compile binaries via `bun build --compile`.
Moves the gitignore from root-level to `apps/cli-v2/.gitignore` with
only the usual build artefacts excluded (node_modules, dist, .turbo,
.cache). Source is now in git at apps/cli-v2/src/.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- .github/workflows/release-cli.yml: build self-contained binaries via
`bun build --compile` for darwin/linux/windows × x64/arm64 on every
cli-v* tag, attach to GitHub Release with SHA256SUMS, auto-bump the
homebrew tap on non-prerelease versions.
- packaging/homebrew/claudemesh.rb.template: formula template for the
homebrew-claudemesh tap.
- packaging/winget/claudemesh.yaml.template: winget manifest template.
- /install script now detects absence of Node and downloads the
platform-appropriate binary from the GitHub Release, installs to
~/.claudemesh/bin, and shims into ~/.local/bin — zero Node required.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /install shell script now points users at `claudemesh <invite-url>`
(one step) instead of the split join+launch
- InstallToggle first-time panel shows single copy-block with
install+launch on the same line
- Also advertises url-handler install and shell completions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Email (broker):
- Rebrand mesh-invitation.tsx to match site (clay accent #d97757,
cream fg, Anthropic Serif/Mono, dark bg). Mesh glyph in header.
- Hero CTA links to the /i/short URL landing page.
- Single one-liner 'npm i -g claudemesh-cli && claudemesh launch --join URL'
so new users copy once, paste once, done.
Web InstallToggle:
- Replace two-step numbered list with single one-liner in the first-time
panel. Reduces copy/paste ops from 2 to 1 and stops prescribing
'YourName' as a literal (CLI now defaults to $USER).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the plain-text invite email with a standalone react-email
template (apps/broker/src/emails/mesh-invitation.tsx) using
@react-email/components + Tailwind. Rendered on demand in
handleCliMeshInvite and sent as both HtmlBody and TextBody via
Postmark (or html+text via Resend).
Self-contained — no dependency on @turbostarter/email, i18n, or ui
packages. Adds react, react-dom, @react-email/components, @react-email/render
to broker deps. Enables tsconfig jsx: react-jsx and .tsx includes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>