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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ toml = "1.1.2"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
mime_guess = "2.0.5"
toon = "0.1.2"

[dev-dependencies]
httpmock = "0.8.3"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ for the full command-to-token matrix, use the [`auth-matrix`](https://kagi.micr.

| command | purpose |
| --- | --- |
| `kagi search` | search Kagi with `json` by default, or render as `pretty`, `compact`, `markdown`, or `csv` |
| `kagi batch` | run multiple searches in parallel with JSON, compact, pretty, markdown, or csv output and shared filters |
| `kagi search` | search Kagi with `json` by default, or render as `toon`, `pretty`, `compact`, `markdown`, or `csv` |
| `kagi batch` | run multiple searches in parallel with JSON, TOON, compact, pretty, markdown, or csv output and shared filters |
| `kagi auth` | launch the auth wizard, or inspect, validate, and save credentials |
| `kagi summarize` | use the paid public summarizer API or the subscriber summarizer with `--subscriber` |
| `kagi watch` | rerun a search on an interval and emit added/removed result URLs |
Expand All @@ -182,7 +182,7 @@ for the full command-to-token matrix, use the [`auth-matrix`](https://kagi.micr.
| `kagi bang custom` | list, inspect, create, update, and delete custom bangs |
| `kagi redirect` | list, inspect, create, update, enable, disable, and delete redirect rules |

for automation, stdout stays JSON by default. `--format pretty` only changes rendering for humans.
for automation, stdout stays JSON by default. Use `--format toon` for token-efficient structured output in LLM context. `--format pretty` only changes rendering for humans.

## shell completion

Expand Down
2 changes: 1 addition & 1 deletion docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ kagi search --limit 5 "rust release notes"
# Per-request personalization override
kagi search --no-personalized "rust release notes"

# Output formats: json (default), pretty, compact, markdown, csv
# Output formats: json (default), toon, pretty, compact, markdown, csv
kagi search --format markdown "query" > results.md
```

Expand Down
1 change: 1 addition & 0 deletions docs/commands/assistant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Output format for prompt mode.
Possible values:

- `json` - pretty JSON
- `toon` - token-efficient structured output for LLM context
- `pretty` - terminal-friendly thread id, message id, and reply content
- `compact` - minified JSON
- `markdown` - only the assistant reply content
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/quick.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ kagi quick "what is rust?"

Output format for the response.

**Supported values:** `json`, `compact`, `pretty`, `markdown`
**Supported values:** `json`, `toon`, `compact`, `pretty`, `markdown`

**Default:** `json`

Expand Down
1 change: 1 addition & 0 deletions docs/commands/search.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Output format.
Possible values:

- `json` - pretty JSON
- `toon` - token-efficient structured output for LLM context
- `compact` - minified JSON
- `pretty` - terminal-friendly output
- `markdown` - markdown list output
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/watch.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Number of polls to run. `0` means run until interrupted.

### `--format <FORMAT>`

Output format for each event: `json`, `compact`, or `pretty`.
Output format for each event: `json`, `toon`, `compact`, or `pretty`.

## Example

Expand Down
8 changes: 4 additions & 4 deletions docs/reference/coverage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ These require no authentication:

| Command | Formats |
|---------|---------|
| `search` | `json`, `compact`, `pretty`, `markdown`, `csv` |
| `batch` | `json`, `compact`, `pretty`, `markdown`, `csv` |
| `quick` | `json`, `compact`, `pretty`, `markdown` |
| `assistant` | `json`, `compact`, `pretty`, `markdown` |
| `search` | `json`, `toon`, `compact`, `pretty`, `markdown`, `csv` |
| `batch` | `json`, `toon`, `compact`, `pretty`, `markdown`, `csv` |
| `quick` | `json`, `toon`, `compact`, `pretty`, `markdown` |
| `assistant` | `json`, `toon`, `compact`, `pretty`, `markdown` |

### Search Options

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/output-contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This page documents the current CLI output behavior as implemented in the repo.
## Core Rules

1. Most commands print pretty-formatted JSON to stdout on success.
2. `kagi search`, `kagi batch`, `kagi quick`, and `kagi assistant` support human-readable output via format flags.
2. `kagi search`, `kagi batch`, `kagi quick`, and `kagi assistant` support alternate output via format flags, including `toon` for token-efficient structured context.
3. Errors are plain text on stderr and exit with status code `1`.
4. Output shapes differ by command. There is no single universal response envelope.

Expand Down
14 changes: 11 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub enum AssistantThreadExportFormat {
pub enum OutputFormat {
/// JSON output (default) - structured data for scripts and APIs
Json,
/// TOON output - token-efficient structured data for LLM context
Toon,
/// Pretty formatted output with colors - human-readable terminal display
Pretty,
/// Compact JSON output - minified JSON for reduced size
Expand All @@ -44,6 +46,7 @@ impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Json => write!(f, "json"),
Self::Toon => write!(f, "toon"),
Self::Pretty => write!(f, "pretty"),
Self::Compact => write!(f, "compact"),
Self::Markdown => write!(f, "markdown"),
Expand All @@ -57,6 +60,8 @@ impl std::fmt::Display for OutputFormat {
pub enum QuickOutputFormat {
/// JSON output (default) - structured data for scripts and APIs
Json,
/// TOON output - token-efficient structured data for LLM context
Toon,
/// Pretty formatted output with colors - human-readable terminal display
Pretty,
/// Compact JSON output - minified JSON for reduced size
Expand All @@ -69,6 +74,7 @@ impl std::fmt::Display for QuickOutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Json => write!(f, "json"),
Self::Toon => write!(f, "toon"),
Self::Pretty => write!(f, "pretty"),
Self::Compact => write!(f, "compact"),
Self::Markdown => write!(f, "markdown"),
Expand All @@ -80,6 +86,7 @@ impl std::fmt::Display for QuickOutputFormat {
/// Output format for assistant responses.
pub enum AssistantOutputFormat {
Json,
Toon,
Pretty,
Compact,
Markdown,
Expand All @@ -89,6 +96,7 @@ impl std::fmt::Display for AssistantOutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Json => write!(f, "json"),
Self::Toon => write!(f, "toon"),
Self::Pretty => write!(f, "pretty"),
Self::Compact => write!(f, "compact"),
Self::Markdown => write!(f, "markdown"),
Expand Down Expand Up @@ -176,7 +184,7 @@ impl NewsFilterScope {

Features:
• Shell completion generation (bash, zsh, fish, powershell)
• Multiple output formats (json, pretty, compact, markdown, csv)
• Multiple output formats (json, toon, pretty, compact, markdown, csv)
• Parallel batch searches with rate limiting
• Colorized terminal output (disable with --no-color)
• Full Kagi API coverage with session token support",
Expand Down Expand Up @@ -206,7 +214,7 @@ pub enum Commands {
/// Example: kagi search "rust programming" --format pretty
///
/// Features:
/// • Multiple output formats: json (default), pretty, compact, markdown, csv
/// • Multiple output formats: json (default), toon, pretty, compact, markdown, csv
/// • Colorized pretty output (disable with --no-color)
/// • Lens support for scoped searches
/// • Region, time, date, order, verbatim, and personalization filters
Expand Down Expand Up @@ -254,7 +262,7 @@ pub enum Commands {
/// Features:
/// • Parallel execution with configurable concurrency
/// • Token bucket rate limiting to respect API limits
/// • All output formats supported (json, pretty, compact, markdown, csv)
/// • All output formats supported (json, toon, pretty, compact, markdown, csv)
/// • Lens support for scoped searches
/// • Shared region, time, date, order, verbatim, and personalization filters
/// • Color output control with --no-color
Expand Down
23 changes: 21 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ async fn run() -> Result<(), KagiError> {
let request = build_search_request(args.query, &options);
let format_str = match args.format {
cli::OutputFormat::Json => "json",
cli::OutputFormat::Toon => "toon",
cli::OutputFormat::Pretty => "pretty",
cli::OutputFormat::Compact => "compact",
cli::OutputFormat::Markdown => "markdown",
Expand Down Expand Up @@ -427,6 +428,7 @@ async fn run() -> Result<(), KagiError> {
};
let format_str = match args.format {
cli::QuickOutputFormat::Json => "json",
cli::QuickOutputFormat::Toon => "toon",
cli::QuickOutputFormat::Pretty => "pretty",
cli::QuickOutputFormat::Compact => "compact",
cli::QuickOutputFormat::Markdown => "markdown",
Expand Down Expand Up @@ -719,6 +721,7 @@ async fn run() -> Result<(), KagiError> {

let format_str = match args.format {
cli::OutputFormat::Json => "json",
cli::OutputFormat::Toon => "toon",
cli::OutputFormat::Pretty => "pretty",
cli::OutputFormat::Compact => "compact",
cli::OutputFormat::Markdown => "markdown",
Expand Down Expand Up @@ -1015,6 +1018,13 @@ fn print_compact_json<T: serde::Serialize>(value: &T) -> Result<(), KagiError> {
Ok(())
}

fn print_toon<T: serde::Serialize>(value: &T) -> Result<(), KagiError> {
let value = serde_json::to_value(value)
.map_err(|error| KagiError::Parse(format!("failed to serialize TOON output: {error}")))?;
println!("{}", toon::encode(&value, None));
Ok(())
}

async fn cached_json<T, K, Fut, F>(
enabled: bool,
ttl_seconds: u64,
Expand Down Expand Up @@ -1100,6 +1110,7 @@ fn print_quick_response(
println!("{}", format_quick_pretty(response, use_color));
Ok(())
}
"toon" => print_toon(response),
"compact" => print_compact_json(response),
"markdown" => {
println!("{}", format_quick_markdown(response));
Expand All @@ -1119,6 +1130,7 @@ fn print_assistant_response(
println!("{}", format_assistant_pretty(response, use_color));
Ok(())
}
AssistantOutputFormat::Toon => print_toon(response),
AssistantOutputFormat::Compact => print_compact_json(response),
AssistantOutputFormat::Markdown => {
println!("{}", format_assistant_markdown(response));
Expand Down Expand Up @@ -1215,6 +1227,9 @@ async fn run_search(
format_template_response(&response, template.as_deref().unwrap())
}
"pretty" => format_pretty_response(&response, use_color),
"toon" => {
return print_toon(&response);
}
"compact" => serde_json::to_string(&response).map_err(|error| {
KagiError::Parse(format!("failed to serialize search response: {error}"))
})?,
Expand Down Expand Up @@ -1396,6 +1411,7 @@ fn print_news_search(
) -> Result<(), KagiError> {
match format {
OutputFormat::Json => print_json(response),
OutputFormat::Toon => print_toon(response),
OutputFormat::Compact => print_compact_json(response),
OutputFormat::Pretty => {
println!("{}", format_pretty_news_response(response, use_color));
Expand Down Expand Up @@ -1647,7 +1663,7 @@ async fn run_batch_search(config: BatchSearchConfig<'_>) -> Result<(), KagiError
}
}

if !failures.is_empty() && (format == "json" || format == "compact") {
if !failures.is_empty() && (format == "json" || format == "compact" || format == "toon") {
// For machine-readable formats, exit with error code if any queries failed
return Err(KagiError::Batch(format_batch_failure_message(
results.len(),
Expand All @@ -1658,7 +1674,7 @@ async fn run_batch_search(config: BatchSearchConfig<'_>) -> Result<(), KagiError
let success_count = results.len();

// Output results in order
if format == "json" || format == "compact" {
if format == "json" || format == "compact" || format == "toon" {
// For machine-readable formats, create a proper JSON envelope
let queries: Vec<String> = results.iter().map(|(query, _)| query.clone()).collect();
let results_payload = results
Expand All @@ -1677,6 +1693,8 @@ async fn run_batch_search(config: BatchSearchConfig<'_>) -> Result<(), KagiError

if format == "compact" {
println!("{}", serde_json::to_string(&results_json)?);
} else if format == "toon" {
println!("{}", toon::encode(&results_json, None));
} else {
println!("{}", serde_json::to_string_pretty(&results_json)?);
}
Expand Down Expand Up @@ -1866,6 +1884,7 @@ async fn run_watch(args: WatchArgs, profile: Option<&str>) -> Result<(), KagiErr

match format.as_str() {
"compact" => print_compact_json(&event)?,
"toon" => print_toon(&event)?,
"pretty" => println!(
"watch #{iteration}: {} added, {} removed",
event["added"].as_array().map_or(0, Vec::len),
Expand Down
32 changes: 32 additions & 0 deletions tests/integration-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,38 @@ fn search_command_returns_json_from_mock_api() {
assert_eq!(body["data"][0]["title"], "Rust Programming Language");
}

#[test]
fn search_command_returns_toon_from_mock_api() {
let server = MockServer::start();
let _search = server.mock(|when, then| {
when.method(GET)
.path("/api/v0/search")
.query_param("q", "rust programming")
.header("authorization", "Bot test-api-token");
then.status(200)
.header("content-type", "application/json")
.json_body(search_payload(
"Rust Programming Language",
"https://www.rust-lang.org",
"Reliable systems programming.",
));
});

let tempdir = TempDir::new().expect("tempdir");
let env = test_env(&server);
let output = run_kagi(
&["search", "rust programming", "--format", "toon"],
&env_refs(&env),
tempdir.path(),
);

assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("data"));
assert!(stdout.contains("Rust Programming Language"));
assert!(stdout.contains("https://www.rust-lang.org"));
}

#[test]
fn search_command_pretty_format_prints_ranked_results() {
let server = MockServer::start();
Expand Down