use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::cloud_client;
use crate::core::wrapped::WrappedReport;
const MAX_LABEL_LEN: usize = 60;
#[derive(Serialize, Deserialize)]
struct PublishPayload {
period: String,
tokens_saved: i64,
cost_avoided_usd: f64,
pricing_estimated: bool,
compression_rate_pct: f64,
#[serde(skip_serializing_if = "Option::is_none")]
display_name: Option<String>,
leaderboard_opt_in: bool,
}
fn build_payload(r: &WrappedReport, name: Option<&str>, leaderboard: bool) -> PublishPayload {
let display_name = name
.map(|s| sanitize(s.trim(), MAX_LABEL_LEN))
.filter(|s| !s.is_empty());
PublishPayload {
period: r.period.clone(),
tokens_saved: clamp_u64(r.tokens_saved),
cost_avoided_usd: r.cost_avoided_usd.max(0.0),
pricing_estimated: r.pricing_estimated,
compression_rate_pct: r.compression_rate_pct.clamp(0.0, 100.0),
display_name,
leaderboard_opt_in: leaderboard,
}
}
fn clamp_u64(v: u64) -> i64 {
i64::try_from(v).unwrap_or(i64::MAX)
}
fn shared_disclosure(has_name: bool) -> String {
let name = if has_name {
", and the display name you chose"
} else {
""
};
format!(
"Shared (aggregate numbers only): tokens saved, estimated USD, compression rate{name}.\n\
Never shared: your code, file contents, file paths, repo names, prompts or messages."
)
}
fn sanitize(s: &str, max: usize) -> String {
s.chars()
.filter(|c| !c.is_control() && *c != '<' && *c != '>')
.take(max)
.collect()
}
#[derive(Serialize, Deserialize, Clone)]
struct PublishedEntry {
id: String,
edit_token: String,
url: String,
period: String,
published_at: String,
#[serde(default)]
auto: bool,
#[serde(default)]
leaderboard: bool,
}
#[derive(Serialize, Deserialize, Default)]
struct PublishedStore {
cards: Vec<PublishedEntry>,
}
fn store_path() -> Option<PathBuf> {
let base = std::env::var("LEAN_CTX_DATA_DIR")
.map(PathBuf::from)
.ok()
.or_else(|| dirs::home_dir().map(|h| h.join(".lean-ctx")))?;
Some(base.join("wrapped").join("published.json"))
}
pub(crate) fn has_published() -> bool {
let store = PublishedStore::load();
!store.cards.is_empty()
}
pub(crate) fn has_leaderboard_entry() -> bool {
PublishedStore::load().cards.iter().any(|c| c.leaderboard)
}
#[derive(Serialize)]
pub(crate) struct LeaderboardStatus {
pub published: bool,
pub on_leaderboard: bool,
pub auto_submit: bool,
pub display_name: Option<String>,
pub url: Option<String>,
pub last_published_at: Option<String>,
}
pub(crate) fn leaderboard_status() -> LeaderboardStatus {
let cfg = crate::core::config::Config::load_global();
let store = PublishedStore::load();
let entry = store
.cards
.iter()
.find(|c| c.period == "all")
.or_else(|| store.cards.last());
LeaderboardStatus {
published: !store.cards.is_empty(),
on_leaderboard: store.cards.iter().any(|c| c.leaderboard),
auto_submit: cfg.gain.auto_publish,
display_name: cfg.gain.display_name.clone(),
url: entry.map(|c| c.url.clone()),
last_published_at: entry.map(|c| c.published_at.clone()),
}
}
pub(crate) fn submit_leaderboard(
name: Option<&str>,
) -> Result<cloud_client::PublishedCard, String> {
let period = "all";
let report = WrappedReport::generate(period);
if report.tokens_saved == 0 {
return Err("Nothing to publish yet — use lean-ctx for a bit, then try again.".to_string());
}
let mut cfg = crate::core::config::Config::load_global();
if let Some(n) = name.map(str::trim).filter(|n| !n.is_empty())
&& cfg.gain.display_name.as_deref() != Some(n)
{
cfg.gain.display_name = Some(n.to_string());
if let Err(e) = cfg.save() {
tracing::warn!("Could not save display name: {e}");
}
}
let effective_name = name
.map(str::to_string)
.or_else(|| cfg.gain.display_name.clone());
publish_report(&report, period, effective_name.as_deref(), true, false)
}
pub(crate) fn set_auto_submit(on: bool) -> Result<(), String> {
crate::core::config::Config::update_global(|c| {
c.gain.auto_publish = on;
if on {
c.gain.leaderboard = true;
}
})
.map(|_| ())
.map_err(|e| format!("could not save config: {e}"))
}
fn linkable_card(store: &PublishedStore) -> Option<&PublishedEntry> {
store
.cards
.iter()
.filter(|c| !c.edit_token.is_empty())
.max_by_key(|c| (c.period == "all", c.published_at.clone()))
}
pub(crate) fn link(code: Option<&str>) {
let store = PublishedStore::load();
let Some(card) = linkable_card(&store) else {
eprintln!(
"No leaderboard card on this machine yet.\n\
1. Publish: lean-ctx gain --publish --leaderboard\n\
2. Then link: lean-ctx gain --link"
);
std::process::exit(1);
};
match code {
None => match cloud_client::link_wrapped_start(&card.id, &card.edit_token) {
Ok(minted) => {
let mins = (minted.expires_in_secs / 60).max(1);
println!("Pairing code: {}", minted.code);
println!();
println!("On your other machine, run within {mins} minutes:");
println!(" lean-ctx gain --link {}", minted.code);
println!();
println!(
"Both machines will then appear as one combined leaderboard entry \
(tokens summed). No account needed — the code proves card ownership."
);
}
Err(e) => {
eprintln!("Could not create a pairing code: {e}");
std::process::exit(1);
}
},
Some(code) => match cloud_client::link_wrapped_complete(&card.id, &card.edit_token, code) {
Ok(()) => {
println!(
"Linked! This machine and the code's machine now stack as one \
leaderboard entry."
);
println!(
"Link more machines anytime: lean-ctx gain --link (on any linked machine)"
);
}
Err(e) => {
eprintln!("Could not complete the link: {e}");
std::process::exit(1);
}
},
}
}
pub(crate) fn link_start() -> Result<cloud_client::LinkCode, String> {
let store = PublishedStore::load();
let card = linkable_card(&store)
.ok_or("No published card on this machine yet. Submit to the leaderboard first.")?;
cloud_client::link_wrapped_start(&card.id, &card.edit_token)
}
pub(crate) fn link_complete(code: &str) -> Result<(), String> {
let store = PublishedStore::load();
let card = linkable_card(&store)
.ok_or("No published card on this machine yet. Submit to the leaderboard first.")?;
cloud_client::link_wrapped_complete(&card.id, &card.edit_token, code)
}
impl PublishedStore {
fn load() -> Self {
store_path()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
fn save(&self) -> Result<(), String> {
let Some(path) = store_path() else {
return Ok(());
};
let json = serde_json::to_string_pretty(self)
.map_err(|e| format!("could not serialize published-card store: {e}"))?;
crate::config_io::write_atomic(&path, &json)
}
}
fn publisher_agent_id() -> String {
std::env::var("LEAN_CTX_AGENT_ID")
.or_else(|_| std::env::var("LCTX_AGENT_ID"))
.unwrap_or_else(|_| "local".to_string())
}
fn publish_report(
report: &WrappedReport,
period: &str,
name: Option<&str>,
leaderboard: bool,
auto: bool,
) -> Result<cloud_client::PublishedCard, String> {
use crate::core::agent_identity;
let payload = build_payload(report, name, leaderboard);
let payload_json =
serde_json::to_string(&payload).map_err(|e| format!("could not build payload: {e}"))?;
let agent = publisher_agent_id();
let signing_key = agent_identity::get_or_create_keypair(&agent)
.map_err(|e| format!("could not load publisher key: {e}"))?;
let public_key = agent_identity::hex_encode(&signing_key.verifying_key().to_bytes());
let signature = agent_identity::hex_encode(&agent_identity::sign_bytes_with(
&signing_key,
payload_json.as_bytes(),
));
let envelope = serde_json::json!({
"payload_json": payload_json,
"public_key": &public_key,
"signature": signature,
});
let mut card = cloud_client::publish_wrapped(&envelope)?;
if card.edit_token.is_none() {
let stored = PublishedStore::load()
.cards
.into_iter()
.find(|entry| entry.id == card.id && !entry.edit_token.is_empty())
.map(|entry| entry.edit_token);
card.edit_token = if let Some(token) = stored {
Some(token)
} else {
let nonce = card.edit_token_challenge.as_deref().ok_or_else(|| {
format!(
"card {} refreshed, but the server did not provide an ownership-recovery challenge",
card.id
)
})?;
let proof = crate::core::wrapped::edit_token_recovery_message(&card.id, nonce);
let recovery_signature = agent_identity::hex_encode(&agent_identity::sign_bytes_with(
&signing_key,
proof.as_bytes(),
));
Some(cloud_client::recover_wrapped_edit_token(
&card.id,
nonce,
&public_key,
&recovery_signature,
)?)
};
}
record_published(&mut card, period, auto, leaderboard);
Ok(card)
}
fn record_published(
card: &mut cloud_client::PublishedCard,
period: &str,
auto: bool,
leaderboard: bool,
) {
let mut store = PublishedStore::load();
for stale in store
.cards
.iter()
.filter(|c| c.period == period && c.id != card.id && !c.edit_token.is_empty())
{
let _ = cloud_client::unpublish_wrapped(&stale.id, &stale.edit_token);
}
let edit_token = card.edit_token.clone().unwrap_or_else(|| {
store
.cards
.iter()
.find(|c| c.id == card.id)
.map(|c| c.edit_token.clone())
.unwrap_or_default()
});
store.cards.retain(|c| c.period != period);
store.cards.push(PublishedEntry {
id: card.id.clone(),
edit_token: edit_token.clone(),
url: card.url.clone(),
period: period.to_string(),
published_at: chrono::Utc::now().to_rfc3339(),
auto,
leaderboard,
});
if let Err(e) = store.save() {
tracing::warn!("Published, but could not save local record: {e}");
return;
}
let persisted = PublishedStore::load()
.cards
.iter()
.any(|entry| entry.id == card.id && entry.edit_token == edit_token);
if !persisted {
tracing::warn!("Published, but local ownership record verification failed");
return;
}
if leaderboard && !edit_token.is_empty() && cloud_client::is_logged_in() {
match cloud_client::claim_wrapped(&card.id, &edit_token) {
Ok(()) => card.account_claimed = true,
Err(e) => {
tracing::warn!("Published, but could not link this card to your account: {e}");
}
}
}
}
pub(crate) fn publish(period: &str, name: Option<&str>, leaderboard: bool) {
let report = WrappedReport::generate(period);
if report.tokens_saved == 0 {
println!("Nothing to publish yet — use lean-ctx for a bit, then try again.");
return;
}
let mut cfg = crate::core::config::Config::load_global();
if let Some(n) = name.map(str::trim).filter(|n| !n.is_empty())
&& cfg.gain.display_name.as_deref() != Some(n)
{
cfg.gain.display_name = Some(n.to_string());
if let Err(e) = cfg.save() {
tracing::warn!("Could not save display name: {e}");
}
}
let effective_name = name
.map(str::to_string)
.or_else(|| cfg.gain.display_name.clone());
match publish_report(
&report,
period,
effective_name.as_deref(),
leaderboard,
false,
) {
Ok(card) => {
println!("Published: {}", card.url);
println!("{}", shared_disclosure(effective_name.is_some()));
if crate::core::share::copy_to_clipboard(&card.url) {
println!("URL copied to clipboard — paste it anywhere.");
}
if leaderboard {
if let Some(base) = card.url.split("/w/").next() {
println!("Listed on the community leaderboard: {base}/metrics#leaderboard");
}
if card.account_claimed {
println!(
"Linked this leaderboard card to your account. Cards published from your other \
signed-in machines stack under the same leaderboard entry."
);
} else {
if cloud_client::is_logged_in() {
println!(
"Account link was not confirmed; the card is published and remains under your control."
);
}
println!();
println!(" ┌─ Multiple machines? ─────────────────────────────────┐");
println!(" │ Combine them into one leaderboard entry: │");
println!(" │ lean-ctx gain --link (no account needed) │");
println!(" │ Or use the dashboard: http://localhost:3333 │");
println!(" └─────────────────────────────────────────────────────┘");
}
if effective_name.is_none() {
println!(
"Tip: claim a handle so you're not listed as \"anonymous\" — \
lean-ctx gain --publish --leaderboard --name=\"your handle\""
);
}
} else {
println!(
"Tip: also appear on the public leaderboard at https://leanctx.com/metrics — \
re-run with lean-ctx gain --publish --leaderboard"
);
}
println!(
"Remove anytime with: lean-ctx gain --unpublish={}",
card.id
);
}
Err(e) => {
eprintln!("Publish failed: {e}");
std::process::exit(1);
}
}
}
pub(crate) fn maybe_auto_publish(period: &str) {
let cfg = crate::core::config::Config::load_global();
let g = &cfg.gain;
if !g.auto_publish {
return;
}
if !auto_publish_due(
g.last_auto_publish.as_deref(),
g.auto_publish_interval_hours,
) {
return;
}
let report = WrappedReport::generate(period);
if report.tokens_saved == 0 {
return;
}
let disclose_name = g.display_name.is_some();
let stored_leaderboard = PublishedStore::load()
.cards
.iter()
.find(|c| c.period == period)
.is_some_and(|c| c.leaderboard);
let leaderboard = g.leaderboard || stored_leaderboard;
match publish_report(
&report,
period,
g.display_name.as_deref(),
leaderboard,
true,
) {
Ok(card) => {
let mut cfg = cfg;
cfg.gain.last_auto_publish = Some(chrono::Utc::now().to_rfc3339());
if let Err(e) = cfg.save() {
tracing::warn!("Auto-published, but could not record timestamp: {e}");
}
println!("\nAuto-published your recap: {}", card.url);
println!("{}", shared_disclosure(disclose_name));
println!(" (disable with: lean-ctx config set gain.auto_publish false)");
}
Err(e) => tracing::warn!("Auto-publish skipped: {e}"),
}
}
pub(crate) fn maybe_auto_publish_background() {
let cfg = crate::core::config::Config::load();
let g = &cfg.gain;
if !g.auto_publish {
return;
}
if !auto_publish_due(
g.last_auto_publish.as_deref(),
g.auto_publish_interval_hours,
) {
return;
}
std::thread::spawn(|| publish_in_background("all"));
}
fn publish_in_background(period: &str) {
let cfg = crate::core::config::Config::load_global();
let g = &cfg.gain;
if !g.auto_publish
|| !auto_publish_due(
g.last_auto_publish.as_deref(),
g.auto_publish_interval_hours,
)
{
return;
}
let report = WrappedReport::generate(period);
if report.tokens_saved == 0 {
return;
}
let stored_leaderboard = PublishedStore::load()
.cards
.iter()
.find(|c| c.period == period)
.is_some_and(|c| c.leaderboard);
let leaderboard = g.leaderboard || stored_leaderboard;
match publish_report(
&report,
period,
g.display_name.as_deref(),
leaderboard,
true,
) {
Ok(card) => {
let mut cfg = cfg;
cfg.gain.last_auto_publish = Some(chrono::Utc::now().to_rfc3339());
if let Err(e) = cfg.save() {
tracing::warn!("Background auto-publish: could not record timestamp: {e}");
}
tracing::info!("Background auto-published recap: {}", card.url);
}
Err(e) => tracing::warn!("Background auto-publish skipped: {e}"),
}
}
fn auto_publish_due(last: Option<&str>, interval_hours: u64) -> bool {
let Some(last) = last else {
return true;
};
let Ok(prev) = chrono::DateTime::parse_from_rfc3339(last) else {
return true;
};
let elapsed = chrono::Utc::now().signed_duration_since(prev.with_timezone(&chrono::Utc));
let interval = i64::try_from(interval_hours.max(1)).unwrap_or(i64::MAX);
elapsed.num_hours() >= interval
}
pub(crate) fn unpublish(id: Option<&str>) {
let mut store = PublishedStore::load();
let entry = match id {
Some(id) => store.cards.iter().find(|c| c.id == id).cloned(),
None => store.cards.last().cloned(),
};
let Some(entry) = entry else {
match id {
Some(id) => println!("No published card with id {id} found locally."),
None => println!("No published cards found. Publish one with: lean-ctx gain --publish"),
}
return;
};
match cloud_client::unpublish_wrapped(&entry.id, &entry.edit_token) {
Ok(()) => {
store.cards.retain(|c| c.id != entry.id);
let _ = store.save();
println!("Unpublished {} ({})", entry.id, entry.url);
}
Err(e) => {
eprintln!("Unpublish failed: {e}");
std::process::exit(1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn report() -> WrappedReport {
WrappedReport {
period: "week".into(),
tokens_saved: 480_600_000,
tokens_input: 600_000_000,
cost_avoided_usd: 1441.79,
total_commands: 1234,
sessions_count: 56,
top_commands: vec![
("ctx_search".into(), 100, 60.0),
("ctx_read".into(), 80, 40.0),
],
compression_rate_pct: 91.2,
files_touched: 789,
daily_savings: vec![1, 2, 3],
bounce_tokens: 100,
model_key: "claude-opus".into(),
pricing_estimated: true,
percentile: Some(99),
}
}
#[test]
fn payload_carries_only_minimal_aggregates() {
let p = build_payload(&report(), Some("yvesg"), false);
let v = serde_json::to_value(&p).unwrap();
let obj = v.as_object().unwrap();
let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(
keys,
vec![
"compression_rate_pct",
"cost_avoided_usd",
"display_name",
"leaderboard_opt_in",
"period",
"pricing_estimated",
"tokens_saved",
]
);
}
#[test]
fn no_name_omits_display_name() {
let p = build_payload(&report(), None, false);
assert!(p.display_name.is_none());
let v = serde_json::to_value(&p).unwrap();
assert!(v.as_object().unwrap().get("display_name").is_none());
}
#[test]
fn leaderboard_flag_sets_opt_in() {
assert!(!build_payload(&report(), None, false).leaderboard_opt_in);
assert!(build_payload(&report(), None, true).leaderboard_opt_in);
}
#[test]
fn auto_publish_due_throttle() {
assert!(auto_publish_due(None, 24));
assert!(auto_publish_due(Some("not-a-timestamp"), 24));
let now = chrono::Utc::now().to_rfc3339();
assert!(!auto_publish_due(Some(&now), 24));
let two_days_ago = (chrono::Utc::now() - chrono::Duration::hours(48)).to_rfc3339();
assert!(auto_publish_due(Some(&two_days_ago), 24));
assert!(!auto_publish_due(Some(&now), 0));
}
#[test]
fn sanitizes_markup_and_truncates() {
assert_eq!(sanitize("ctx_search", MAX_LABEL_LEN), "ctx_search");
assert_eq!(sanitize("<script>", MAX_LABEL_LEN), "script");
assert_eq!(
sanitize(&"a".repeat(100), MAX_LABEL_LEN).chars().count(),
MAX_LABEL_LEN
);
}
#[test]
fn display_name_is_sanitized_and_capped() {
let p = build_payload(&report(), Some(" <b>hi</b> "), false);
let name = p.display_name.unwrap();
assert!(!name.contains('<') && !name.contains('>'));
assert!(name.chars().count() <= MAX_LABEL_LEN);
}
#[test]
fn compression_is_clamped_into_range() {
let mut r = report();
r.compression_rate_pct = 250.0;
let p = build_payload(&r, None, false);
assert!((0.0..=100.0).contains(&p.compression_rate_pct));
}
#[test]
fn published_token_store_roundtrips_atomically() {
let _isolated = crate::core::data_dir::isolated_data_dir();
let store = PublishedStore {
cards: vec![PublishedEntry {
id: "card-1".into(),
edit_token: "secret-token".into(),
url: "https://leanctx.com/w/card-1".into(),
period: "all".into(),
published_at: "2026-01-01T00:00:00Z".into(),
auto: false,
leaderboard: true,
}],
};
store.save().unwrap();
let loaded = PublishedStore::load();
assert_eq!(loaded.cards.len(), 1);
assert_eq!(loaded.cards[0].edit_token, "secret-token");
}
}