Files
claudemesh/apps/cli/CHANGELOG.md
Alejandro Gutiérrez 3753a6e137 feat(cli): 1.27.0 — state/memory through daemon + workspace alias
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>
2026-05-04 09:41:18 +01:00

16 KiB
Raw Blame History

Changelog

1.27.0 (2026-05-04) — state + memory through the daemon, workspace alias

Two more verb families now route through the local daemon's IPC for the warm path: state get/set/list and remember/recall/forget. Same pattern as 1.25.0 for peers/skills — try the socket first (~1 ms warm), fall back to the cold WS path when the daemon isn't running.

What changed

  • claudemesh state get|set|list route through /v1/state when the daemon socket is present. --mesh <slug> forwards as a query/body field. Single-mesh daemons auto-pick; multi-mesh daemons require --mesh for state set.
  • claudemesh remember, claudemesh recall, claudemesh forget (and claudemesh memory <sub>) route through /v1/memory. Aggregates across attached meshes for recall; requires --mesh for remember/forget when ambiguous.
  • New claudemesh workspace <verb> alias surface — early teaser for the 1.28.0 mesh→workspace public rename. Mirrors list, info, create, join, delete, rename, share, launch, overview. No-arg claudemesh workspace falls through to launch (same as bare claudemesh).

IPC surface

  • GET /v1/state — list (?mesh=<slug> filter) or single key lookup (?key=<k>&mesh=<slug>). Returns 404 with { error: "state_not_found" } when missing.
  • POST /v1/state{ key, value, mesh? }. 400 + attached list when multi-mesh and no mesh field.
  • GET /v1/memory?q=<query>&mesh=<slug> — recall. Aggregates across meshes, each match tagged with its mesh field.
  • POST /v1/memory{ content, tags?, mesh? }. Returns { id, mesh }.
  • DELETE /v1/memory/:id?mesh=<slug> — forget.
  • ipc_features gains state and memory keys.

Why this matters

State and memory were the last verbs that opened a fresh broker WS on every invocation. Now they reuse the daemon's existing connection — the warm-path latency cliff (~150 ms cold WS handshake → ~1 ms IPC) extends to two more flows agents poll heavily.

The workspace alias is cosmetic but lays the groundwork for 1.28.0's documented rename without breaking anyone's muscle memory.

1.26.0 (2026-05-04) — multi-mesh daemon

The daemon now attaches to all joined meshes simultaneously by default. Ambient mode (raw claude after claudemesh install) finally delivers what v2.0.0 promised: one daemon process, one PID per user, all your meshes available concurrently with no manual switching.

What changed

  • claudemesh daemon up (no --mesh flag) attaches to every joined mesh. One DaemonBrokerClient per mesh, all in one process. Pass --mesh <slug> to scope to a single mesh (legacy mode).
  • daemon_started log line now reports meshes: [...] (array) instead of mesh: <slug> (single).
  • Outbox dispatch picks the broker via the mesh column added in 1.25.0. Legacy rows (mesh=NULL) fall back to the only broker if there's exactly one; otherwise mark dead with a clear error.

IPC surface

  • 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. GET /v1/skills/:name walks attached meshes and returns the first match (or ?mesh=<slug> to scope).
  • POST /v1/send requires mesh field when the daemon is attached to multiple meshes; auto-picks the only one in single-mesh mode. Returns 400 with the attached mesh list if ambiguous.
  • POST /v1/profile accepts optional mesh field — without it, applies the update to every attached mesh (presence stays consistent across meshes by default).

CLI integration

  • claudemesh send --mesh <slug> forwards the mesh in the daemon request body. The CLI's expectedMesh argument was previously informational; now it's authoritative for routing.
  • claudemesh peer list already aggregates because the IPC endpoint does — no change needed in the verb.
  • Verified end-to-end: claudemesh send --mesh A and claudemesh send --mesh B from the same CLI invocation both reach outbox.status=done with broker-issued IDs, dispatched to the correct broker per row.

What this unlocks

Ambient mode for users with N meshes. Run claudemesh install once, then claude from anywhere — channel push, slash commands, and resources flow through the daemon for every joined mesh simultaneously. No more "which mesh is the daemon attached to?" mental overhead.

1.25.0 (2026-05-04) — Sprint 4 outbound routing + ambient mode

Daemon outbound routing (Sprint 4)

The v0.9.0 daemon shipped outbox infrastructure but its drain worker was a placeholder — every queued send went out as a broadcast (*). That's now fixed. Outbound resolution and crypto_box encryption happen at IPC accept time, then the drain worker just forwards the already-encrypted ciphertext to the broker.

  • Outbox schema additions (additive, NULL allowed for legacy rows): mesh, target_spec, nonce, ciphertext, priority. Existing v0.9.0 rows keep draining via the broadcast fallback.
  • IPC /v1/send resolves the user-friendly to (display name, hex prefix, full pubkey, @group, *, #topicId) into a broker-format target_spec and encrypts the plaintext using crypto_box for DMs (against recipient pubkey + sender session secret) or base64 for broadcast / topic / group targets.
  • Drain worker reads target_spec, nonce, ciphertext, priority from the row and dispatches as-is. No per-row resolution at drain time means peer-presence flicker doesn't affect in-flight sends.
  • Pubkey prefix matching: 16+ char hex prefix matches against peer.pubkey and peer.memberPubkey of connected peers. Ambiguous prefixes return 502 with a clear error.

Smoke test verified end-to-end: claudemesh send --self <prefix> "..." through daemon resolves, encrypts, and delivers. Outbox reaches status=done with broker-issued broker_message_id.

CLI thin-client routing extensions

claudemesh peer list and claudemesh skill list/get now route through the daemon when its socket is present, mirroring the trySendViaDaemon pattern from send.ts. Same fall-back chain: daemon → bridge → cold path.

New helpers in services/bridge/daemon-route.ts:

  • tryListPeersViaDaemon()
  • tryListSkillsViaDaemon()
  • tryGetSkillViaDaemon(name)

Ambient mode

After claudemesh install (which now installs and starts the daemon service), raw claude Just Works for the daemon's attached mesh. No claudemesh launch ceremony needed for the common case. Channel push, slash commands, and resources flow through the daemon-backed MCP shim.

claudemesh launch remains the override path: explicit mesh selection, fresh display name, headless modes, system-prompt injection, or multi-mesh users who want to spawn into a non-default mesh.

Roadmap spec

.artifacts/specs/2026-05-04-v2-roadmap-completion.md documents exactly what's done vs. what remains for the full v2.0.0 endpoint: multi-mesh daemon (1.26.0), full CLI-to-thin-client conversion (1.27.0), mesh→workspace rename (1.28.0), HKDF identity (2.0.0).

1.24.0 (2026-05-03) — daemon required + thin MCP shim

The architectural convergence v0.9.0 was building toward.

Daemon promoted from optional to required (for in-Claude-Code use)

The CLI itself (claudemesh send, peer list, inbox, vault, watch, webhook, etc.) keeps working without a daemon. But the MCP server — which provides Claude Code's mid-turn channel push, slash commands, and resource browser — now requires the daemon. There is no fallback.

  • claudemesh install auto-installs and starts the daemon service (launchd / systemd-user) for the user's primary mesh. Pass --no-service to opt out.
  • claudemesh launch ensures the daemon is running before spawning Claude Code; spawns it foreground if absent.
  • The MCP shim probes ~/.claudemesh/daemon/daemon.sock at boot. If missing after a 2s grace window, it bails with actionable instructions ("run claudemesh daemon up --mesh <slug>").

MCP server: 979 → ~300 LoC of push-pipe code

apps/cli/src/mcp/server.ts is now a thin daemon-SSE translator. It no longer holds a broker WebSocket, decrypts messages, manages mesh state, or runs reconnection logic. All of that is the daemon's job.

  • Subscribes to daemon /v1/events SSE; translates each message event into a notifications/claude/channel emit.
  • Sources mesh-published skills via daemon /v1/skills IPC for ListPrompts / GetPrompt / ListResources / ReadResource.
  • ListTools returns [] (the CLI is the API, taught via the bundled skill).
  • The mesh-service proxy mode (claudemesh-cli --service <name>, the sub-MCP-server for proxying a deployed mesh-MCP service) is unchanged — separate code path, different lifecycle.

Bundle size: MCP entry dropped from 154KB → 104KB (gzipped 34KB → 19KB).

Daemon SSE event payload extended

message events on /v1/events now include plaintext-decrypted body, sender member pubkey, priority, and subtype — everything the MCP shim needs to render a complete channel notification without going back to the broker.

Daemon IPC: GET /v1/skills (list) and GET /v1/skills/:name (get)

The daemon exposes mesh-published skills over IPC so the MCP shim can surface them as MCP prompts/resources without holding its own broker WS. Same wire format as before from Claude Code's perspective.

Why this is the right architecture

MCP and the daemon are no longer independent broker clients with duplicated WS, decrypt, and dedupe logic. The daemon owns the broker relationship; MCP is a Claude-Code-specific UX adapter that reads from the daemon. Industry-normal shape (Tailscale, Slack, Ollama, Docker) where the long-lived runtime is required and the per-app integrations attach to it.

1.23.0 (2026-05-03) — close the CLI surface, prune dead MCP stubs

Three previously-MCP-only write verbs land on the CLI, closing every functional gap between the (defunct since 1.5.0) MCP tool registry and the CLI:

  • claudemesh vault set <key> <value> — encrypts client-side via crypto_secretbox_easy with a fresh symmetric key, then seals the key to the member's own pubkey via crypto_box_seal (same shape as the file-share crypto). Flags: --type env|file, --mount <path>, --description <text>. Pairs with the existing vault list/delete.
  • claudemesh watch add <url> — registers a URL change watcher. Flags: --label, --interval <sec>, --mode, --extract <css>, --notify-on changed|always. Pairs with watch list/remove.
  • claudemesh webhook create <name> — issues a fresh inbound webhook; prints url + one-shot secret. Pairs with webhook list/delete.

Cleanup: removed 22 dead stub files under apps/cli/src/mcp/tools/*, the unused router.ts, middleware/*, and handlers/* directories (~120 LoC). The MCP server in 1.5.0+ has been a tool-less push-pipe; these stubs were leftover scaffolding that never wired into the tools/list response. The legitimate MCP surfaces stay untouched:

  • <channel source="claudemesh"> push pipe (the irreducible reason MCP exists at all — no other Claude Code surface can inject events mid-turn).
  • Mesh skills exposed as MCP prompts (slash commands) and resources (skill://claudemesh/<name>).
  • Mesh-deployed MCP services proxied via the sub-process tool surface (separate code path under server.ts:855+).

1.22.1 (2026-05-03) — daemon docs + help

  • Root claudemesh --help now lists the daemon subcommand suite under its own section (was missing in 1.22.0).
  • claudemesh daemon (no subcommand) now prints a usage block instead of silently launching the daemon. daemon help|--help|-h work too.
  • Bundled SKILL.md gained a "Daemon path (v0.9.0, opt-in, fastest)" section explaining the runtime, lifecycle commands, and how it relates to claudemesh install (independent — not auto-started).

1.22.0 (2026-05-03) — daemon v0.9.0

New: claudemesh daemon — long-lived peer mesh runtime

Persistent local process that holds the broker WS, durable outbox/inbox in SQLite, IPC over UDS (+ optional loopback TCP with bearer token), and SSE event stream. Surrogates wire-up; claudemesh send and friends route through the daemon when its socket is present, falling back to the existing bridge / cold paths otherwise.

Subcommands:

  • daemon up|start [--mesh <slug>] [--name ...] [--no-tcp] [--public-health]
  • daemon status [--json], daemon down|stop, daemon version
  • daemon outbox list [--failed|--pending|--inflight|--done]
  • daemon outbox requeue <id> [--new-client-id <id>]
  • daemon accept-host (per-host fingerprint pin)
  • daemon install-service --mesh <slug> (macOS launchd / Linux systemd-user)
  • daemon uninstall-service

Idempotency end-to-end:

  • Caller-stable client_message_id + canonical request_fingerprint (sha256 of envelope_version || dest_kind || dest_ref || reply_to || priority || canonical_meta_json || body_hash) attach on every send.
  • Broker persists both on mesh.message_queue (migration 0028, additive
    • nullable) and echoes them on push, so receiving daemons dedupe their inbox by client_message_id.
  • §4.5.1 IPC duplicate-lookup table (11 cases × no-row / 5 statuses × match/mismatch) covered by 15 unit tests.

Crash recovery:

  • Outbox row transitions: pendinginflightdone / dead / aborted. BEGIN IMMEDIATE serializes daemon-local writes; the drain worker is wakeable via promise-replacement and backs off failed sends.
  • Decrypt path tries session secret key, then member secret key, then base64 fallback, so legacy unencrypted pushes still inbox cleanly.

Sprint 7 (broker-side dedupe enforcement: partial unique index + mesh.client_message_dedupe atomic-accept table) is intentionally deferred — see .artifacts/shipped/2026-05-03-daemon-spec-broker- hardening-followups.md.

1.0.0-alpha.0 (2026-04-13)

Architecture

  • Complete folder restructure: entrypoints/, cli/, commands/, services/ (17 feature-folders with facade pattern), ui/, mcp/, constants/, types/, utils/, locales/, templates/
  • 212 source files, 10,900 lines
  • ESM-only, Bun bundler, TypeScript strict mode

New CLI commands

  • claudemesh register — account creation via browser handoff
  • claudemesh login — device-code OAuth
  • claudemesh logout — revoke session + clear credentials
  • claudemesh whoami — identity check with --json support
  • claudemesh new <name> — create mesh from CLI (was dashboard-only)
  • claudemesh invite [email] — generate invite from CLI (was dashboard-only)

Ported from v1 (full feature parity)

  • All 79 MCP tools
  • All 85 WS message types (broker protocol unchanged)
  • Welcome wizard, launch flow, install/uninstall
  • Ed25519 + NaCl crypto (keypairs, crypto_box DMs, file encryption)
  • Reconnect with exponential backoff
  • Status priority engine, scheduled messages, URL watch
  • Doctor checks, Telegram bridge connect wizard

Security hardening (25 bugs fixed across 4 reviews)

  • execFile instead of exec for browser open (command injection fix)
  • ReDoS-safe pattern matching in peer file sharing
  • Atomic config writes via temp file + rename
  • Auth token stored with openSync(mode: 0o600) — no permission race
  • Decryption oracle collapsed to generic error in get_file
  • Download size limit (100MB) on file retrieval
  • Path traversal protection with realpathSync for symlink escapes
  • Callback listener double-resolve guard
  • Push buffer 1MB per-message truncation
  • makeReqId uses crypto.randomBytes instead of Math.random
  • Connect guard prevents double-connect race

Breaking changes from v0.10.x

  • Flat command namespace (no launch subcommand, no advanced prefix)
  • New config shape (same data, cleaner layout)
  • New --json output format with schema_version: "1.0"
  • New exit codes (see constants/exit-codes.ts)