use anyhow::{Context, Result, anyhow, bail};
use clap::{Parser, Subcommand};
use serde_json::{Value, json};
use crate::config;
mod comms;
mod demo;
mod group;
mod identity;
mod lifecycle;
mod mesh;
mod pairing;
mod relay;
mod session;
mod setup;
mod status;
mod upgrade;
pub(crate) use comms::here_summary;
pub(crate) use comms::parse_deadline_until;
pub(crate) use relay::cmd_bind_relay;
pub use relay::error_smells_like_slot_4xx;
pub use relay::run_sync_pull;
pub use relay::run_sync_push;
pub use session::maybe_auto_init_cwd_session;
pub(crate) use pairing::{DialTarget, resolve_name_to_target};
pub(crate) use pairing::{
ResolveError, add_local_sister_core, cmd_add_local_sister, resolve_peer_handle,
};
pub(crate) use identity::op_claims_from_card;
pub(super) use identity::{cmd_claim, cmd_init};
#[derive(Parser, Debug)]
#[command(
name = "wire",
version,
about = "Magic-wormhole for AI agents — bilateral signed-message bus",
long_about = None,
after_help = "\x1b[1mStart here:\x1b[0m\n \
wire up come online (one command)\n \
wire dial <name> \"hi\" reach a peer and send\n \
wire tail read replies\n \
wire here who am I, who's around?\n \
wire doctor something off? full health check\n\
\nThe ~40 verbs below are mostly plumbing — the five above cover daily use.\n\
Guide: https://github.com/SlanchaAi/wire"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
#[command(hide = true)]
Init {
#[arg(long)]
relay: Option<String>,
#[arg(long, conflicts_with = "relay")]
offline: bool,
#[arg(long)]
json: bool,
},
Whoami {
#[arg(long)]
json: bool,
#[arg(long, conflicts_with = "json")]
short: bool,
#[arg(long, conflicts_with_all = ["json", "short"])]
colored: bool,
},
Peers {
#[arg(long)]
json: bool,
},
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
Here {
#[arg(long)]
json: bool,
},
Pending {
#[arg(long)]
json: bool,
},
Send {
peer: String,
kind_or_body: String,
body: Option<String>,
#[arg(long)]
deadline: Option<String>,
#[arg(long)]
no_auto_pair: bool,
#[arg(long)]
queue: bool,
#[arg(long)]
json: bool,
},
SendProject {
project: String,
body: String,
#[arg(long, default_value = "claim")]
kind: String,
#[arg(long)]
deadline: Option<String>,
#[arg(long)]
json: bool,
},
Project {
tag: Option<String>,
#[arg(long, conflicts_with = "tag")]
clear: bool,
#[arg(long)]
json: bool,
},
Dial {
name: String,
message: Option<String>,
#[arg(long)]
json: bool,
},
Tail {
peer: Option<String>,
#[arg(long)]
json: bool,
#[arg(long, default_value_t = 0)]
limit: usize,
#[arg(long)]
oldest: bool,
},
Monitor {
#[arg(long)]
peer: Option<String>,
#[arg(long)]
json: bool,
#[arg(long)]
include_handshake: bool,
#[arg(long, default_value_t = 500)]
interval_ms: u64,
#[arg(long, default_value_t = 0)]
replay: usize,
},
Verify {
path: String,
#[arg(long)]
json: bool,
},
Mcp,
RelayServer {
#[arg(long, default_value = "127.0.0.1:8770")]
bind: String,
#[arg(long)]
local_only: bool,
#[arg(long)]
uds: Option<std::path::PathBuf>,
},
BindRelay {
url: String,
#[arg(long)]
scope: Option<String>,
#[arg(long)]
replace: bool,
#[arg(long)]
migrate_pinned: bool,
#[arg(long)]
json: bool,
},
AddPeerSlot {
handle: String,
url: String,
slot_id: String,
slot_token: String,
#[arg(long)]
json: bool,
},
Push {
peer: Option<String>,
#[arg(long)]
json: bool,
},
Pull {
#[arg(long)]
json: bool,
},
Status {
#[arg(long)]
peer: Option<String>,
#[arg(long)]
json: bool,
},
Responder {
#[command(subcommand)]
command: ResponderCommand,
},
Pin {
card_file: String,
#[arg(long)]
json: bool,
},
RotateSlot {
#[arg(long)]
no_announce: bool,
#[arg(long)]
json: bool,
},
ForgetPeer {
handle: String,
#[arg(long)]
purge: bool,
#[arg(long)]
json: bool,
},
Supervisor {
#[arg(long)]
json: bool,
},
Daemon {
#[arg(long, default_value_t = 5)]
interval: u64,
#[arg(long)]
once: bool,
#[arg(long)]
all_sessions: bool,
#[arg(long)]
session: Option<String>,
#[arg(long)]
json: bool,
},
#[command(subcommand)]
Session(SessionCommand),
Identity {
#[command(subcommand)]
cmd: IdentityCommand,
},
#[command(subcommand)]
Mesh(MeshCommand),
#[command(subcommand)]
Group(GroupCommand),
#[command(subcommand)]
Enroll(EnrollCommand),
#[command(subcommand)]
Org(OrgCommand),
Setup {
#[arg(long)]
apply: bool,
#[arg(long)]
statusline: bool,
#[arg(long)]
remove: bool,
},
Whois {
handle: Option<String>,
#[arg(long)]
json: bool,
#[arg(long)]
relay: Option<String>,
},
Add {
handle: String,
#[arg(long)]
relay: Option<String>,
#[arg(long)]
local_sister: bool,
#[arg(long)]
json: bool,
},
Up {
relay: Option<String>,
#[arg(long, conflicts_with_all = ["relay", "with_local"])]
offline: bool,
#[arg(long)]
with_local: Option<String>,
#[arg(long)]
no_local: bool,
#[arg(long)]
json: bool,
},
Demo {
#[arg(long)]
json: bool,
},
Doctor {
#[arg(long)]
json: bool,
#[arg(long, default_value_t = 5)]
recent_rejections: usize,
},
#[command(visible_alias = "update")]
Upgrade {
#[arg(long)]
check: bool,
#[arg(long)]
local: bool,
#[arg(long = "restart-mcp")]
restart_mcp: bool,
#[arg(long = "refresh-stale-children")]
refresh_stale_children: bool,
#[arg(long)]
json: bool,
},
Nuke {
#[arg(long, visible_alias = "yes")]
force: bool,
#[arg(long)]
purge: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
really_this_machine: bool,
#[arg(long)]
json: bool,
},
Service {
#[command(subcommand)]
action: ServiceAction,
},
Diag {
#[command(subcommand)]
action: DiagAction,
},
#[command(hide = true)]
Claim {
nick: String,
#[arg(long)]
relay: Option<String>,
#[arg(long)]
public_url: Option<String>,
#[arg(long)]
hidden: bool,
#[arg(long)]
json: bool,
},
Profile {
#[command(subcommand)]
action: ProfileAction,
},
#[command(hide = true)] Invite {
#[arg(long, default_value = "https://wireup.net")]
relay: String,
#[arg(long, default_value_t = 86_400)]
ttl: u64,
#[arg(long, default_value_t = 1)]
uses: u32,
#[arg(long)]
share: bool,
#[arg(long)]
json: bool,
},
Accept {
target: String,
#[arg(long)]
json: bool,
},
#[command(alias = "invite-accept")]
AcceptInvite {
url: String,
#[arg(long)]
json: bool,
},
Reject {
peer: String,
#[arg(long)]
json: bool,
},
BlockPeer {
did: String,
#[arg(long)]
note: Option<String>,
#[arg(long)]
json: bool,
},
UnblockPeer {
did: String,
#[arg(long)]
json: bool,
},
Blocked {
#[arg(long)]
json: bool,
},
Notify {
#[arg(long, default_value_t = 2)]
interval: u64,
#[arg(long)]
peer: Option<String>,
#[arg(long)]
once: bool,
#[arg(long)]
json: bool,
},
Quiet {
#[command(subcommand)]
action: QuietAction,
},
}
#[derive(Subcommand, Debug)]
pub enum QuietAction {
On,
Off,
Status {
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum DiagAction {
Tail {
#[arg(long, default_value_t = 20)]
limit: usize,
#[arg(long)]
json: bool,
},
Enable,
Disable,
Status {
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum EnrollCommand {
Op {
#[arg(long, default_value = "operator")]
handle: String,
#[arg(long)]
json: bool,
},
OrgCreate {
#[arg(long)]
handle: String,
#[arg(long)]
json: bool,
},
OrgAddMember {
op_did: String,
#[arg(long)]
org: String,
#[arg(long)]
json: bool,
},
Republish {
#[arg(long)]
json: bool,
},
AddMembership {
#[arg(long)]
bundle: Option<String>,
#[arg(long)]
org: Option<String>,
#[arg(long = "org-pubkey")]
org_pubkey: Option<String>,
#[arg(long = "member-cert")]
member_cert: Option<String>,
#[arg(long)]
json: bool,
},
RotateOpKey {
#[arg(long)]
json: bool,
},
RotateOrgKey {
org_did: String,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum OrgCommand {
Bind {
domain: String,
#[arg(long, default_value = "notify")]
mode: String,
#[arg(long)]
json: bool,
},
List {
#[arg(long)]
json: bool,
},
Forget {
org_did: String,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum IdentityCommand {
Show {
#[arg(long)]
json: bool,
},
List {
#[arg(long)]
json: bool,
},
#[command(hide = true)]
Publish {
nick: String,
#[arg(long)]
relay: Option<String>,
#[arg(long, alias = "public")]
public_url: Option<String>,
#[arg(long)]
hidden: bool,
#[arg(long)]
json: bool,
},
Destroy {
name: String,
#[arg(long)]
force: bool,
#[arg(long)]
json: bool,
},
Create {
#[arg(long)]
name: Option<String>,
#[arg(long, conflicts_with = "local")]
anonymous: bool,
#[arg(long)]
local: bool,
#[arg(long)]
json: bool,
},
Persist {
name: String,
#[arg(long = "as", value_name = "NEW_NAME")]
as_name: Option<String>,
#[arg(long)]
json: bool,
},
Demote {
name: String,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum SessionCommand {
New {
name: Option<String>,
#[arg(long, default_value = "https://wireup.net")]
relay: String,
#[arg(long)]
with_local: bool,
#[arg(long, default_value = "http://127.0.0.1:8771")]
local_relay: String,
#[arg(long)]
with_lan: bool,
#[arg(long)]
lan_relay: Option<String>,
#[arg(long)]
with_uds: bool,
#[arg(long)]
uds_socket: Option<std::path::PathBuf>,
#[arg(long)]
no_daemon: bool,
#[arg(long)]
local_only: bool,
#[arg(long)]
json: bool,
},
List {
#[arg(long)]
json: bool,
},
ListLocal {
#[arg(long)]
json: bool,
},
PairAllLocal {
#[arg(long, default_value_t = 1)]
settle_secs: u64,
#[arg(long, default_value = "https://wireup.net")]
federation_relay: String,
#[arg(long)]
json: bool,
},
MeshStatus {
#[arg(long, default_value_t = 300)]
stale_secs: u64,
#[arg(long)]
json: bool,
},
Env {
name: Option<String>,
#[arg(long)]
json: bool,
},
Current {
#[arg(long)]
json: bool,
},
Bind {
name: Option<String>,
#[arg(long)]
json: bool,
},
Destroy {
name: String,
#[arg(long)]
force: bool,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum GroupCommand {
Create {
name: String,
#[arg(long)]
json: bool,
},
Add {
group: String,
peer: String,
#[arg(long)]
json: bool,
},
Send {
group: String,
message: String,
#[arg(long)]
json: bool,
},
Tail {
group: String,
#[arg(long, default_value_t = 20)]
limit: usize,
#[arg(long)]
json: bool,
},
List {
#[arg(long)]
json: bool,
},
Invite {
group: String,
#[arg(long)]
json: bool,
},
Join {
code: String,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum MeshCommand {
Status {
#[arg(long, default_value_t = 300)]
stale_secs: u64,
#[arg(long)]
json: bool,
},
Broadcast {
#[arg(long, default_value = "claim")]
kind: String,
#[arg(long, default_value = "local")]
scope: String,
#[arg(long)]
exclude: Vec<String>,
#[arg(long)]
noreply: bool,
body: String,
#[arg(long)]
json: bool,
},
Role {
#[command(subcommand)]
action: MeshRoleAction,
},
Route {
role: String,
#[arg(long, default_value = "round-robin")]
strategy: String,
#[arg(long)]
exclude: Vec<String>,
#[arg(long, default_value = "claim")]
kind: String,
body: String,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum MeshRoleAction {
Set {
role: String,
#[arg(long)]
json: bool,
},
Get {
peer: Option<String>,
#[arg(long)]
json: bool,
},
List {
#[arg(long)]
json: bool,
},
Clear {
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum ServiceAction {
Install {
#[arg(long)]
local_relay: bool,
#[arg(long)]
json: bool,
},
Uninstall {
#[arg(long)]
local_relay: bool,
#[arg(long)]
json: bool,
},
Status {
#[arg(long)]
local_relay: bool,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum ResponderCommand {
Set {
status: String,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
json: bool,
},
Get {
peer: Option<String>,
#[arg(long)]
json: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum ProfileAction {
Set {
field: String,
value: String,
#[arg(long)]
json: bool,
},
Get {
#[arg(long)]
json: bool,
},
Clear {
field: String,
#[arg(long)]
json: bool,
},
}
pub fn run() -> Result<()> {
crate::session::maybe_adopt_session_wire_home("cli");
let cli = Cli::parse();
match cli.command {
Command::Init {
relay,
offline,
json,
} => cmd_init(relay.as_deref(), offline, json),
Command::Status { peer, json } => {
if let Some(peer) = peer {
status::cmd_status_peer(&peer, json)
} else {
status::cmd_status(json)
}
}
Command::Whoami {
json,
short,
colored,
} => identity::cmd_whoami(json_default(json), short, colored),
Command::Peers { json } => comms::cmd_peers(json_default(json)),
Command::Here { json } => comms::cmd_here(json_default(json)),
Command::Demo { json } => demo::cmd_demo(json_default(json)),
Command::Completions { shell } => {
use clap::CommandFactory;
let mut cmd = Cli::command();
clap_complete::generate(shell, &mut cmd, "wire", &mut std::io::stdout());
Ok(())
}
Command::Pending { json } => pairing::cmd_pair_list_inbound(json_default(json)),
Command::Reject { peer, json } => pairing::cmd_pair_reject(&peer, json_default(json)),
Command::BlockPeer { did, note, json } => {
pairing::cmd_block_peer(&did, note, json_default(json))
}
Command::UnblockPeer { did, json } => pairing::cmd_unblock_peer(&did, json_default(json)),
Command::Blocked { json } => pairing::cmd_blocked(json_default(json)),
Command::Send {
peer,
kind_or_body,
body,
deadline,
no_auto_pair,
queue,
json,
} => {
let (kind, body) = match body {
Some(real_body) => (kind_or_body, real_body),
None => ("claim".to_string(), kind_or_body),
};
comms::cmd_send(
&peer,
&kind,
&body,
deadline.as_deref(),
no_auto_pair,
queue,
json_default(json),
)
}
Command::SendProject {
project,
body,
kind,
deadline,
json,
} => comms::cmd_send_project(
&project,
&kind,
&body,
deadline.as_deref(),
json_default(json),
),
Command::Project { tag, clear, json } => {
identity::cmd_project(tag.as_deref(), clear, json_default(json))
}
Command::Dial {
name,
message,
json,
} => pairing::cmd_dial(&name, message.as_deref(), json_default(json)),
Command::Tail {
peer,
json,
limit,
oldest,
} => comms::cmd_tail(peer.as_deref(), json, limit, oldest),
Command::Monitor {
peer,
json,
include_handshake,
interval_ms,
replay,
} => comms::cmd_monitor(
peer.as_deref(),
json,
include_handshake,
interval_ms,
replay,
),
Command::Verify { path, json } => comms::cmd_verify(&path, json),
Command::Responder { command } => match command {
ResponderCommand::Set {
status,
reason,
json,
} => status::cmd_responder_set(&status, reason.as_deref(), json),
ResponderCommand::Get { peer, json } => {
status::cmd_responder_get(peer.as_deref(), json)
}
},
Command::Mcp => relay::cmd_mcp(),
Command::RelayServer {
bind,
local_only,
uds,
} => relay::cmd_relay_server(&bind, local_only, uds.as_deref()),
Command::BindRelay {
url,
scope,
replace,
migrate_pinned,
json,
} => relay::cmd_bind_relay(&url, scope.as_deref(), replace, migrate_pinned, json),
Command::AddPeerSlot {
handle,
url,
slot_id,
slot_token,
json,
} => relay::cmd_add_peer_slot(&handle, &url, &slot_id, &slot_token, json),
Command::Push { peer, json } => relay::cmd_push(peer.as_deref(), json),
Command::Pull { json } => relay::cmd_pull(json),
Command::Pin { card_file, json } => pairing::cmd_pin(&card_file, json),
Command::RotateSlot { no_announce, json } => relay::cmd_rotate_slot(no_announce, json),
Command::ForgetPeer {
handle,
purge,
json,
} => relay::cmd_forget_peer(&handle, purge, json),
Command::Supervisor { json } => status::cmd_supervisor(json),
Command::Daemon {
interval,
once,
all_sessions,
session,
json,
} => relay::cmd_daemon(interval, once, all_sessions, session, json),
Command::Session(cmd) => cmd_session(cmd),
Command::Identity { cmd } => identity::cmd_identity(cmd),
Command::Mesh(cmd) => cmd_mesh(cmd),
Command::Group(cmd) => cmd_group(cmd),
Command::Enroll(cmd) => identity::cmd_enroll(cmd),
Command::Org(cmd) => identity::cmd_org(cmd),
Command::Invite {
relay,
ttl,
uses,
share,
json,
} => pairing::cmd_invite(&relay, ttl, uses, share, json),
Command::Accept { target, json } => {
let j = json_default(json);
if target.starts_with("wire://pair?") || target.starts_with("http") {
anyhow::bail!(
"`wire accept` takes a peer name, not a URL. \
Use `wire accept-invite {target}` to accept an invite URL."
);
} else {
pairing::cmd_pair_accept(&target, j)
}
}
Command::AcceptInvite { url, json } => pairing::cmd_accept(&url, json_default(json)),
Command::Whois {
handle,
json,
relay,
} => {
match handle.as_deref() {
Some(h) if !h.contains('@') => pairing::cmd_whois_local(h, json),
other => pairing::cmd_whois(other, json, relay.as_deref()),
}
}
Command::Add {
handle,
relay,
local_sister,
json,
} => pairing::cmd_add(&handle, relay.as_deref(), local_sister, json),
Command::Up {
relay,
offline,
with_local,
no_local,
json,
} => setup::cmd_up(
relay.as_deref(),
offline,
with_local.as_deref(),
no_local,
json,
),
Command::Doctor {
json,
recent_rejections,
} => status::cmd_doctor(json, recent_rejections),
Command::Upgrade {
check,
local,
restart_mcp,
refresh_stale_children,
json,
} => upgrade::cmd_upgrade(check, local, restart_mcp, refresh_stale_children, json),
Command::Service { action } => upgrade::cmd_service(action),
Command::Diag { action } => status::cmd_diag(action),
Command::Claim {
nick,
relay,
public_url,
hidden,
json,
} => identity::cmd_claim(&nick, relay.as_deref(), public_url.as_deref(), hidden, json),
Command::Profile { action } => identity::cmd_profile(action),
Command::Setup {
apply,
statusline,
remove,
} => {
if statusline {
setup::cmd_setup_statusline(apply, remove)
} else {
setup::cmd_setup(apply)
}
}
Command::Notify {
interval,
peer,
once,
json,
} => comms::cmd_notify(interval, peer.as_deref(), once, json),
Command::Nuke {
force,
purge,
dry_run,
really_this_machine,
json,
} => lifecycle::cmd_nuke(force, purge, dry_run, really_this_machine, json),
Command::Quiet { action } => lifecycle::cmd_quiet(action),
}
}
pub(crate) fn scan_jsonl_dir(dir: &std::path::Path) -> Result<Value> {
if !dir.exists() {
return Ok(json!({"files": 0, "events": 0}));
}
let mut files = 0usize;
let mut events = 0usize;
for entry in std::fs::read_dir(dir)? {
let path = entry?.path();
if path.extension().map(|x| x == "jsonl").unwrap_or(false)
&& !path
.file_name()
.and_then(|s| s.to_str())
.map(|n| n.ends_with(".pushed.jsonl"))
.unwrap_or(false)
{
files += 1;
if let Ok(body) = std::fs::read_to_string(&path) {
events += body.lines().filter(|l| !l.trim().is_empty()).count();
}
}
}
Ok(json!({"files": files, "events": events}))
}
pub(super) fn effective_peer_tier(trust: &Value, relay_state: &Value, handle: &str) -> String {
crate::trust::effective_tier(trust, relay_state, handle)
}
#[cfg(test)]
mod tier_tests {
use super::*;
use serde_json::json;
fn trust_with(handle: &str, tier: &str) -> Value {
json!({
"version": 1,
"agents": {
handle: {
"tier": tier,
"did": format!("did:wire:{handle}"),
"card": {"capabilities": ["wire/v3.1"]}
}
}
})
}
#[test]
fn pending_ack_when_verified_but_no_slot_token() {
let trust = trust_with("willard", "VERIFIED");
let relay_state = json!({
"peers": {
"willard": {
"relay_url": "https://relay",
"slot_id": "abc",
"slot_token": "",
}
}
});
assert_eq!(
effective_peer_tier(&trust, &relay_state, "willard"),
"PENDING_ACK"
);
}
#[test]
fn verified_when_slot_token_present() {
let trust = trust_with("willard", "VERIFIED");
let relay_state = json!({
"peers": {
"willard": {
"relay_url": "https://relay",
"slot_id": "abc",
"slot_token": "tok123",
}
}
});
assert_eq!(
effective_peer_tier(&trust, &relay_state, "willard"),
"VERIFIED"
);
}
#[test]
fn raw_tier_passes_through_for_non_verified() {
let trust = trust_with("willard", "UNTRUSTED");
let relay_state = json!({
"peers": {"willard": {"slot_token": ""}}
});
assert_eq!(
effective_peer_tier(&trust, &relay_state, "willard"),
"UNTRUSTED"
);
}
#[test]
fn pending_ack_when_relay_state_missing_peer() {
let trust = trust_with("willard", "VERIFIED");
let relay_state = json!({"peers": {}});
assert_eq!(
effective_peer_tier(&trust, &relay_state, "willard"),
"PENDING_ACK"
);
}
}
pub(super) fn parse_kind(s: &str) -> Result<u32> {
if let Ok(n) = s.parse::<u32>() {
return Ok(n);
}
for (id, name) in crate::signing::kinds() {
if *name == s {
return Ok(*id);
}
}
Ok(1)
}
fn cmd_group(cmd: GroupCommand) -> Result<()> {
match cmd {
GroupCommand::Create { name, json } => group::cmd_group_create(&name, json),
GroupCommand::Add { group, peer, json } => group::cmd_group_add(&group, &peer, json),
GroupCommand::Send {
group,
message,
json,
} => group::cmd_group_send(&group, &message, json),
GroupCommand::Tail { group, limit, json } => group::cmd_group_tail(&group, limit, json),
GroupCommand::List { json } => group::cmd_group_list(json),
GroupCommand::Invite { group, json } => group::cmd_group_invite(&group, json),
GroupCommand::Join { code, json } => group::cmd_group_join(&code, json),
}
}
fn cmd_mesh(cmd: MeshCommand) -> Result<()> {
match cmd {
MeshCommand::Status { stale_secs, json } => cmd_session_mesh_status(stale_secs, json),
MeshCommand::Broadcast {
kind,
scope,
exclude,
noreply,
body,
json,
} => mesh::cmd_mesh_broadcast(&kind, &scope, &exclude, noreply, &body, json),
MeshCommand::Role { action } => mesh::cmd_mesh_role(action),
MeshCommand::Route {
role,
strategy,
exclude,
kind,
body,
json,
} => mesh::cmd_mesh_route(&role, &strategy, &exclude, &kind, &body, json),
}
}
fn cmd_session(cmd: SessionCommand) -> Result<()> {
match cmd {
SessionCommand::New {
name,
relay,
with_local,
local_relay,
with_lan,
lan_relay,
with_uds,
uds_socket,
no_daemon,
local_only,
json,
} => session::cmd_session_new(
name.as_deref(),
&relay,
with_local,
&local_relay,
with_lan,
lan_relay.as_deref(),
with_uds,
uds_socket.as_deref(),
no_daemon,
local_only,
json,
),
SessionCommand::List { json } => session::cmd_session_list(json),
SessionCommand::ListLocal { json } => session::cmd_session_list_local(json),
SessionCommand::PairAllLocal {
settle_secs,
federation_relay,
json,
} => session::cmd_session_pair_all_local(settle_secs, &federation_relay, json),
SessionCommand::MeshStatus { stale_secs, json } => {
cmd_session_mesh_status(stale_secs, json)
}
SessionCommand::Env { name, json } => session::cmd_session_env(name.as_deref(), json),
SessionCommand::Current { json } => session::cmd_session_current(json),
SessionCommand::Bind { name, json } => cmd_session_bind(name.as_deref(), json),
SessionCommand::Destroy { name, force, json } => {
session::cmd_session_destroy(&name, force, json)
}
}
}
fn cmd_session_bind(name_arg: Option<&str>, json: bool) -> Result<()> {
let cwd = std::env::current_dir().with_context(|| "reading cwd")?;
let cwd_str = crate::session::normalize_cwd_key(&cwd);
let resolved_name = match name_arg {
Some(n) => crate::session::sanitize_name(n),
None => crate::session::sanitize_name(
cwd.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| anyhow!("cwd has no basename to derive session name from"))?,
),
};
let session_home = crate::session::session_dir(&resolved_name)?;
if !session_home.exists() {
bail!(
"session `{resolved_name}` does not exist (looked at {}). Create it first with `wire session new {resolved_name}` or pass an existing name.",
session_home.display()
);
}
let prior = crate::session::read_registry()
.ok()
.and_then(|r| r.by_cwd.get(&cwd_str).cloned());
if prior.as_deref() == Some(resolved_name.as_str()) {
if json {
println!(
"{}",
serde_json::to_string(&json!({
"cwd": cwd_str,
"session": resolved_name,
"changed": false,
}))?
);
} else {
println!("cwd `{cwd_str}` already bound to session `{resolved_name}` (no change)");
}
return Ok(());
}
if let Some(prior_name) = &prior {
eprintln!(
"wire session bind: cwd `{cwd_str}` was bound to `{prior_name}`; overwriting with `{resolved_name}`."
);
}
crate::session::update_registry(|reg| {
reg.by_cwd.insert(cwd_str.clone(), resolved_name.clone());
Ok(())
})?;
if json {
println!(
"{}",
serde_json::to_string(&json!({
"cwd": cwd_str,
"session": resolved_name,
"changed": true,
"previous": prior,
}))?
);
} else {
println!("bound cwd `{cwd_str}` → session `{resolved_name}`");
println!("(next `wire` invocation from this cwd will auto-detect into this session)");
}
Ok(())
}
pub(super) fn run_wire_with_home(
session_home: &std::path::Path,
args: &[&str],
) -> Result<std::process::ExitStatus> {
let bin = std::env::current_exe().with_context(|| "locating self exe")?;
let status = std::process::Command::new(&bin)
.env("WIRE_HOME", session_home)
.env_remove("RUST_LOG")
.env("WIRE_AUTO_INIT", "0")
.args(args)
.status()
.with_context(|| format!("spawning `wire {}`", args.join(" ")))?;
Ok(status)
}
pub(super) fn session_has_peer(session_home: &std::path::Path, peer_name: &str) -> bool {
val_session_relay_state(session_home)
.and_then(|v| v.get("peers").cloned())
.and_then(|p| p.get(peer_name).cloned())
.is_some()
}
fn val_session_relay_state(session_home: &std::path::Path) -> Option<Value> {
let path = session_home.join("config").join("wire").join("relay.json");
let bytes = std::fs::read(&path).ok()?;
serde_json::from_slice(&bytes).ok()
}
fn cmd_session_mesh_status(stale_secs: u64, as_json: bool) -> Result<()> {
use std::collections::BTreeMap;
let listing = crate::session::list_local_sessions()?;
let mut by_name: BTreeMap<String, crate::session::LocalSessionView> = BTreeMap::new();
for group in listing.local.into_values() {
for s in group {
by_name.entry(s.name.clone()).or_insert(s);
}
}
let sessions: Vec<crate::session::LocalSessionView> = by_name.into_values().collect();
let federation_only = listing.federation_only;
if sessions.is_empty() {
let msg = "no sister sessions with a local endpoint on this machine.".to_string();
if as_json {
println!(
"{}",
serde_json::to_string(&json!({
"sessions": [],
"edges": [],
"local_relay": null,
"federation_only": federation_only.iter().map(|f| &f.name).collect::<Vec<_>>(),
"summary": {
"session_count": 0,
"edge_count": 0,
"healthy": 0,
"stale": 0,
"asymmetric": 0,
},
"note": msg,
}))?
);
} else {
println!("{msg}");
println!("Use `wire session new --with-local` to create one.");
}
return Ok(());
}
struct SessionState {
view: crate::session::LocalSessionView,
relay_state: Value,
local_relay_url: Option<String>,
}
let mut sstates: Vec<SessionState> = Vec::with_capacity(sessions.len());
for s in sessions {
let relay_state = val_session_relay_state(&s.home_dir)
.unwrap_or_else(|| json!({"self": Value::Null, "peers": {}}));
let local_relay_url = s.local_endpoints.first().map(|e| e.relay_url.clone());
sstates.push(SessionState {
view: s,
relay_state,
local_relay_url,
});
}
let mut local_relays: BTreeMap<String, bool> = BTreeMap::new();
for s in &sstates {
if let Some(url) = &s.local_relay_url
&& !local_relays.contains_key(url)
{
let healthy = probe_relay_healthz(url);
local_relays.insert(url.clone(), healthy);
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut edges: Vec<Value> = Vec::new();
let mut healthy_count = 0u32;
let mut stale_count = 0u32;
let mut asymmetric_count = 0u32;
for i in 0..sstates.len() {
for j in (i + 1)..sstates.len() {
let a = &sstates[i];
let b = &sstates[j];
let b_key = b.view.handle.as_deref().unwrap_or(b.view.name.as_str());
let a_key = a.view.handle.as_deref().unwrap_or(a.view.name.as_str());
let a_to_b = probe_directed_edge(&a.relay_state, b_key, now);
let b_to_a = probe_directed_edge(&b.relay_state, a_key, now);
let bilateral = a_to_b.pinned && b_to_a.pinned;
let scope = match (a_to_b.scope.as_deref(), b_to_a.scope.as_deref()) {
(Some("local"), _) | (_, Some("local")) => "local",
(Some("federation"), _) | (_, Some("federation")) => "federation",
_ => "unknown",
};
let mut status = if bilateral { "healthy" } else { "asymmetric" };
if bilateral {
let either_stale = [&a_to_b, &b_to_a].iter().any(|d| match d.silent_secs {
Some(s) => s > stale_secs,
None => d.probed,
});
if either_stale {
status = "stale";
}
}
match status {
"healthy" => healthy_count += 1,
"stale" => stale_count += 1,
"asymmetric" => asymmetric_count += 1,
_ => {}
}
edges.push(json!({
"from": a.view.name,
"to": b.view.name,
"bilateral": bilateral,
"scope": scope,
"status": status,
"directions": {
a.view.name.clone(): direction_summary(&a_to_b),
b.view.name.clone(): direction_summary(&b_to_a),
},
}));
}
}
let summary = json!({
"sessions": sstates.iter().map(|s| json!({
"name": s.view.name,
"handle": s.view.handle,
"cwd": s.view.cwd,
"daemon_running": s.view.daemon_running,
"local_relay": s.local_relay_url,
})).collect::<Vec<_>>(),
"edges": edges,
"local_relays": local_relays.iter().map(|(url, healthy)| json!({
"url": url,
"healthy": healthy,
})).collect::<Vec<_>>(),
"federation_only": federation_only.iter().map(|f| &f.name).collect::<Vec<_>>(),
"summary": {
"session_count": sstates.len(),
"edge_count": edges.len(),
"healthy": healthy_count,
"stale": stale_count,
"asymmetric": asymmetric_count,
"stale_threshold_secs": stale_secs,
},
});
if as_json {
println!("{}", serde_json::to_string(&summary)?);
return Ok(());
}
println!(
"wire mesh: {} session(s), {} edge(s)",
sstates.len(),
edges.len()
);
for (url, healthy) in &local_relays {
let tick = if *healthy { "✓" } else { "✗" };
println!(" local-relay {url} {tick}");
}
if !federation_only.is_empty() {
print!(" federation-only sessions:");
for f in &federation_only {
print!(" {}", f.name);
}
println!();
}
let names: Vec<&str> = sstates.iter().map(|s| s.view.name.as_str()).collect();
let col_w = names.iter().map(|n| n.len()).max().unwrap_or(8).max(7) + 1;
print!("\n{:>col_w$}", "", col_w = col_w);
for n in &names {
print!("{n:>col_w$}");
}
println!();
for (i, row) in names.iter().enumerate() {
print!("{row:>col_w$}");
for (j, col) in names.iter().enumerate() {
let cell = if i == j {
"self".to_string()
} else {
let d = probe_directed_edge(&sstates[i].relay_state, col, now);
match d.scope.as_deref() {
Some("local") => "local".to_string(),
Some("federation") => "fed".to_string(),
_ => "—".to_string(),
}
};
print!("{cell:>col_w$}");
}
println!();
}
println!("\nHealth (stale threshold: {stale_secs}s):");
for e in &edges {
let from = e["from"].as_str().unwrap_or("?");
let to = e["to"].as_str().unwrap_or("?");
let scope = e["scope"].as_str().unwrap_or("?");
let status = e["status"].as_str().unwrap_or("?");
let mark = match status {
"healthy" => "✓",
"stale" => "⚠",
"asymmetric" => "!",
_ => "?",
};
let dirs = e["directions"].as_object().cloned().unwrap_or_default();
let mut details: Vec<String> = Vec::new();
for (who, d) in &dirs {
let silent = d.get("silent_secs").and_then(Value::as_u64);
let pinned = d.get("pinned").and_then(Value::as_bool).unwrap_or(false);
let probed = d.get("probed").and_then(Value::as_bool).unwrap_or(false);
let label = match (pinned, probed, silent) {
(false, _, _) => format!("{who} has not pinned"),
(true, false, _) => format!("{who} pinned but no endpoint to probe"),
(true, true, Some(s)) if s <= stale_secs => format!("{who} fresh ({s}s)"),
(true, true, Some(s)) => format!("{who} silent {s}s"),
(true, true, None) => format!("{who} never pulled"),
};
details.push(label);
}
println!(
" {mark} {from} ↔ {to} scope={scope} {status:>10} [{}]",
details.join(" | ")
);
}
Ok(())
}
#[derive(Default)]
struct DirectedEdge {
pinned: bool,
scope: Option<String>,
last_pull_at_unix: Option<u64>,
silent_secs: Option<u64>,
probed: bool,
event_count: usize,
}
fn probe_directed_edge(from_state: &Value, to_name: &str, now: u64) -> DirectedEdge {
let pinned = from_state
.get("peers")
.and_then(|p| p.get(to_name))
.is_some();
if !pinned {
return DirectedEdge::default();
}
let endpoints = crate::endpoints::peer_endpoints_in_priority_order(from_state, to_name);
let ep = match endpoints.into_iter().next() {
Some(e) => e,
None => {
return DirectedEdge {
pinned: true,
..Default::default()
};
}
};
let scope = Some(
match ep.scope {
crate::endpoints::EndpointScope::Local => "local",
crate::endpoints::EndpointScope::Lan => "lan",
crate::endpoints::EndpointScope::Uds => "uds",
crate::endpoints::EndpointScope::Federation => "federation",
}
.to_string(),
);
let client = crate::relay_client::RelayClient::new(&ep.relay_url);
let (count, last) = client
.slot_state(&ep.slot_id, &ep.slot_token)
.unwrap_or((0, None));
let silent = last.map(|t| now.saturating_sub(t));
DirectedEdge {
pinned: true,
scope,
last_pull_at_unix: last,
silent_secs: silent,
probed: true,
event_count: count,
}
}
fn direction_summary(d: &DirectedEdge) -> Value {
json!({
"pinned": d.pinned,
"scope": d.scope,
"probed": d.probed,
"last_pull_at_unix": d.last_pull_at_unix,
"silent_secs": d.silent_secs,
"event_count": d.event_count,
})
}
fn probe_relay_healthz(url: &str) -> bool {
let probe_url = format!("{}/healthz", url.trim_end_matches('/'));
let client = match reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_millis(500))
.build()
{
Ok(c) => c,
Err(_) => return false,
};
match client.get(&probe_url).send() {
Ok(r) => r.status().is_success(),
Err(_) => false,
}
}
fn json_default(explicit: bool) -> bool {
if explicit {
return true;
}
if std::env::var("WIRE_NO_AUTO_JSON").is_ok() {
return false;
}
use std::io::IsTerminal;
!std::io::stdout().is_terminal()
}
pub(super) fn process_alive_pid(pid: u32) -> bool {
crate::platform::process_alive(pid)
}
fn levenshtein_ci(a: &str, b: &str) -> usize {
let a: Vec<char> = a.to_ascii_lowercase().chars().collect();
let b: Vec<char> = b.to_ascii_lowercase().chars().collect();
let (a, b) = if a.len() < b.len() { (a, b) } else { (b, a) };
let (m, n) = (a.len(), b.len());
if m == 0 {
return n;
}
let mut prev: Vec<usize> = (0..=m).collect();
let mut curr = vec![0usize; m + 1];
for j in 1..=n {
curr[0] = j;
for i in 1..=m {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
curr[i] = std::cmp::min(
std::cmp::min(curr[i - 1] + 1, prev[i] + 1),
prev[i - 1] + cost,
);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[m]
}
pub fn closest_candidates(
needle: &str,
pool: &[String],
max_distance: usize,
max_results: usize,
) -> Vec<String> {
let mut scored: Vec<(usize, &String)> = pool
.iter()
.map(|c| (levenshtein_ci(needle, c), c))
.filter(|(d, _)| *d <= max_distance)
.collect();
scored.sort_by_key(|(d, _)| *d);
scored
.into_iter()
.take(max_results)
.map(|(_, c)| c.clone())
.collect()
}
pub(super) fn host_of_url(url: &str) -> String {
let no_scheme = url
.trim_start_matches("https://")
.trim_start_matches("http://");
no_scheme
.split('/')
.next()
.unwrap_or("")
.split(':')
.next()
.unwrap_or("")
.to_string()
}
pub(super) fn known_local_names() -> Vec<String> {
let mut names: Vec<String> = Vec::new();
if let Ok(trust) = config::read_trust() {
if let Some(agents) = trust.get("agents").and_then(Value::as_object) {
for (handle, agent) in agents {
names.push(handle.clone());
if let Some(did) = agent.get("did").and_then(Value::as_str) {
let ch = crate::character::Character::from_did(did);
names.push(ch.nickname);
}
}
}
}
if let Ok(sessions) = crate::session::list_sessions() {
for s in sessions {
names.push(s.name.clone());
if let Some(h) = &s.handle {
names.push(h.clone());
}
if let Some(ch) = &s.character {
names.push(ch.nickname.clone());
}
}
}
names.sort();
names.dedup();
names
}
#[cfg(test)]
mod scan_jsonl_dir_tests {
use super::*;
#[test]
fn scan_jsonl_dir_excludes_pushed_audit_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("alice.jsonl"),
"{\"event_id\":\"a\"}\n{\"event_id\":\"b\"}\n",
)
.unwrap();
let many: String = (0..100)
.map(|i| format!("{{\"event_id\":\"x{i}\",\"ts\":\"...\"}}\n"))
.collect();
std::fs::write(dir.path().join("alice.pushed.jsonl"), many).unwrap();
let result = scan_jsonl_dir(dir.path()).unwrap();
assert_eq!(
result["events"], 2,
"events count must include only live outbox lines, not pushed-log audit lines"
);
assert_eq!(
result["files"], 1,
"files count must reflect 1 live outbox file (the .pushed.jsonl audit log doesn't count as a queued-events surface)"
);
}
#[test]
fn scan_jsonl_dir_zero_when_only_pushed_log_present() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("alice.pushed.jsonl"),
"{\"event_id\":\"a\"}\n",
)
.unwrap();
let result = scan_jsonl_dir(dir.path()).unwrap();
assert_eq!(result["events"], 0);
assert_eq!(result["files"], 0);
}
#[test]
fn scan_jsonl_dir_returns_zero_for_missing_dir() {
let result = scan_jsonl_dir(std::path::Path::new("/nonexistent")).unwrap();
assert_eq!(result["events"], 0);
assert_eq!(result["files"], 0);
}
}