1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! `astrid who` — list connected clients with per-agent attribution.
//!
//! The daemon's `KernelRequest::GetStatus` exposes both a total
//! `connected_clients` count and a `connections_by_principal`
//! breakdown sourced from `Kernel::active_connections` (incremented
//! on socket handshake, decremented on disconnect, keyed by the
//! claimed principal). One row is emitted per `(principal,
//! connection)` pair so the operator sees a real per-agent picture
//! instead of a fabricated `default`-only roster.
//!
//! Platform attribution (CLI vs Discord vs Telegram) still needs the
//! `admin.agent.link` IPC tracked in #657. Until that lands, the
//! `PLATFORM` column reads `cli` for every row — the only uplink
//! shipping today — and the info-line footer points at the tracking
//! issue so operators know it's a placeholder.
use std::process::ExitCode;
use anyhow::Result;
use astrid_core::kernel_api::{KernelRequest, KernelResponse};
use clap::Args;
use colored::Colorize;
use serde::Serialize;
use crate::commands::daemon;
use crate::theme::Theme;
use crate::value_formatter::{ValueFormat, emit_structured};
#[derive(Args, Debug, Clone)]
pub(crate) struct WhoArgs {
/// Output format.
#[arg(long, default_value = "pretty")]
pub format: String,
}
/// JSON/YAML/TOML emission shape.
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Connection {
/// Agent principal.
pub agent: String,
/// Platform descriptor — `cli`, `discord`, `telegram` once the
/// link IPC ships; `unknown` until then.
pub platform: String,
}
/// Entry point for `astrid who`.
pub(crate) async fn run(args: WhoArgs) -> Result<ExitCode> {
let format = ValueFormat::parse(&args.format);
let socket_path = crate::socket_client::proxy_socket_path();
if !socket_path.exists() {
if format.is_pretty() {
println!("{}", Theme::info("No Astrid daemon is running."));
} else {
emit_structured(&Vec::<Connection>::new(), format)?;
}
return Ok(ExitCode::SUCCESS);
}
let mut client = match crate::socket_client::connect_kernel_for_workspace(None).await {
Ok(client) => client,
Err(error) => {
eprintln!(
"{}",
Theme::error(&format!("Failed to connect to daemon: {error:#}"))
);
return Ok(ExitCode::from(1));
},
};
let status = match client.request(KernelRequest::GetStatus).await {
Ok(KernelResponse::Status(status)) => status,
Ok(KernelResponse::Error(message)) => {
eprintln!(
"{}",
Theme::error(&format!("Daemon rejected status request: {message}"))
);
return Ok(ExitCode::from(1));
},
Ok(_) => {
eprintln!("{}", Theme::error("Unexpected response from daemon"));
return Ok(ExitCode::from(1));
},
Err(error) => {
eprintln!(
"{}",
Theme::error(&format!("Failed to query daemon: {error}"))
);
return Ok(ExitCode::from(1));
},
};
let connections: Vec<Connection> = match status {
s if !s.connections_by_principal.is_empty() => s
.connections_by_principal
.iter()
.flat_map(|pc| {
(0..pc.count).map(move |_| Connection {
agent: pc.principal.clone(),
platform: "cli".into(),
})
})
.collect(),
// Daemon doesn't expose per-principal yet (older build) — fall
// back to the bare count attributed to `default`. Matches the
// pre-#22 behaviour so a CLI/daemon version skew degrades
// gracefully instead of returning an empty roster.
s => {
let principal = astrid_core::PrincipalId::default();
(0..s.connected_clients)
.map(|_| Connection {
agent: principal.to_string(),
platform: "cli".into(),
})
.collect()
},
};
if !format.is_pretty() {
emit_structured(&connections, format)?;
return Ok(ExitCode::SUCCESS);
}
if connections.is_empty() {
println!("{}", Theme::info("No clients connected."));
return Ok(ExitCode::SUCCESS);
}
println!(
"{:<24} {:<12} {}",
"AGENT".bold(),
"PLATFORM".bold(),
"STATE".bold()
);
for c in &connections {
println!("{:<24} {:<12} {}", c.agent, c.platform, "active".green());
}
println!(
"\n{}",
Theme::info(
"Per-client identity attribution (idle time, platform user) needs `admin.agent.link` IPC — tracking #657."
)
);
// Use daemon helper to avoid unused warning until we add idle-time
// attribution.
let _ = daemon::format_uptime;
Ok(ExitCode::SUCCESS)
}