Commit Graph

159 Commits

Author SHA1 Message Date
Alejandro Gutiérrez
e84914b25b feat: claudemesh slug <old> <new> — change a mesh's slug (v1.20.0)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 15:08:32 +01:00
Alejandro Gutiérrez
5a1d5d6a49 fix(cli): rename syncs local config + picker shows display name
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 14:52:50 +01:00
Alejandro Gutiérrez
f3649d761f fix(rename): split 404 vs 403 + surface API error body (v1.19.2)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 14:44:22 +01:00
Alejandro Gutiérrez
b69df75f0c fix(cli+web): claudemesh rename via inline-JWT route (v1.19.1)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 14:11:31 +01:00
Alejandro Gutiérrez
3a3d2a6c4c feat(cli): file share / file get + same-host fast path (v1.19.0)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 14:05:24 +01:00
Alejandro Gutiérrez
f9ed3fa286 feat(cli): claudemesh skill prints bundled SKILL.md (v1.18.0)
Some checks failed
CI / Docker build (linux/amd64) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
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>
2026-05-03 12:24:45 +01:00
Alejandro Gutiérrez
50b2ae97c2 feat(cli): peer list self-marking + send self-DM guard
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 11:45:03 +01:00
Alejandro Gutiérrez
f679b49b6c feat(workspace): default-aggregation for task/state/memory
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 10:17:41 +01:00
Alejandro Gutiérrez
5ceb311d74 feat(cli): default-aggregation for topic list + notification list
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 04:56:33 +01:00
Alejandro Gutiérrez
e60980cfd7 feat(workspace): claudemesh me search + dashboard parity
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 04:45:54 +01:00
Alejandro Gutiérrez
ff3d11d42d feat(workspace): claudemesh me activity + dashboard parity
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 04:35:52 +01:00
Alejandro Gutiérrez
43e429f204 feat(workspace): claudemesh me notifications + dashboard parity
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 02:35:57 +01:00
Alejandro Gutiérrez
354c47c3d6 chore: remove diagnostic endpoint + debug probe scaffolding
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
restores api + cli to clean state after isolating the v0.4.0
phase 2 deploy issue (web app needed an explicit coolify deploy
trigger — it does not auto-deploy from gitea-vps push the way the
broker does).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:35:08 +01:00
Alejandro Gutiérrez
2262564680 chore(api): rename diagnostic to a unique path to defeat any stale routing cache 2026-05-03 01:28:36 +01:00
Alejandro Gutiérrez
c18891191e chore(api): add /v1/me/ping sanity probe
confirms whether new GET routes under /me/* deploy correctly to
vercel — diagnostic in the middle of the /me/topics 404 chase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:26:58 +01:00
Alejandro Gutiérrez
3964de4962 fix(api): use notInArray + inArray in unread-count subqueries
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
the sql.join() form of NOT IN crashed the route handler before
it could respond — vercel surfaced the crash as a plaintext 404
instead of going through hono's exception handler. switching to
drizzle's notInArray() / inArray() emits stable parameter
bindings and resolves both /v1/me/topics (fresh endpoint) and
/v1/topics (older endpoint with the same ANY() pattern bug).

also cleans up debug instrumentation that was added while
chasing the 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:05:42 +01:00
Alejandro Gutiérrez
c795df4fd4 feat(workspace): claudemesh me topics + dashboard topics page
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-03 00:39:58 +01:00
Alejandro Gutiérrez
7de13cbb71 feat(cli): claudemesh me — cross-mesh workspace overview (v0.4.0)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
2026-05-02 23:35:01 +01:00
Alejandro Gutiérrez
ce321c0a21 docs(skill): add Windows pane-spawn primitives for launch
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Adds Windows Terminal (wt.exe new-tab + split-pane), PowerShell
Start-Process, cmd.exe start, and WSL routing examples to the
"Spawning new sessions" section. Plus the platform's gotchas:
single-quote nesting in cmd.exe, -NoExit semantics, WSL ~/.claudemesh
path-vs-host divergence, and pwsh / --profile selectors for Windows
Terminal. Bumps CLI to 1.9.5.
2026-05-02 22:48:16 +01:00
Alejandro Gutiérrez
9ecf2d65af docs(skill): wizard-free launch patterns for spawning peer sessions
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Adds a "Spawning new sessions (no wizard)" section to the bundled
claudemesh skill. Documents every flag of `claudemesh launch`
(--name, --mesh, --join, --groups, --role, --message-mode,
--system-prompt, --resume, --continue, -y, -q, plus -- pass-through),
shows wizard-free spawn templates from minimal to cold-start-with-
join, and the canonical pane-creation primitives (tmux send-keys,
iTerm2 osascript, Terminal.app, gnome-terminal, screen) that wrap
the verb when spawning into a fresh terminal pane or window.

Closes the gap where Claude knew the verb existed but had no
playbook for "how do I start another peer in a new pane without an
interactive prompt firing." Bumps CLI to 1.9.4 so the skill ships
on `claudemesh install`.
2026-05-02 22:44:00 +01:00
Alejandro Gutiérrez
80755dbf9b feat(cli+broker): structured argument validation, msg-status prefixes (v1.9.3)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Adds apps/cli/src/cli/validators.ts — a small module of shape
validators (pubkey, pubkey prefix, message id, mesh slug) that return
discriminated results so callers can distinguish "shape is wrong"
(INVALID_ARGS exit) from "value is well-shaped, lookup failed"
(NOT_FOUND exit). Includes renderValidationError() for a consistent
three-tier error contract: what's wrong, what would be valid, closest
valid alternative.

First adopter is `claudemesh msg-status`:
- Validates id locally before opening WS — typos return immediately.
- Accepts 8-32 char prefixes (full ids are 32). Pastes that get
  copy-truncated by the terminal still work.
- Distinct error messages for malformed input vs not-in-queue vs
  ambiguous prefix; --json emits the structured shape.

Broker side: WS message_status handler validates idStr is 8-32
base62 before querying. Prefix lookups use LIKE 'prefix%' scoped to
the caller's mesh (no cross-mesh leak). Returns ambiguous_prefix
when more than one match.

Establishes the canonical pattern; rolling out to send / grant /
revoke / topic post --reply-to in subsequent patches.
2026-05-02 22:40:45 +01:00
Alejandro Gutiérrez
82ee89d0dc feat(cli+docs): colorize --help output + workspace view spec
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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.
2026-05-02 22:28:46 +01:00
Alejandro Gutiérrez
8697c1c032 fix(api+cli): topic post messageId is the durable historyId (v1.9.2)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Previously POST /v1/messages returned the message_queue row id as
`messageId`. Topic posts ARE durable (in topic_message); the queue
entry drains on delivery. Pasting that id into `--reply-to` failed
because the broker validates parents against topic_message, not the
queue. Now `messageId` aliases `historyId` for topic posts; both
`historyId` and `queueId` remain available as explicit fields.

Roadmap and CLI README updated with v0.3.1 reply-to + v0.3.2
multi-session entries.
2026-05-02 22:10:13 +01:00
Alejandro Gutiérrez
716e674473 fix(broker+cli): multi-session DM routing + broadcast self-loopback (v0.3.2)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Two related bugs surfaced in multi-session production use of 1.8.0:

1. Replies via `claudemesh send <from_id>` rejected with "no connected
   peer for target" when the original sender's session had rotated
   (Claude Code restart, /resume). Root cause: from_id carried the
   ephemeral session pubkey, which disappears the moment the session
   ends. Fix: handleSend pre-flight now also resolves the target
   pubkey against the persistent meshMember table and routes to the
   owning member's live session(s); MCP push channel now sets from_id
   to the stable member pubkey and exposes the ephemeral one under
   from_session_pubkey.

2. Broadcast/* and @group sends loopback'd to the sender's *sibling*
   sessions (same member, different session keypair), surfacing a
   spurious "tampered or wrong keypair" decrypt warning on the
   sender's own inboxes. Fix: broadcast/group fan-out now skips by
   memberPubkey, not just by presence_id, so the entire sender member
   is excluded — direct sends keep per-presence skip so a member can
   still DM their own sibling session intentionally.

Push envelope now also carries senderMemberPubkey alongside
senderPubkey so any other client of the WS channel can choose the
right one.
2026-05-02 22:05:11 +01:00
Alejandro Gutiérrez
038a5b5bf7 feat(broker+api+cli): topic message reply-to threading (v0.3.1)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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.
2026-05-02 21:58:21 +01:00
Alejandro Gutiérrez
3c35932191 docs(skill): cover topic tail/post + member list + notification list
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Adds v1.7.0 (terminal parity) and v1.8.0 (per-topic encryption)
verbs to the bundled claudemesh skill so Claude Code sessions
discover them via the auto-installed SKILL.md instead of the
README-only path.

Sections added:
  - topic tail / topic post under the topic block
  - member resource (distinct from peer)
  - notification resource
  - per-topic encryption block — explains v2 ciphertext marker,
    re-seal flow, and 404 behaviour

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:12:55 +01:00
Alejandro Gutiérrez
77f4316f2d feat(broker+api+cli): per-topic E2E encryption — v0.3.0 phase 3 (CLI)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-02 21:03:11 +01:00
Alejandro Gutiérrez
dd80d4e946 feat(cli): v1.7.0 — terminal parity for SSE + members + mentions
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-02 20:02:29 +01:00
Alejandro Gutiérrez
d7cef45640 chore(release): claudemesh-cli@1.6.1
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-02 18:50:22 +01:00
Alejandro Gutiérrez
0f32529370 fix(apikey): revoke must verify a row was actually updated
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-02 18:39:25 +01:00
Alejandro Gutiérrez
7d35c779f4 chore(release): claudemesh-cli@1.6.0
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-02 16:11:56 +01:00
Alejandro Gutiérrez
9dd1e401b0 feat(sdk+cli): bridge peer — forward a topic between two meshes
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-02 13:41:50 +01:00
Alejandro Gutiérrez
13d691980a feat(broker+cli): apikey create/list/revoke verbs (v0.2.0 #71)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-05-02 02:13:12 +01:00
Alejandro Gutiérrez
1afae7a507 feat(broker+cli): topics — conversation scope within a mesh (v0.2.0)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Adds the third axis of mesh organization: mesh = trust boundary,
group = identity tag, topic = conversation scope. Topic-tagged
messages filter delivery by topic_member rows and persist to a
topic_message history table for back-scroll on reconnect.

Schema (additive):
- mesh.topic, mesh.topic_member, mesh.topic_message tables
- topic_visibility (public|private|dm) and topic_member_role
  (lead|member|observer) enums
- migration 0022_topics.sql, hand-written following project convention
  (drizzle journal has been drifting since 0011)

Broker:
- 10 helpers (createTopic, listTopics, findTopicByName, joinTopic,
  leaveTopic, topicMembers, getMemberTopicIds, appendTopicMessage,
  topicHistory, markTopicRead)
- drainForMember matches "#<topicId>" target_specs via member's
  topic memberships
- 7 WS handlers (topic_create/list/join/leave/members/history/mark_read)
  + resolveTopicId helper accepting id-or-name
- handleSend auto-persists topic-tagged messages to history

CLI:
- claudemesh topic create/list/join/leave/members/history/read
- claudemesh send "#deploys" "..." resolves topic name to id
- bundled skill teaches Claude the DM/group/topic decision matrix
- policy-classify recognizes topic create/join/leave as writes

Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:53:42 +01:00
Alejandro Gutiérrez
b4f457fceb feat(cli): 1.5.0 — CLI-first architecture, tool-less MCP, policy engine
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
CLI becomes the API; MCP becomes a tool-less push-pipe. Bundle -42%
(250 KB → 146 KB) after stripping ~1700 lines of dead tool handlers.

- Tool-less MCP: tools/list returns []. Inbound peer messages still
  arrive as experimental.claude/channel notifications mid-turn.
- Resource-noun-verb CLI: peer list, message send, memory recall, etc.
  Legacy flat verbs (peers, send, remember) remain as aliases.
- Bundled claudemesh skill auto-installed by `claudemesh install` —
  sole CLI-discoverability surface for Claude.
- Unix-socket bridge: CLI invocations dial the push-pipe's warm WS
  (~220 ms warm vs ~600 ms cold).
- --mesh <slug> flag: connect a session to multiple meshes.
- Policy engine: every broker-touching verb runs through a YAML gate
  at ~/.claudemesh/policy.yaml (auto-created). Destructive verbs
  prompt; non-TTY auto-denies. Audit log at ~/.claudemesh/audit.log.
- --approval-mode plan|read-only|write|yolo + --policy <path>.

Spec: .artifacts/specs/2026-05-02-architecture-north-star.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:18:19 +01:00
Alejandro Gutiérrez
ff551ccf3d chore(cli): release 1.0.1
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Ships disconnect/kick/ban three-tier peer removal, revoked-hello
friendly error with 'contact mesh owner' message, WS close codes
4001 (kicked) and 4002 (banned) that stop CLI auto-reconnect.

latest + alpha dist-tags both → 1.0.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:59:52 +01:00
Alejandro Gutiérrez
b49e9a9b61 feat(cli+broker): three-tier peer removal: disconnect, kick, ban
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-20 09:55:05 +01:00
Alejandro Gutiérrez
163e1be70a chore(cli): release 1.0.0 — out of alpha
Some checks failed
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-20 02:06:11 +01:00
Alejandro Gutiérrez
3d2ab0cb4b fix(cli): production-grade peer disambiguation (alpha.42)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-19 22:56:41 +01:00
Alejandro Gutiérrez
2abf86d540 fix(cli): short-circuit join <slug> when already a member (alpha.41)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-19 20:11:46 +01:00
Alejandro Gutiérrez
a5347cebc0 fix(cli): silence "session restored" log for one-shot commands (alpha.40)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Add quiet opt to BrokerClient; withMesh passes quiet:true so commands
like peers/state/info/remind no longer print per-mesh restore chatter.
Long-running paths (launch, MCP) stay verbose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:54:53 +01:00
Alejandro Gutiérrez
622ea569ad fix(cli): filter self from claudemesh peers output (alpha.39)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-19 19:50:18 +01:00
Alejandro Gutiérrez
d7f381a1e8 fix(cli): surface broker error messages in ban/unban (alpha.38 fix)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 08:57:08 +01:00
Alejandro Gutiérrez
3ceac68e67 feat(cli+broker): kick, ban, unban, bans commands
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-17 08:37:38 +01:00
Alejandro Gutiérrez
49e0af0fc0 chore(cli): bump to alpha.36 with security fixes
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
2026-04-15 19:18:57 +01:00
Alejandro Gutiérrez
2be5e9dccb fix(security): resolve all 17 codex findings — auth, grants, crypto, ops
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
Critical: broker HTTP auth via cli_session bearer token on all /cli/*;
file download requires auth+membership; v2 claim gated; duplicate
claimInviteV2Core removed; grant enforcement tries member then
session pubkey; audit hash uses canonical sorted-keys JSON.

High: rate limit args fixed (burst 10, 60/min) + both buckets swept;
BROKER_ENCRYPTION_KEY fail-fast in prod; migrate uses pg_try + lock_
timeout; hello validates sessionPubkey hex; blocked DMs rejected pre-
queue; watch timers cleaned on disconnect.

Medium: inbound pushes serialized; reconnect jitter + timer guard;
hardcoded URLs through env; v2 claim path configurable.

Low: WSHelloMessage optional protocolVersion+capabilities.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:18:25 +01:00
Alejandro Gutiérrez
1a7a059e75 fix: queue TTL + per-member send rate limit + size cap + no-recipient reject + ack.error
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-15 14:44:09 +01:00
Alejandro Gutiérrez
39fe296aaa fix(cli): decrypt falls back to member secret key when session key fails
Some checks failed
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
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>
2026-04-15 14:37:36 +01:00
Alejandro Gutiérrez
4bc3c045ae fix(cli): send_message hard-fails on unknown peer name; dedup-annotate list_peers
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
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>
2026-04-15 14:10:47 +01:00
Alejandro Gutiérrez
1bb702e481 chore(cli): bump to alpha.32 2026-04-15 08:54:26 +01:00