drop the orphaned bridge tier (~600 LoC). client/server/protocol
files deleted; tryBridge had returned null in production for seven
releases since the 1.24.0 mcp shim rewrite stopped opening the
sockets. each verb now has two paths: daemon (with 1.27.3's
auto-spawn) → cold ws.
add per-process daemon policy: --strict (error instead of cold
fallback) and --no-daemon (skip daemon entirely). enforcement at
withMesh so a single chokepoint covers every verb. env equivalents
CLAUDEMESH_STRICT_DAEMON / CLAUDEMESH_NO_DAEMON. flag wins.
net -394 loc; the daemon-up case ships ~600 loc lighter and the
fallback story is one tier simpler. first sprint A drop; per-session
ipc tokens and the wizard refactors follow in 1.29.0+.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
six flags declared on `LaunchFlags` were silently dropped at the CLI
layer — `--role`, `--groups`, `--message-mode`, `--system-prompt`,
`--continue`, and `--quiet`. each was honored inside `runLaunch` if it
arrived, but the four call sites in the entrypoint forwarded a hardcoded
5-key subset.
now forwarded at every entry: bare command, bare invite URL, the
launch/connect verb, and the new workspace launch alias. pure plumbing;
no behaviour change for users who weren't passing these flags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
extend the daemon thin-client surface to two more verb families: state
get/set/list now routes through `/v1/state`, and remember/recall/forget
through `/v1/memory`. same warm-path pattern as 1.25.0 — try the unix
socket first, fall back to the cold ws path when the daemon is absent.
multi-mesh aware (aggregates on read, requires `--mesh` for writes
when ambiguous).
also ships an early `claudemesh workspace <verb>` alias surface — bare
teaser for the 1.28.0 mesh→workspace public rename. no-arg falls
through to launch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the last functional gaps where the MCP tool registry exposed
write verbs the CLI didn't:
- vault set <k> <v> [--type env|file --mount <path> --description ...]
Client-side crypto_secretbox_easy with a fresh symmetric key sealed
to the member's own pubkey via crypto_box_seal — same pattern used
for file shares. Pairs with the existing vault list/delete.
- watch add <url> [--label --interval --mode --extract --notify-on]
Pairs with watch list/remove.
- webhook create <name> — pairs with webhook list/delete.
Cleanup: deletes 22 stub files under apps/cli/src/mcp/tools/* plus
router.ts, middleware/, handlers/ (~120 LoC). These were FAMILY/TOOLS
metadata-only re-exports left over from before the 1.5.0 tool-less
push-pipe flip; nothing imports them. The legitimate MCP surfaces
stay: the inbound <channel> push pipe, mesh skills as prompts and
skill:// resources, and the mesh-service proxy mode.
Released as 1.23.0 on npm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Root --help now lists the daemon subcommand suite (was missing).
- claudemesh daemon (no subcommand) prints a usage block instead of
silently launching the foreground daemon. Adds help|--help|-h aliases.
- SKILL.md gains a "Daemon path (v0.9.0, opt-in, fastest)" section
explaining the runtime, lifecycle, and that it's independent from
claudemesh install.
Released as 1.22.1 on npm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-launch fix: every visible surface already keyed on slug, so
"name" was a parallel string that only existed to confuse users
on rename ("I renamed but nothing visible changed").
Now slug IS the identifier. claudemesh rename <old> <new> is the
whole rename surface. PATCH /api/cli/meshes/:slug body becomes
{ slug } and the route writes both columns to keep them in sync.
Mesh create derives slug from input.name and stores name = slug.
Pickers drop the (parens). The claudemesh slug verb shipped 30
min ago is removed — merged into rename.
The mesh.name DB column stays for now to avoid touching ~25
reader sites; a follow-up migration drops it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slugs are not globally unique (mesh.id is canonical) so the route
only validates the regex and updates the row. CLI refuses a local
collision (two joined meshes sharing a slug would make the picker
ambiguous) and rewrites ~/.claudemesh/config.json on success.
Other peers pick up the new slug on next claudemesh sync.
Server: PATCH /api/cli/meshes/:slug body now accepts { name?, slug? }
— same route, just optional both fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new CLI verbs for the file-sharing surface that already existed
on the broker (HTTP /upload + WS get_file/list_files) but was only
reachable through MCP-style docstrings referencing tools that do
not in fact exist:
claudemesh file share <path> [--to peer] [--message "..."]
claudemesh file get <id> [--out path]
Same-host fast path: when --to resolves to a session on the same
hostname, skip MinIO and DM the absolute filepath. The receiver
reads it off disk directly. No bucket roundtrip, no 50 MB cap.
Falls back to encrypted upload when the peer is remote or --upload
is set.
Routes the same-host DM by session pubkey, not displayName, so
sibling sessions of the same member do not trip the v0.5.1
self-DM guard.
Updates the bundled SKILL.md and the MCP server instructions to
reference the real CLI verbs instead of the fictional share_file()
/ get_file() tool calls.
Also: rename.ts now distinguishes mesh-membership from web-account
auth and points users at claudemesh login + the dashboard rather
than emitting a bare "Not signed in".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zero-install access to the protocol reference: a fresh `npm i -g
claudemesh-cli` user (or someone running the prebuilt binary) can
now `claudemesh skill | claude --skill-add -` without copying any
files into ~/.claude/skills. The skill markdown is embedded into
the CLI bundle at build time via Bun's text-import attribute.
Also replaces two `<> ALL(...)` raw SQL fragments in the dashboard
unread-count queries with drizzle's notInArray() helper — matches
the same fix already applied to /v1/me/topics in the API package.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
closes the "DM looped back to my own inbox" footgun.
what was happening: peer list returns one row per presence,
including the caller's own session AND its sibling sessions.
the cli filtered out the exact-session row but left siblings
unlabeled — copying their pubkey from peer list silently
targeted your own sibling, and the message arrived in "your
own inbox" because the sender was you.
fix is two-part.
(1) peer list — tag rows whose memberPubkey matches the
caller's stable JoinedMesh.pubkey:
● displayName (this session) — the exact session running
the cli call
● displayName (your other session) — sibling session of
your own member
visually identical otherwise; just the marker.
(2) claudemesh send — refuse a target that exactly matches the
caller's own member pubkey on the mesh, with a hint pointing
at --self for the rare intentional sibling-DM case.
both changes additive — existing scripts that pass display
names or other peers' pubkeys behave identically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ships v0.5.0 phase 2.
api: three new aggregator endpoints for the per-mesh subsystems
that didn't have one yet.
- GET /v1/me/tasks — open + claimed by default; ?status=all
surfaces completed (30d window). sorted open > claimed > done.
- GET /v1/me/state — every (key, value) row across the user's
meshes, sorted by recency. ?key=foo filters to one key.
- GET /v1/me/memory?q=... — ilike on content + tags, no q
returns the last 30 days. excludes forgotten rows.
cli (1.16.0): task list, state list, recall now route through
the matching aggregator when --mesh is omitted. --mesh foo
still scopes to one mesh (existing behavior preserved).
with this, every per-mesh read verb in the cli either has a
cross-mesh aggregator or doesn't need one. v0.5.0 substrate is
complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ships v0.5.0 phase 1.
omitting --mesh on these read verbs now routes through
/v1/me/topics and /v1/me/notifications instead of prompting
the user to pick a mesh. behavior preserved for explicit
--mesh foo.
implementation: resolveMeshForMint helper in commands/me.ts
silently picks the first joined mesh for apikey-mint when
flags.mesh is null. /v1/me/* endpoints resolve the user from
the apikey issuer regardless of which mesh issued the key, so
mint location is irrelevant — only the user identity matters.
help text updated to reflect the new default.
phase 2 (task list, state list, memory recall) needs /v1/me/*
aggregator endpoints first; deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ships v0.4.0 phase 5 — final aggregating verb. v0.4.0 substrate
is complete after this.
api: GET /v1/me/search?q=... matches against topic names +
sender display names + v1 message snippets (base64 decode then
ilike). v2 ciphertext matches only on topic/sender — server has
no topic keys. 30-day window on messages, capped at 50 hits per
category.
cli (1.14.0): claudemesh me search <query> renders topic + msg
sections with inline yellow highlighting. min 2 chars; --json
returns the raw response.
web: /dashboard/search adds an autofocused input + mark
highlighting on every match site (topic name, sender, snippet).
sidebar gets a search entry between activity and invites.
roadmap: phase 5 marked shipped, v0.5.0 default-aggregation
behavior added as the natural next track.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ships v0.4.0 phase 4. final aggregating verb after this is
me search (phase 5).
api: GET /v1/me/activity returns topic messages across every
mesh the user belongs to in a 24h default window (?since=iso
override), excluding messages the caller authored themselves.
"what is happening that i missed", capped at 200.
cli (1.13.0): claudemesh me activity prints a condensed feed
with mesh + topic + sender + relative timestamp + snippet (or
[encrypted] for v2 ciphertext).
web: /dashboard/activity clusters consecutive messages from the
same topic into thread blocks for readability. sidebar gains an
activity entry between notifications and invites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ships v0.4.0 phase 3.
api: GET /v1/me/notifications aggregates the mesh.notification
table across every joined mesh in a 7-day window (?since=iso
overrides, ?include=all surfaces already-read). returns sender +
topic + mesh context plus a 240-char snippet for v1 plaintext
messages or raw ciphertext for v2 (the dashboard topic-key cache
decrypts client-side).
cli (1.12.0): claudemesh me notifications — terse unread feed
with @ dot, --all to include read, --since for custom window.
web: /dashboard/notifications mirrors the cli view in card form,
adds a notifications entry to the dashboard sidebar between
topics and invites. each card links straight to the topic chat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ships v0.4.0 phase 2: a cross-mesh topic feed.
api: GET /v1/me/topics aggregates topics across every mesh the
caller belongs to with per-topic unread counts (vs the user's
member-row last_read_at) and last-message timestamps. Sorted by
last activity.
cli (1.11.0): claudemesh me topics renders the feed; --unread
filters to topics with pending reads; --json returns raw.
web: /dashboard/topics ssr's the same view server-side (direct
db queries, no apikey-mint roundtrip) and adds a Topics entry
to the dashboard sidebar between Meshes and Invites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Help text was a wall of monochrome ASCII. Now section headers print
bold-clay, the program title is brand-orange, each verb's syntax is
tinted cyan, and `(alias: ...)` parentheticals are dimmed so they
read as secondary metadata. The styles helper already gates on TTY +
NO_COLOR, so non-interactive output stays unchanged.
Adds .artifacts/specs/2026-05-02-workspace-view.md — the v0.4.0
spec for a per-user virtual workspace that aggregates reads across
all joined meshes while keeping writes mesh-scoped. Roadmap entry
added under v0.3.0.
Adds a reply_to_id column (self-FK on topic_message) plus end-to-end
plumbing so a message can mark itself as a reply to a previous one in
the same topic.
- Schema: 0027_topic_message_reply_to.sql adds reply_to_id with
ON DELETE SET NULL + index for backlink lookup.
- Broker: appendTopicMessage validates parent shares the topic, writes
reply_to_id; topicHistory + topic_history_response surface it; WS
push envelope now carries senderMemberId, senderName, topic name,
reply_to_id, and message_id so recipients have everything they need
to reply without a follow-up query.
- REST: POST /v1/messages accepts replyToId (validated server-side);
GET /messages and SSE /stream emit it per row.
- CLI: \`topic post --reply-to <id|prefix>\` resolves prefixes against
recent history; \`topic tail\` renders an "↳ in reply to <name>:
<snippet>" line above replies and shows a copyable #shortid tag on
every row.
- MCP push pipe: channel attributes now include from_pubkey,
from_member_id, message_id, topic, reply_to_id — the recipient can
thread a reply directly from the inbound notification.
- Skill + identity prompt updated to teach Claude how to use the new
attributes for replies.
Bumped CLI to 1.9.0.
Wire format:
topic_member_key.encrypted_key = base64(
<32-byte sender x25519 pubkey> || crypto_box(topic_key)
)
Embedding sender pubkey inline lets re-sealed copies (carrying a
different sender than the original creator-seal) decode the same
way as creator copies, without an extra schema column or join.
topic.encrypted_key_pubkey stays for backwards-compat metadata
but the wire truth is the inline prefix.
API (phase 3):
GET /v1/topics/:name/pending-seals list members without keys
POST /v1/topics/:name/seal submit a re-sealed copy
POST /v1/messages now accepts bodyVersion (1|2); v2 skips the
regex mention extraction (server can't read v2 ciphertext).
GET /messages + /stream now return bodyVersion per row.
Broker + web mutations updated to use the inline-sender format
when sealing. ensureGeneralTopic (web) also generates topic keys
per the bugfix that landed earlier today; both producers now
share one wire format.
CLI (claudemesh-cli@1.8.0):
+ apps/cli/src/services/crypto/topic-key.ts — fetch/decrypt/encrypt/seal
+ claudemesh topic post <name> <msg> — encrypted REST send (v2)
* claudemesh topic tail <name> — decrypts v2 on render, runs a
30s background re-seal loop for pending joiners
Web client stays on v1 plaintext until phase 3.5 (browser-side
persistent identity in IndexedDB). Mention fan-out from phase 1
already works for both versions, so /v1/notifications keeps
working through the cutover.
Spec at .artifacts/specs/2026-05-02-topic-key-onboarding.md
updated with the implemented inline-sender format and the
phase 3.5 web plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new verbs that wrap the v1.6.x REST surface:
claudemesh topic tail <name> → live SSE consumer with N-message backfill
claudemesh member list → mesh roster decorated with online state
claudemesh notification list → recent @-mentions of you across topics
Each command auto-mints a 5-minute read-only apikey via the WS
broker and revokes on exit, so users don't manage tokens. SSE
client uses fetch + ReadableStream so the bearer stays in the
Authorization header.
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>
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>
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>
- 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>