Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ autter stats <start_sha>..<end_sha> --json

It computes % AI code, AI lines generated vs committed, acceptance rates, and human overrides, broken down by tool and model. More in the [stats command reference](https://autter.dev/docs/cli/reference#stats).

For a year at a glance, `autter wrapped` draws a screenshot-ready card of your AI-assisted coding in the repository: AI share by month, top agent and model, most-prompted file, longest prompt, streak and busiest weekday. `--json` prints the same numbers, and `--share` publishes the card as a page with a share image. Prompt text never leaves the machine unless you pass `--include-prompt-text`.

<details>
<summary>Example JSON output</summary>

Expand Down
32 changes: 32 additions & 0 deletions changelog/wrapped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# `autter wrapped`

## Summary

A Spotify-Wrapped-style summary of your year of AI-assisted coding in a repository, drawn as a 60-column card made for screenshots, with a JSON form that the share-image pipeline consumes.

## Added

- **`autter wrapped [--year <yyyy>] [--author <email> | --all-authors] [--json] [--include-prompt-text] [--no-color]`**: walks the year's commits (yours by default, from `git user.email`), reads each authorship note and folds the per-commit stats into one summary:
- the AI share of added lines, overall and by month;
- the top agent and top model by accepted lines, with the full ranked lists;
- the file with the most distinct prompts attached to it, with the top five;
- the longest first prompt of the year by character count;
- the longest streak of days with an AI-attributed commit and the busiest weekday.
- The terminal card shows the headline percentage in block digits, a twelve-bar monthly chart, the rows above and an `autter.dev/wrapped` footer. It honors `NO_COLOR` and `--no-color`, falls back to ASCII on terminals without Unicode, and is deterministic for a given summary so screenshots come out the same every time.
- With fewer than three noted commits in the year, the command prints a short note instead of a card.

## Sharing

- **`--share`** publishes the summary as a page at `app.autter.dev/wrapped/<token>` with a 1200x630 share image, prints the link after the card and opens it in the browser at a terminal. Sharing again for the same user, year and repository updates the same page and keeps the link. With `--json`, stdout stays the exact JSON contract and the link goes to stderr.
- **`--hide-repo`** shows no repository name on the page.
- **`--unshare <url-or-token>`** takes a page down.

Sharing needs a connected account and sends nothing in local mode. Each server refusal is one line: not signed in, not a member of the organization, too large, nothing to share yet, or the server's own message. `app.autter.dev/wrapped/demo` shows a sample page.

## Privacy

Nothing leaves the machine unless you pass `--share`, and then only the JSON summary. The longest prompt is measured from transcripts already in the local cache; prompts that only exist remotely are skipped. Its text is never printed or included in the JSON unless `--include-prompt-text` is passed, and then only the first 120 characters.

## JSON

`autter wrapped --json` prints a `wrapped/1.0.0` document: `year`, `author`, `commits`, `commits_with_authorship`, `added_lines`, `ai_lines`, `human_lines`, `untracked_lines`, `ai_percent`, `by_month` (12 entries of `month`, `ai_accepted`, `added_lines`, `ai_percent`, `commits`), `top_agent`, `top_model`, `agents`, `models`, `most_prompted_file`, `files`, `prompts_total`, `prompts_with_transcript`, `longest_prompt` (`chars`, `words`, `agent`, `model`, `commit`, `snippet`), `busiest_day` and `streak_days`. The walk stops after 5,000 commits and says so in `capped`.
32 changes: 32 additions & 0 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,19 @@ impl ApiContext {
(agent, request)
}

/// Create a DELETE request with common headers (User-Agent, X-Distinct-ID).
pub fn http_delete(url: &str, timeout_secs: Option<u64>) -> (ureq::Agent, ureq::Request) {
let agent = http::build_agent(timeout_secs);
let request = agent
.delete(url)
.set(
"User-Agent",
&format!("autter/{}", env!("CARGO_PKG_VERSION")),
)
.set("X-Distinct-ID", &config::get_or_create_distinct_id());
(agent, request)
}

/// Create a new API context, automatically using stored credentials if available
/// If base_url is None, uses api_base_url from config (which can be set via config file, env var, or defaults)
/// Uses Config::fresh() to support runtime config updates (daemon mode)
Expand Down Expand Up @@ -648,6 +661,25 @@ impl ApiContext {

http::send(request).map_err(|e| AutterError::Generic(format!("HTTP request failed: {}", e)))
}

/// Make a DELETE request
pub fn delete(&self, endpoint: &str) -> Result<http::Response, AutterError> {
let url = self.build_url(endpoint)?;

let (_agent, mut request) = Self::http_delete(&url, self.timeout_secs);

if let Some(api_key) = &self.api_key {
request = request.set("X-API-Key", api_key);
if let Some(identity) = &self.author_identity {
request = request.set("X-Author-Identity", identity);
}
}
if let Some(token) = &self.auth_token {
request = request.set("Authorization", &format!("Bearer {}", token));
}

http::send(request).map_err(|e| AutterError::Generic(format!("HTTP request failed: {}", e)))
}
}

/// API client wrapper
Expand Down
16 changes: 15 additions & 1 deletion src/api/data_plane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
//! endpoints on the server.

use crate::api::client::ApiClient;
use crate::api::types::{CommitAuthorshipSummary, FileChangeCount, FileChangeUploadResponse};
use crate::api::types::{
CommitAuthorshipSummary, FileChangeCount, FileChangeUploadResponse, RepoBadgeLink,
};
use crate::error::AutterError;

fn require_success(
Expand Down Expand Up @@ -61,6 +63,18 @@ impl ApiClient {
serde_json::from_str(&body).map_err(AutterError::JsonError)
}

/// Ask for (or create) the public token behind the repository's live
/// `AI-written` badge, served at `/api/public/repos/<token>/badge.svg`.
pub fn create_repo_badge_link(&self, repo_url: &str) -> Result<RepoBadgeLink, AutterError> {
let request = serde_json::json!({ "repo_url": repo_url });
let body = require_success(
self.context()
.post_json("/api/cli/repo-badge-link", &request)?,
"Repo badge link",
)?;
serde_json::from_str(&body).map_err(AutterError::JsonError)
}

/// Probe the server-side data plane without exposing or opening an org DB
/// connection from the CLI.
pub fn check_data_plane(&self) -> Result<(), AutterError> {
Expand Down
1 change: 1 addition & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod data_plane;
pub mod metrics;
pub mod notes;
pub mod types;
pub mod wrapped_share;

pub use client::{ApiClient, ApiContext};
pub use metrics::upload_metrics_with_retry;
Expand Down
14 changes: 14 additions & 0 deletions src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,20 @@ pub struct CommitAuthorshipSummary {
pub hunks: serde_json::Value,
}

/// Response of `POST /api/cli/repo-badge-link`: the public token behind a
/// repository's live `AI-written` badge.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RepoBadgeLink {
/// Public token that names the repository in the badge route.
pub token: String,
/// Full badge URL, when the server prefers to state it.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub badge_url: Option<String>,
/// The repository's Autter page, which the badge links to.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page_url: Option<String>,
}

/// Aggregated file-change count uploaded by the daemon.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileChangeCount {
Expand Down
Loading
Loading