feat(cli+broker): three-tier peer removal: disconnect, kick, ban
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>
This commit is contained in:
@@ -30,9 +30,10 @@ Mesh
|
||||
claudemesh delete [slug] delete a mesh (alias: rm)
|
||||
claudemesh rename <slug> <name> rename a mesh
|
||||
claudemesh share [email] share mesh (invite link / send email)
|
||||
claudemesh kick <peer> disconnect a peer (can reconnect)
|
||||
claudemesh kick --stale 30m disconnect idle peers (> duration)
|
||||
claudemesh kick --all disconnect everyone except you
|
||||
claudemesh disconnect <peer> soft disconnect (peer auto-reconnects)
|
||||
claudemesh kick <peer> end session (peer must manually rejoin)
|
||||
claudemesh kick --stale 30m kick peers idle > duration
|
||||
claudemesh kick --all kick everyone except yourself
|
||||
claudemesh ban <peer> kick + permanently revoke (can't rejoin)
|
||||
claudemesh unban <peer> lift a ban
|
||||
claudemesh bans list banned members
|
||||
@@ -139,6 +140,7 @@ async function main(): Promise<void> {
|
||||
case "delete": case "rm": { const { deleteMesh } = await import("~/commands/delete-mesh.js"); process.exit(await deleteMesh(positionals[0] ?? "", { yes: !!flags.y || !!flags.yes })); break; }
|
||||
case "rename": { const { rename } = await import("~/commands/rename.js"); process.exit(await rename(positionals[0] ?? "", positionals[1] ?? "")); break; }
|
||||
case "share": case "invite": { const { invite } = await import("~/commands/invite.js"); process.exit(await invite(positionals[0], { mesh: flags.mesh as string, json: !!flags.json })); break; }
|
||||
case "disconnect": { const { runDisconnect } = await import("~/commands/kick.js"); process.exit(await runDisconnect(positionals[0], { mesh: flags.mesh as string, stale: flags.stale as string, all: !!flags.all })); break; }
|
||||
case "kick": { const { runKick } = await import("~/commands/kick.js"); process.exit(await runKick(positionals[0], { mesh: flags.mesh as string, stale: flags.stale as string, all: !!flags.all })); break; }
|
||||
case "ban": { const { runBan } = await import("~/commands/ban.js"); process.exit(await runBan(positionals[0], { mesh: flags.mesh as string })); break; }
|
||||
case "unban": { const { runUnban } = await import("~/commands/ban.js"); process.exit(await runUnban(positionals[0], { mesh: flags.mesh as string })); break; }
|
||||
|
||||
Reference in New Issue
Block a user