claudemesh send <16-hex-prefix> would ack with sent to <prefix> (daemon)
but the recipient never received the message. Broker pre-flight and
the drain query both exact-match on full 64-char pubkey, so a prefix
queued successfully but no recipient drain ever fetched the row.
Sender saw sent, recipient saw nothing — silent drop.
Fix: CLI resolves any hex prefix (4-63 chars, not full 64) to the
full pubkey via the daemon peer list before submitting. Outcomes:
- unique match: canonicalize and continue
- no match: clear error + list of online peer display names
- multiple: clear error + candidate list + hint to lengthen prefix
The 16-hex prefix shown in peer list rows is now safe to paste
straight into claudemesh send.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 1.31.4 the human renderer surfaced role and groups, but launched-
session LLMs still dropped them when they called peer list --json and
built their own tables.
- Top-level role field. The broker returns role nested under
profile.role; the CLI now lifts it to a top-level role field at
parse time so it is the second-most-visible JSON field after
displayName. profile.role is preserved.
- Updated claudemesh skill SKILL.md peer-list section with the full
JSON shape (memberPubkey, sessionId, role, profile, isSelf,
isThisSession) plus explicit guidance to render role + groups in
any peer table inside a launched session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claudemesh peer list now surfaces each peer's profile-level role
(set via claudemesh profile) and any joined groups inline next to
the display name, e.g.
● mou [role:lead, @flexicar:reviewer, @oncall] (ai) · 0d215762…
When both are empty, an explicit footer is added so absence is
unambiguous:
● peer [...]
role: (none) groups: (none)
JSON output is unchanged — the broker has been returning profile
and groups all along, only the human renderer was missing the role.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1.31.2 published with the right code change (DAEMON_PATHS no longer
follow CLAUDEMESH_CONFIG_DIR) but a stale baked-in VERSION constant
because the build ran before the version bump. Same fix, rebuilt
cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real production bug observed in 1.31.0 / 1.31.1: every CLI verb from
inside a claudemesh launch-spawned session printed
[claudemesh] warn service-managed daemon not responding within 8000ms
even when the launchd-managed daemon was healthy and answering
direct UDS probes in 10ms.
Root cause: claudemesh launch exports CLAUDEMESH_CONFIG_DIR to a
per-session tmpdir so joined-mesh state and the IPC session token
stay isolated. DAEMON_PATHS read from the same env, so inside a
launched session the CLI looked for daemon.sock at
/var/folders/.../claudemesh-XXXX/daemon/daemon.sock — which never
exists. The CLI declared the daemon down, fell into the service-
managed wait branch, and timed out.
The daemon is a per-machine singleton serving every session; its
files live at ~/.claudemesh/daemon/ regardless of overlays. Pin
DAEMON_PATHS.DAEMON_DIR to that location. New CLAUDEMESH_DAEMON_DIR
override is preserved for tests and multi-daemon dev setups.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1.31.0 introduced a session reaper that called execFileSync(ps) once
per registered session every 5s. With many sessions registered, the
daemon's event loop stalled for hundreds of ms — long enough that
incoming /v1/version probes from the CLI timed out against a healthy
daemon and the new service-managed warning fired.
Fix:
- getProcessStartTime is now async (execFile + promisify); never
blocks the event loop
- New getProcessStartTimes(pids) issues one batched ps for all
survivors instead of N separate forks. Sweep cost is fixed
regardless of session count.
- registerSession stays sync; start-time capture is fire-and-forget
- reapDead is now async; the setInterval wrapper voids it so a
rejected sweep cannot crash the daemon
Behavior is otherwise unchanged from 1.31.0: same 5s cadence, same
PID-reuse guard semantics, same broker-WS teardown via the registry
hook. 83/83 tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The default body-max-length=100 was firing a warning on every
substantive commit because 100 chars total can't fit a real changelog
message. Disabled (level 0). body-max-line-length bumped to 200 so
long URLs / paths / pasted errors don't trip a warning that adds
nothing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three operability fixes for users running the daemon under launchd or
systemd.
PID-watcher autoclean
=====================
The session reaper already dropped registry entries with dead pids on
a 30s loop, but had two real-world gaps:
- 30s sweep let stale presence linger on the broker for half a minute
- bare process.kill(pid, 0) trusts a recycled pid; a registry entry
could survive its real owner's death whenever the OS rolled the
pid number forward to a new program
Process-exit IPC from claude-code is best-effort and skipped on
SIGKILL / OOM / segfault / panic, so it cannot replace the sweep.
Fix:
- New process-info.ts captures opaque per-process start-times via
ps -o lstart= (works on macOS and Linux, ~1 ms per call)
- registerSession stores the start-time alongside the pid
- reapDead drops entries when pid is dead OR start-time changed
since register
- Sweep cadence 30s -> 5s
- Best-effort fallback to bare liveness when start-time capture
fails at register time
Registry hooks already close the per-session broker WS on
deregister, so peer list rebuilds within one sweep of any session
exit.
Service-managed daemon: no more "spawn failed" false alarms
===========================================================
After claudemesh install (which writes a launchd plist or systemd
unit with KeepAlive=true), users routinely saw
[claudemesh] warn daemon spawn failed: socket did not appear
within 3000ms
even when the daemon was running fine. Two contributing causes:
1. Probe timeout was 800ms — the first IPC after a launchd-driven
restart can take longer (SQLite migration + broker WS opens) and
tripped it. Bumped to 2500ms.
2. On a failed probe the CLI tried its own detached spawn, which
collided with launchd's KeepAlive restart cycle (singleton lock
fails, child exits) and we'd then time out polling for a socket
that was actually about to come up.
Now: when the launchd plist or systemd unit exists, the CLI does not
attempt a spawn. It waits up to 8s for the OS-managed unit to bring
the socket up. New service-not-ready state distinguishes "OS hasn't
restarted it yet" from "we tried to spawn and it failed".
Install verifies broker connectivity, not just process start
============================================================
Previously install ended once launchctl reported the unit loaded —
a daemon that boots but cannot reach the broker (blocked :443,
expired TLS, DNS, broker outage) only surfaced on the user's first
peer list or send.
/v1/health now includes per-mesh broker WS state. install polls it
for up to 15s after service boot and prints either "broker
connected (mesh=...)" or a warning naming the meshes still in
connecting state, with a hint at common causes.
The verification is best-effort and does not fail the install — it
just surfaces the issue early.
Tests
=====
4 new vitest cases cover the reaper paths: dead pid, live pid plus
matching start-time, live pid plus mismatched start-time (PID
reuse), and the no-start-time fallback. 83 of 83 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claudemesh install was baking --mesh <primary> into the launchd plist /
systemd unit, locking the daemon to a single mesh and contradicting
1.26.0's multi-mesh design. users with >1 joined mesh fell off the
daemon path on every non-primary verb (cold-WS fallback, peer list
returning all meshes because the server-side filter ran against zero
attached state, "daemon spawn failed: socket did not appear" from
launched sessions in sibling meshes).
now: meshSlug is optional in InstallArgs; claudemesh install omits it
so the unit runs `claudemesh daemon up` with no flag, which attaches
to every joined mesh. `claudemesh daemon install-service --mesh <slug>`
is preserved as opt-in for single-mesh hosts and CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
two install-path fixes that bit on first 1.30.0 upgrade:
- pin node by absolute path in launchd plist / systemd unit. shebang's
/usr/bin/env node resolved against the service environment PATH and
picked up system Node 22.x, which lacks node:sqlite (experimental)
→ daemon died with ERR_UNKNOWN_BUILTIN_MODULE. process.execPath now
goes first, so the daemon always runs under the same Node that ran
claudemesh install.
- tear down the old daemon before bootstrapping. claudemesh install on
a machine with an already-running daemon hit Bootstrap failed: 5:
Input/output error (launchctl refuses to re-bootstrap a loaded unit
+ old daemon held the singleton lock). Now we run launchctl bootout
(systemd: systemctl --user stop) first, plus SIGTERM to any orphan
pid in daemon.pid, so subsequent installs replace cleanly.
both fixes apply to darwin and linux paths. windows path is unchanged
— it doesn't have a service-install today (daemon-install-service
errors with "unsupported platform" on win32).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- broker-actions: msg-status section header used out-of-scope `id`
variable; was a real bug (renders "message undefined…" on the JSON
path). Fixed to use the in-scope lookupId.
- exit-codes: add IO_ERROR (10) — referenced in three places by
platform-actions but never declared.
- types/text-import.d.ts: declare wildcard `*.md` module so Bun's
text-import attribute used by skill.ts typechecks.
- ipc/server: cast PeerSummary/SkillSummary through unknown before
spreading into Record<string, unknown>.
- mcp/server: typed JSON.parse for SSE events.
- bridge/daemon-route: import path with .ts → .js (esm).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
paid down the broker's accumulated type debt. zero behavioral changes,
purely type-system tightening:
- broker.ts: row extraction helper for postgres-js result vs pg shape;
findMemberByPubkey defaultGroups null-coalescing.
- env.ts: zod default ordered before transform (zod v4 ordering).
- index.ts: typed JSON.parse for the tg/token, upload-auth, file-upload,
member patch and mesh-settings handlers; export SelfEditablePolicy
from member-api; added bodyVersion to WSSendMessage; added the
disconnect/kick/ban/unban/list_bans message types to WSClientMessage;
String(key) cast for neo4j record symbol-typed keys.
- jwt.ts, paths.ts, telegram-token.ts: typed JSON.parse results.
- service-manager.ts: typed package.json + MCP JSON-RPC reader.
- telegram-bridge.ts: typed WS message handler; missing log import;
null-tolerant BridgeRow + skip rows missing memberId/displayName;
typed e in catch.
- types.ts: bodyVersion on WSSendMessage, manifest on WSSkillData,
five new admin message types (kick/disconnect/ban/unban/list_bans).
- packages/db/server.ts: drizzle constructor positional args + scoped
ts-expect-error for the namespace-bag schema generic mismatch.
apps/broker/src/types.ts will eventually want a real audit pass to
catch every WS verb and surface the orphans, but this clears the path
for 1.30.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
per-session presence is small and uncomplicated enough that a rollback
flag isn't load-bearing. backwards compat is already covered at the
protocol layer — older brokers reply unknown_message_type to
session_hello and the SessionBrokerClient marks itself closed for that
mesh, which is the same outcome the flag would have given. removing
the flag, the helper, and the conditional from the registry hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flips CLAUDEMESH_SESSION_PRESENCE default to ON. With the broker side
already shipped (the session_hello handler from earlier in this sprint
A wave), every claudemesh launch now gets its own long-lived broker
presence row owned by the daemon and identified by a per-launch
ephemeral keypair vouched by the member's stable key. Two sessions in
the same cwd finally see each other in peer list — the symptom users
have been hitting since 1.28.0 dropped the bridge tier.
Bumps roadmap: 1.30.0 = presence (was queued for 1.30/wizard); the
launch-wizard refactor moves to 1.31.0, setup wizard to 1.32.0, the
mesh→workspace rename to 1.33.0. Verification smoke documented in the
1.30.0 changelog entry.
Rollback: CLAUDEMESH_SESSION_PRESENCE=0 (also accepts "false"/"off").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claudemesh launch now also generates a per-launch ed25519 keypair and a
parent-vouched attestation (12h TTL), included in the body of POST
/v1/sessions/register under body.presence. The daemon stores it on
SessionInfo and, with CLAUDEMESH_SESSION_PRESENCE=1, opens a long-lived
broker WS so the session has its own presence row.
Also fixes a latent 1.29.0 bug: claudeSessionId was referenced before
its const declaration, hitting the TDZ → ReferenceError silently
swallowed by the surrounding catch. Net: the IPC session-token
registration has been failing every launch since 1.29.0, falling back
to user-level scope for every session. Hoisted the declaration up so
the registration actually runs.
The presence payload is forward-compat: older daemons ignore unknown
body fields, so 1.30.0 CLIs work fine against unupgraded daemons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
whoami --json exits with EXIT.AUTH_FAILED (=2) when not signed in.
The JSON output is the contract under test, valid regardless of exit
code — execSync was throwing on exit 2 so the assertion never ran.
Switch to spawnSync, accept {0,2}, parse stdout independently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
daemon-side half of 1.30.0 per-session broker presence. behind
CLAUDEMESH_SESSION_PRESENCE=1 (default OFF this cycle so the broker
side bakes before the flag flips).
- SessionBrokerClient (apps/cli/src/daemon/session-broker.ts) — slim
WS that opens with session_hello, presence-only, no outbox drain.
- session-hello-sig.ts — signParentAttestation (12h TTL, ≤24h cap) and
signSessionHello, mirroring the broker canonical formats.
- session-registry: optional presence field on SessionInfo;
setRegistryHooks for onRegister/onDeregister callbacks. Hook errors
are caught so they can never throttle registry mutations.
- IPC POST /v1/sessions/register accepts the presence material under
body.presence (session_pubkey, session_secret_key, parent_attestation).
Older callers without it stay scoped + supported.
- run.ts wires the registry hooks: on register, opens a SessionBrokerClient
for the matching mesh; on deregister (explicit or reaper), closes it.
Shutdown closes any remaining session WSes before the IPC server.
8 new unit tests cover registry lifecycle (replace/throw/presence
roundtrip) and signature canonical-bytes verification against libsodium.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 1.30.0 daemon-multiplexed presence flow needs a way for the daemon
to open a WS keyed on a per-launch ephemeral pubkey. This commit adds:
- WSSessionHelloMessage in types.ts (additive — older clients still use
WSHelloMessage; older brokers reply with unknown_message_type so newer
clients can fall back).
- handleSessionHello in index.ts: validates parentAttestation (TTL ≤24h,
ed25519 by parent), session signature (skew + ed25519 by session),
parent membership in mesh.member, and parentMemberId/pubkey coherence.
- Inserts a presence row keyed on sessionPubkey but member_id from the
parent — member-targeted operations (revocation, send-by-member-pubkey)
keep working unchanged.
- Broadcasts peer_joined to ALL siblings in the mesh, including the
same-member ones (the regular hello path skips those to avoid self-
spam, but session_hello explicitly wants sibling visibility).
Behavior parity tests will land alongside the daemon SessionBrokerClient.
The unit tests added in the previous commit cover the crypto layer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the crypto primitives the 1.30.0 per-session broker presence flow
needs: canonicalSessionAttestation/canonicalSessionHello bytes, and
verifySessionAttestation/verifySessionHelloSignature with TTL bounds
(≤24h) plus standard ed25519 + skew checks.
10 unit tests cover the hostile cases — expired attestation, over-TTL,
wrong-key signing, tampered fields, and the "attacker captured the
attestation but doesn't hold the session secret key" scenario.
No wire changes yet — types and dispatch land in the next two commits.
Spec: .artifacts/specs/2026-05-04-per-session-presence.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
records the design for daemon-multiplexed broker presence — every
launched claude session gets its own long-lived presence row owned
by the daemon, identified by a per-launch ephemeral keypair vouched
by the member's stable keypair.
resolves the "two sibling sessions can't see each other in peer list"
gap that surfaced when the bridge tier was deleted in 1.28.0. covers
state machine, broker session_hello handler, parent-attestation
signing, ipc route extension, sequencing (broker first, daemon
flagged, cli third), compat with older builds, and verification
smoke.
~440 loc estimate across cli + daemon + broker. queued for 1.30.0
alongside the launch-wizard refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
extend the v0.9.x section with a new "v1.26.0 → v1.29.0 — sprint A
toward v2" block listing what each release delivered. trim the
v2.0.0 section to just the remaining HKDF identity work; everything
else from the original v2 spec is now shipped.
queue 1.30.0 (launch wizard), 1.31.0 (setup wizard), 1.32.0 (full
workspace rename) as the explicit remaining items before HKDF
ships as 2.0.0 in its own sprint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
every claudemesh launch-spawned session now mints a 32-byte random
token, writes it under tmpdir (mode 0600), and registers it with the
daemon. cli invocations from inside that session inherit
CLAUDEMESH_IPC_TOKEN_FILE in env, attach the token via Authorization:
ClaudeMesh-Session <hex>, and the daemon resolves it to a SessionInfo.
server-side: every read route that filters by mesh now uses meshFromCtx —
explicit query/body wins, session default fills in when missing. write
routes follow the same pattern.
cli-side: peers.ts (and other multi-mesh-iterating verbs in future)
prefers session-token mesh over all joined meshes when the user didn't
pass --mesh explicitly.
backward-compatible in both directions — tokenless callers behave
exactly as before. registry is in-memory; daemon restart loses it but
the 30s reaper handles dead pids and most callers re-register on next
launch.
verified end-to-end: peer list with token returns 4 prueba1 peers,
without token returns 3 meshes' peers (aggregate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
every daemon-routed verb now probes the ipc socket via /v1/version
(instead of trusting existsSync), cleans up stale sock/pid files left
by a crashed daemon, and auto-spawns a detached `claudemesh daemon up`
under a file-lock when the daemon is down. polls for liveness up to a
budget (3s for ad-hoc verbs, 10s for launch) before falling through to
cold path.
includes a per-process result cache (script doing 50 sends pays spawn
cost at most once), a 30s recently-failed marker (no thundering-herd
retries on crash-loop), a spawn-lock (concurrent invocations share one
attempt), and a recursion guard env var (nested cli calls inside the
daemon process skip auto-spawn).
fixes the stale-socket bug where launch's ensureDaemonRunning returned
early on a left-over socket file from a crashed daemon, silently
breaking the spawned claude session's mcp shim.
deferred to 1.28.0: --strict / --no-daemon flags, lazy-loading of
cold-path code, per-session ipc tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
adds a kitchen-sink "every flag set explicitly" recipe under
wizard-free spawn templates, with a per-position annotation table.
agents copy this verbatim instead of stitching flags from the table
when spawning unattended sessions.
corrects two stale items: --system-prompt forwards to claude
--system-prompt (not --append-system-prompt), and -q is currently a
no-op (only --quiet is wired).
flags the 1.27.1 cutoff: all twelve launch flags are only end-to-end
wired from that version on; older builds silently dropped half of them.
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>
The 1.26.0 step that finally delivers ambient mode for multi-mesh
users. Daemon holds Map<slug, DaemonBrokerClient>; one process, one
PID per user, all your meshes online concurrently.
run.ts: claudemesh daemon up with no --mesh attaches to every joined
mesh from config. --mesh <slug> still scopes to one (legacy mode).
The daemon_started log line reports meshes: [...] instead of mesh.
drain.ts: dispatches each outbox row to the broker keyed by row.mesh
(column added in 1.25.0). Legacy rows with mesh=NULL fall back to the
only broker if there's exactly one, otherwise mark dead with a clear
error.
ipc/server.ts:
- GET /v1/peers aggregates across all attached meshes; each peer
record gains a mesh field. ?mesh=<slug> narrows server-side.
- GET /v1/skills aggregates similarly; /v1/skills/:name walks meshes
and returns first match.
- POST /v1/send requires mesh field on multi-mesh daemons; auto-picks
on single-mesh; returns 400 with attached list if ambiguous.
- POST /v1/profile accepts optional mesh; without it, fans out to all
attached meshes (consistent presence).
CLI: trySendViaDaemon now forwards expectedMesh as the body's mesh
field (was informational, now authoritative). claudemesh send
--mesh A and --mesh B from the same shell both route to the right
broker via the same daemon process.
Verified: aggregated peer list across 3 attached meshes; cross-mesh
sends from CLI reach status=done with correct broker_message_ids.
Released as 1.26.0 on npm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Daemon outbox now stores resolved target_spec + crypto_box ciphertext
+ nonce per row. Drain worker is a forwarder; no per-row resolution at
drain time. Outbound routing is no longer a placeholder.
Schema additions (additive, NULL allowed for legacy rows): outbox.mesh,
target_spec, nonce, ciphertext, priority. v0.9.0 rows keep draining via
the broadcast fallback so existing in-flight rows finish cleanly.
IPC /v1/send resolves the user-friendly to (display name, hex prefix,
full pubkey, @group, *, #topicId) into a broker-format target_spec at
accept time. DMs encrypt via crypto_box; broadcast/topic/group base64
the plaintext. Hex prefixes (16+ chars) match against connected peers.
CLI thin-client routing extends trySendViaDaemon pattern to peer list
and skill list/get. Three new helpers in services/bridge/daemon-route.ts.
SKILL.md gains ambient mode section: after claudemesh install, raw
claude works for the daemon's attached mesh. Launch stays as the
override path.
Spec at .artifacts/specs/2026-05-04-v2-roadmap-completion.md orders
the remaining v2.0.0 work: multi-mesh daemon (1.26), CLI-to-thin-client
(1.27), mesh-to-workspace rename (1.28), HKDF identity (2.0).
Released as 1.25.0 on npm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The architectural convergence v0.9.0 was building toward. CLI keeps
working without a daemon (claudemesh send/peer/inbox/...), but the MCP
push-pipe — which Claude Code uses for mid-turn channel emits, slash
commands, and resources — now requires the daemon. There is no fallback.
Daemon (additive):
- /v1/skills (list) and /v1/skills/:name (get) IPC endpoints, so the
MCP shim can surface mesh skills without holding its own broker WS.
- listSkills() / getSkill() on DaemonBrokerClient.
- SSE 'message' event now carries plaintext body, sender_member_pubkey,
priority, and subtype — full payload the MCP shim needs to render a
channel notification.
MCP server: 979 → 469 LoC (470 of the remaining 469 is the unrelated
mesh-service proxy mode; the push-pipe path is ~200 LoC including
boilerplate).
- Probes ~/.claudemesh/daemon/daemon.sock at boot. Bails loudly with
actionable instructions if missing.
- Subscribes to /v1/events SSE and translates each event into a
notifications/claude/channel emit.
- Fetches mesh skills from the daemon for ListPrompts/GetPrompt and
ListResources/ReadResource. ListTools returns []; the CLI is the API.
- No broker WS, no decryption, no reconnect logic. Daemon owns all of it.
claudemesh install: auto-installs and starts the daemon service for the
user's primary mesh (launchd / systemd-user). Pass --no-service to skip.
claudemesh launch: probes the daemon socket; if absent, spawns
'claudemesh daemon up --mesh <slug>' detached and waits up to 10s for
the socket. Surfaces a clear warning on timeout but doesn't block —
Claude Code's MCP shim will print the same error if the daemon really
isn't there.
Bundle: dist/entrypoints/mcp.js drops from 154KB → 104KB (gzipped 34KB
→ 19KB). Test: MCP boots cleanly via stdio, declares correct
capabilities, talks JSON-RPC; daemon /v1/skills returns the empty list
as expected on a mesh with no skills.
Released as 1.24.0 on npm.
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>
- Bump apps/cli/package.json to 1.22.0 (additive feature: claudemesh
daemon long-lived runtime).
- CHANGELOG entry for 1.22.0 covering subcommands, idempotency wiring,
crash recovery, and the deferred Sprint 7 broker hardening.
- Roadmap entry for v0.9.0 daemon foundation right above the v2.0.0
daemon redesign section, so the bridge release is documented as the
shipped step toward the larger architectural shift.
- Move shipped daemon specs (v1..v10 iteration trail + locked v0.9.0
spec + broker-hardening followups) from .artifacts/specs/ to
.artifacts/shipped/ per the project artifact-pipeline convention.
Not in this commit: npm publish and the cli-v1.22.0 GitHub release tag
— both are public-distribution actions and require explicit user
approval.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Additive plumbing for v0.9.0 daemon spec §4.2/§4.4. Adds two nullable
columns to mesh.message_queue — client_message_id (caller-supplied) and
request_fingerprint (canonical sha256 of the send shape) — and threads
them through the broker:
- handleSend reads them off the wire envelope when present
- queueMessage persists them on the row
- drainForMember projects them onto the push so receiving daemons
can dedupe their local inbox by client_message_id
Columns stay nullable so legacy traffic (launch CLI, dashboard chat)
continues to flow uninterrupted. Sprint 7 (broker hardening) will add
the partial unique index and the client_message_dedupe atomic-accept
table once we're ready to enforce dedupe broker-side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mesh.slug actually carries a UNIQUE constraint (mesh_slug_unique)
even though the schema comment claimed otherwise. Trying to rename
to a slug another mesh already owns blew up as a generic 500.
Now: caught at the route, surfaced as 409 with body
{"error":"slug \"<x>\" is already taken"}; CLI maps it to
EXIT.ALREADY_EXISTS and prints the message.
Schema comment corrected to match DB reality.
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>
The launch welcome flow's menuSelect was rendering opts.meshes.map(
m => m.slug) — so even after rename writes the new name to local
config, the picker still only showed the slug. Renders as
"name (slug)" when they differ; falls back to slug alone when
they match (default for never-renamed meshes).
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>
After renaming the mesh display name on the server, the launch
picker still showed the slug ("flexicar-2") because (a) local
config.json was not updated and (b) the picker only printed
mesh.slug. Now: rename writes the new name back into config.json
on success, and the picker prints "name (slug)" when they differ.
Also surfaces a hint that slugs are immutable (today).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rename route was collapsing "mesh doesn't exist" and "exists
but you don't own it" into a single 404 with body
{"error":"mesh not found or you are not the owner"}, and the CLI
was throwing that body away — the user only saw "API error 404:
Not Found", which is actively misleading when they have multiple
accounts and signed in to the wrong one.
Server: separate lookup-then-update. 404 only when the slug is
missing; 403 with an actionable message when the caller is not
the owner.
CLI: parse the {error} body off ApiError and print it instead of
the bare statusText. Map status codes to specific exit codes
(401 -> AUTH_FAILED, 403 -> PERMISSION_DENIED, 404 -> NOT_FOUND).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coolify's last deploy reused the cached image — the new
/api/cli/meshes/[slug] route never made it into .next/server.
Adding force=true to the deploy API call so Coolify rebuilds
from the current commit instead of replaying the cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /api/my/meshes/:slug PATCH route was never implemented and
better-auth's enforceAuth middleware can't validate the CLI's
device-code JWT (signed with CLI_SYNC_SECRET, not a better-auth
session). Adds /api/cli/meshes/:slug on the web app — verifies
the HS256 JWT inline, scopes the rename to (slug, ownerUserId).
CLI now calls the new path. Mirrors the cli-sync-token pattern.
Closes the "API error 401: Unauthorized" hit after a successful
claudemesh login.
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>
the previous form had drizzle render the date param as a js
toString() value which postgres rejected (Fri Apr 03 2026
GMT+0000 doesn't parse as timestamp without help). fix:
serialize to iso then cast ::timestamp inside the sql tag.
simplified the where clause too — the prior conditional dance
emitted "status != completed" three times redundantly. one
"completed_at IS NULL OR > window" covers active + recent-done
in one clause; status filtering happens client-side via the
existing statusSet pass.
also cleans up the debug probe scaffolding.
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>