Skip to content

Commit 48a8d05

Browse files
committed
feat: add contribai prs command — filtered PR list (v6.8.0)
The `stats` command shows only the last 5 PRs as a teaser. To see all open PRs (or merged-only, closed-only, etc.) users currently have to run `sqlite3 ~/.contribai/memory.db ...`. This adds a proper CLI for that. ## Flags - --status open|merged|closed|failed|all — case-insensitive, default "all". Reuses Memory::get_prs which already supports filtering at the SQL layer. - --limit N (default 20) — passed to the SQL LIMIT. - --json — emit a JSON array; pr_number parsed to int so consumers can sort/compare numerically. ## Pretty mode - Color-codes status (green=merged, cyan=open, red=closed, yellow=failed). - Renders ISO timestamp trimmed to "YYYY-MM-DD HH:MM" for terminal width, with PR#, repo, title, and a clickable URL line. 4 unit tests on the JSON converter (pr_number int parsing, fallback to string when unparseable) and the timestamp trimmer (ISO trim and short-input passthrough). Smoke-tested against real memory.db: pretty mode shows 3 open PRs correctly, --json --limit 1 emits a single well-formed object.
1 parent 582dded commit 48a8d05

9 files changed

Lines changed: 195 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ It discovers repos, analyzes code, generates fixes, and submits pull requests
1111
**It is NOT** a library/SDK, web app, or CLI tool intended for end-user consumption.
1212
It is itself an AI agent that operates on other GitHub repositories.
1313

14-
> **v6.7.0 — Primary implementation is Rust** (`crates/contribai-rs/`).
14+
> **v6.8.0 — Primary implementation is Rust** (`crates/contribai-rs/`).
1515
> Python code is in `python/` (legacy v4.1.0, kept for reference).
1616
1717
## Tech Stack
@@ -35,7 +35,7 @@ It is itself an AI agent that operates on other GitHub repositories.
3535

3636
```
3737
ContribAI/
38-
├── crates/contribai-rs/ ← PRIMARY: Rust v6.7.0
38+
├── crates/contribai-rs/ ← PRIMARY: Rust v6.8.0
3939
│ ├── src/
4040
│ │ ├── main.rs entry point
4141
│ │ ├── lib.rs library root
@@ -74,7 +74,7 @@ ContribAI/
7474
│ │ ├── web/mod.rs axum dashboard API
7575
│ │ ├── sandbox/sandbox.rs Docker + ast fallback
7676
│ │ └── tools/protocol.rs tool interface
77-
│ ├── Cargo.toml v6.7.0
77+
│ ├── Cargo.toml v6.8.0
7878
│ └── tests/ 418 Rust tests
7979
8080
├── python/ LEGACY Python v4.1.0
@@ -86,7 +86,7 @@ ContribAI/
8686
└── config.yaml.template shared config template
8787
```
8888

89-
## Architecture (v6.7.0)
89+
## Architecture (v6.8.0)
9090

9191
### Core Pipeline
9292
```

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [6.8.0] - 2026-04-28
11+
12+
### Added
13+
- **`contribai prs` command** — list submitted PRs from local memory with status filter:
14+
- `--status open|merged|closed|failed|all` (default `all`)
15+
- `--limit N` (default 20)
16+
- `--json` — emit a JSON array (PR number parsed as int) for piping to `jq`/scripts
17+
- Pretty mode color-codes status (green=merged, cyan=open, red=closed, yellow=failed) and renders date / PR# / repo / title / URL.
18+
- Complements `stats` (which shows only the last 5) by giving a full filtered view.
19+
- 4 unit tests on the JSON converter and date trimmer.
20+
1021
## [6.7.0] - 2026-04-28
1122

1223
### Added

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
**Autonomous AI agent that discovers, analyzes, and submits<br>Pull Requests to open source projects on GitHub.**
66

77
[![Rust](https://img.shields.io/badge/Rust-1.75+-f74c00?style=for-the-badge&logo=rust&logoColor=white)](https://www.rust-lang.org/)
8-
[![Version](https://img.shields.io/badge/v6.7.0-blue?style=for-the-badge&logo=github&logoColor=white)](https://github.com/tang-vu/ContribAI/releases)
8+
[![Version](https://img.shields.io/badge/v6.8.0-blue?style=for-the-badge&logo=github&logoColor=white)](https://github.com/tang-vu/ContribAI/releases)
99
[![License](https://img.shields.io/badge/AGPL--3.0-green?style=for-the-badge&logo=opensourceinitiative&logoColor=white)](LICENSE)
1010
[![Tests](https://img.shields.io/badge/602_tests-passing-brightgreen?style=for-the-badge&logo=checkmarx&logoColor=white)](#testing)
1111
[![PRs Merged](https://img.shields.io/badge/10_PRs-merged-blueviolet?style=for-the-badge&logo=git&logoColor=white)](HALL_OF_FAME.md)
@@ -261,7 +261,7 @@ contribai notify-test # Test Slack/Discord/Telegram
261261

262262
```
263263
ContribAI/
264-
├── crates/contribai-rs/src/ ← Rust v6.7.0 (primary)
264+
├── crates/contribai-rs/src/ ← Rust v6.8.0 (primary)
265265
│ ├── cli/ 40+ commands + ratatui TUI
266266
│ ├── core/ Config, events, error types
267267
│ ├── github/ REST v3 + GraphQL client

crates/contribai-rs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "contribai"
3-
version = "6.7.0"
3+
version = "6.8.0"
44
edition = "2021"
55
authors = ["tang-vu <tang.vu@contribai.dev>"]
66
description = "AI agent that autonomously contributes to open source projects on GitHub"

crates/contribai-rs/src/cli/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub mod models;
2020
pub mod notify_test;
2121
pub mod patrol;
2222
pub mod profile;
23+
pub mod prs;
2324
pub mod run;
2425
pub mod schedule;
2526
pub mod serve;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
//! Handles `Commands::Prs` — list submitted PRs from local memory.
2+
//!
3+
//! Reads the `submitted_prs` table via `Memory::get_prs`. The `stats`
4+
//! command shows only the most recent 5; this command exposes the full
5+
//! list with status filtering and a JSON mode for scripting.
6+
7+
use anyhow::{Context, Result};
8+
use colored::Colorize;
9+
use std::collections::HashMap;
10+
11+
use crate::cli::{create_memory, load_config};
12+
13+
/// Run `contribai prs`.
14+
///
15+
/// - `status`: "open" | "merged" | "closed" | "all" (case-insensitive). Default "all".
16+
/// - `limit`: max rows to fetch from memory (default 20)
17+
/// - `json`: emit JSON array instead of pretty output
18+
pub fn run_prs(config_path: Option<&str>, status: &str, limit: usize, json: bool) -> Result<()> {
19+
let config = load_config(config_path)?;
20+
let memory = create_memory(&config)?;
21+
22+
let normalized = status.trim().to_lowercase();
23+
let status_filter = match normalized.as_str() {
24+
"all" | "" => None,
25+
other => Some(other),
26+
};
27+
28+
let prs = memory
29+
.get_prs(status_filter, limit)
30+
.context("reading submitted_prs from memory")?;
31+
32+
if json {
33+
let arr = serde_json::Value::Array(prs.iter().map(map_to_json).collect::<Vec<_>>());
34+
println!("{}", serde_json::to_string_pretty(&arr)?);
35+
return Ok(());
36+
}
37+
38+
println!("{}", "📦 ContribAI Submitted PRs".cyan().bold());
39+
println!("{}", "━".repeat(60).dimmed());
40+
println!(
41+
" {:<20} {}",
42+
"Status filter:".dimmed(),
43+
if status_filter.is_some() {
44+
normalized.cyan()
45+
} else {
46+
"all".cyan()
47+
}
48+
);
49+
println!(
50+
" {:<20} {}",
51+
"Rows:".dimmed(),
52+
prs.len().to_string().cyan()
53+
);
54+
println!();
55+
56+
if prs.is_empty() {
57+
println!(" {}", "No PRs match the current filter.".dimmed());
58+
return Ok(());
59+
}
60+
61+
for pr in &prs {
62+
let status_str = pr.get("status").map(|s| s.as_str()).unwrap_or("unknown");
63+
let pr_number = pr.get("pr_number").cloned().unwrap_or_default();
64+
let repo = pr.get("repo").cloned().unwrap_or_default();
65+
let title = pr.get("title").cloned().unwrap_or_default();
66+
let url = pr.get("pr_url").cloned().unwrap_or_default();
67+
let created = pr.get("created_at").cloned().unwrap_or_default();
68+
69+
let status_colored = match status_str {
70+
"merged" => status_str.green().bold(),
71+
"open" => status_str.cyan().bold(),
72+
"closed" => status_str.red().bold(),
73+
"failed" => status_str.yellow().bold(),
74+
_ => status_str.dimmed().bold(),
75+
};
76+
77+
println!(
78+
" {} #{} {} [{}]",
79+
short_date(&created).dimmed(),
80+
pr_number.cyan(),
81+
repo.dimmed(),
82+
status_colored
83+
);
84+
println!(" {}", title);
85+
if !url.is_empty() {
86+
println!(" {}", url.blue().underline());
87+
}
88+
println!();
89+
}
90+
91+
Ok(())
92+
}
93+
94+
/// Convert a memory row (HashMap<String,String>) into a JSON object.
95+
/// PR number is parsed to a number when possible so JSON consumers can
96+
/// sort/compare numerically.
97+
fn map_to_json(row: &HashMap<String, String>) -> serde_json::Value {
98+
let mut obj = serde_json::Map::with_capacity(row.len());
99+
for (k, v) in row {
100+
if k == "pr_number" {
101+
if let Ok(n) = v.parse::<i64>() {
102+
obj.insert(k.clone(), serde_json::Value::from(n));
103+
continue;
104+
}
105+
}
106+
obj.insert(k.clone(), serde_json::Value::String(v.clone()));
107+
}
108+
serde_json::Value::Object(obj)
109+
}
110+
111+
/// Trim a YYYY-MM-DDTHH:MM:SS timestamp down to YYYY-MM-DD HH:MM for terminal display.
112+
/// Returns the input unchanged if it's shorter than expected.
113+
fn short_date(s: &str) -> String {
114+
// Tolerate either "T" or " " between date and time.
115+
let s = s.replace('T', " ");
116+
if s.len() >= 16 {
117+
s[..16].to_string()
118+
} else {
119+
s
120+
}
121+
}
122+
123+
#[cfg(test)]
124+
mod tests {
125+
use super::*;
126+
127+
#[test]
128+
fn map_to_json_parses_pr_number_as_int() {
129+
let mut row = HashMap::new();
130+
row.insert("pr_number".to_string(), "42".to_string());
131+
row.insert("title".to_string(), "fix bug".to_string());
132+
let v = map_to_json(&row);
133+
assert_eq!(v["pr_number"], serde_json::json!(42));
134+
assert_eq!(v["title"], serde_json::json!("fix bug"));
135+
}
136+
137+
#[test]
138+
fn map_to_json_keeps_pr_number_as_string_when_unparseable() {
139+
let mut row = HashMap::new();
140+
row.insert("pr_number".to_string(), "n/a".to_string());
141+
let v = map_to_json(&row);
142+
assert_eq!(v["pr_number"], serde_json::json!("n/a"));
143+
}
144+
145+
#[test]
146+
fn short_date_trims_iso_timestamp() {
147+
assert_eq!(short_date("2026-04-27T10:23:58Z"), "2026-04-27 10:23");
148+
}
149+
150+
#[test]
151+
fn short_date_passthrough_when_shorter_than_expected() {
152+
assert_eq!(short_date("2026-04-27"), "2026-04-27");
153+
}
154+
}

crates/contribai-rs/src/cli/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,21 @@ enum Commands {
269269
/// Run environment diagnostics — check config, auth, LLM, and system health
270270
Doctor,
271271

272+
/// List submitted PRs from memory, with status filter
273+
Prs {
274+
/// Status filter: open, merged, closed, failed, or all
275+
#[arg(short, long, default_value = "all")]
276+
status: String,
277+
278+
/// Maximum rows to return
279+
#[arg(short, long, default_value = "20")]
280+
limit: usize,
281+
282+
/// Emit a JSON array (machine-readable)
283+
#[arg(long)]
284+
json: bool,
285+
},
286+
272287
/// Tail the events log (~/.contribai/events.jsonl)
273288
Logs {
274289
/// Number of most recent events to show
@@ -442,6 +457,11 @@ impl Cli {
442457
Commands::Logs { tail, filter, json } => {
443458
commands::logs::run_logs(tail, filter.as_deref(), json)
444459
}
460+
Commands::Prs {
461+
status,
462+
limit,
463+
json,
464+
} => commands::prs::run_prs(self.config.as_deref(), &status, limit, json),
445465
Commands::CircuitBreaker => {
446466
commands::circuit_breaker::run_circuit_breaker_status(self.config.as_deref()).await
447467
}

install.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
$Version = "v6.7.0"
1+
$Version = "v6.8.0"
22
$Repo = "tang-vu/ContribAI"
33
$Binary = "contribai-windows-x86_64.exe"
44
$InstallDir = "$env:USERPROFILE\.contribai\bin"

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22
set -e
33

4-
VERSION="v6.7.0"
4+
VERSION="v6.8.0"
55
REPO="tang-vu/ContribAI"
66
INSTALL_DIR="/usr/local/bin"
77

0 commit comments

Comments
 (0)