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
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ curl -fsSL https://headsdown.app/install.sh | sh
# Authenticate (opens browser for approval)
hd auth

# Install HeadsDown into local agent tools
hd install claude
hd install --all
hd doctor claude

# Check your current status and availability
hd status
hd availability
Expand Down Expand Up @@ -93,8 +98,13 @@ hd watch
| `hd limited [duration]` | Set mode to limited |
| `hd verdict "desc"` | Submit a task proposal and get a verdict |
| `hd watch` | Live-updating status dashboard |
| `hd doctor` | Check CLI health and connectivity |
| `hd update` | Self-update to the latest version |
| `hd install <tool>` | Install the HeadsDown integration for a supported local tool, such as `claude`, `pi`, or `codex` |
| `hd install --all` | Install HeadsDown integrations for every detected supported tool |
| `hd doctor [tool]` | Check CLI health or local integration health |
| `hd doctor --all` | Check every supported local integration without printing sensitive local content |
| `hd update [tool]` | Refresh installed HeadsDown integrations, or use `--cli` to self-update the CLI binary |
| `hd update --all` | Refresh HeadsDown integrations for detected supported tools |
| `hd remove <tool>` | Remove the HeadsDown integration from a tool without uninstalling the tool itself |
| `hd hook install` | Install git hooks (auto-busy on branch switch) |
| `hd hook uninstall` | Remove git hooks |
| `hd hook status` | Show git hook status |
Expand Down Expand Up @@ -141,6 +151,37 @@ hd focus
hd standup
```

## Local Agent Integrations

Install means “install the HeadsDown integration for this tool,” not “install Claude Code, Pi, or Codex itself.” The installer only writes HeadsDown-owned integration artifacts and is safe to re-run:

```sh
hd install claude
hd install pi
hd install codex
hd install --all --dry-run
hd install --all --yes
```

Refresh or remove integrations with the same short command shape:

```sh
hd update
hd update claude
hd update --all --yes
hd remove claude
```

Doctor reports derived health facts only:

```sh
hd doctor
hd doctor claude
hd doctor --all
```

Integration diagnostics do not print prompts, transcripts, source code, diffs, file paths, repository names, logs, tokens, or raw config content. Local integrations keep execution context local; hosted HeadsDown receives only structured routing metadata when hosted features are used.

## Git Hooks

Auto-set your availability based on git activity:
Expand Down
2 changes: 2 additions & 0 deletions src/commands/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ pub fn set(name: &str, command: &str) -> Result<()> {
"presets",
"preset",
"watch",
"install",
"doctor",
"update",
"remove",
"hook",
"telemetry",
"alias",
Expand Down
44 changes: 18 additions & 26 deletions src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,29 @@ use crate::config;
use crate::format;

pub async fn run(api_url: &str, json: bool) -> Result<()> {
let mut checks: Vec<(&str, bool, String)> = Vec::new();
let mut checks: Vec<(String, bool, String)> = Vec::new();

// Check 1: CLI version
let version = env!("CARGO_PKG_VERSION");
checks.push(("CLI version", true, version.to_string()));
checks.push(("CLI version".to_string(), true, version.to_string()));

// Check 2: Config directory
let config_ok = config::config_dir().is_ok();
let config_detail = match config::config_dir() {
Ok(dir) => format!("{}", dir.display()),
Ok(_) => "Available".to_string(),
Err(e) => format!("Error: {}", e),
};
checks.push(("Config directory", config_ok, config_detail));
checks.push(("Config directory".to_string(), config_ok, config_detail));

// Check 3: Credentials
let creds = auth::load_token();
let creds_ok = matches!(&creds, Ok(Some(_)));
let creds_detail = match &creds {
Ok(Some(token)) => {
if token.starts_with("hd_") {
format!(
"{}...{}",
&token[..6],
&token[token.len().saturating_sub(4)..]
)
} else {
"Present (unknown format)".to_string()
}
}
Ok(Some(_)) => "Present".to_string(),
Ok(None) => "Not found. Run `hd auth`".to_string(),
Err(e) => format!("Error: {}", e),
Err(_) => "Error reading credentials".to_string(),
};
checks.push(("Credentials", creds_ok, creds_detail));
checks.push(("Credentials".to_string(), creds_ok, creds_detail));

// Check 4: API connectivity
let http = reqwest::Client::builder()
Expand All @@ -55,7 +45,7 @@ pub async fn run(api_url: &str, json: bool) -> Result<()> {
}
Err(e) => (false, format!("Cannot reach {}: {}", api_url, e)),
};
checks.push(("API connectivity", api_ok, api_detail));
checks.push(("API connectivity".to_string(), api_ok, api_detail));

// Check 5: Authentication validity
let auth_ok;
Expand All @@ -66,11 +56,9 @@ pub async fn run(api_url: &str, json: bool) -> Result<()> {
.execute(r#"query { profile { name email } }"#, None)
.await
{
Ok(data) => {
let name = data["profile"]["name"].as_str().unwrap_or("Unknown");
let email = data["profile"]["email"].as_str().unwrap_or("Unknown");
Ok(_) => {
auth_ok = true;
auth_detail = format!("{} ({})", name, email);
auth_detail = "Valid".to_string();
}
Err(e) => {
auth_ok = false;
Expand All @@ -81,23 +69,27 @@ pub async fn run(api_url: &str, json: bool) -> Result<()> {
auth_ok = false;
auth_detail = "Skipped (no credentials)".to_string();
};
checks.push(("Authentication", auth_ok, auth_detail));
checks.push(("Authentication".to_string(), auth_ok, auth_detail));

// Check 6: Config file
let cfg_result = config::load();
let (cfg_ok, cfg_detail) = match cfg_result {
Ok(_) => (true, "Valid".to_string()),
Err(e) => (false, format!("Error: {}", e)),
Err(_) => (false, "Invalid or unreadable config file".to_string()),
};
checks.push(("Config file", cfg_ok, cfg_detail));
checks.push(("Config file".to_string(), cfg_ok, cfg_detail));

// Check 7: OS/Arch
checks.push((
"Platform",
"Platform".to_string(),
true,
format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH),
));

for (name, ok, detail) in crate::commands::integrations::default_doctor_checks()? {
checks.push((name, ok, detail));
}

if json {
let json_checks: Vec<serde_json::Value> = checks
.iter()
Expand Down
Loading
Loading