Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

Commit 6ae1291

Browse files
MCP respects explicit --json/--plain flags, add audience tests
Before: MCP silently forced Audience::Agent regardless of flags. Now: explicit flags override the default. MCP default is still Agent when no flag is passed. CLI default is still Human. - OutputFormat::audience_with_default(default) replaces audience() - dispatch passes Audience::Agent for MCP, Audience::Human for CLI - Deleted 9 dead audience() methods (8 commands + OutputFormat) - Added 8 tests covering all audience resolution scenarios
1 parent 05bbbe4 commit 6ae1291

10 files changed

Lines changed: 128 additions & 63 deletions

File tree

o8v/src/commands/build.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@ pub struct Args {
3030
pub format: super::output_format::OutputFormat,
3131
}
3232

33-
impl Args {
34-
pub fn audience(&self) -> o8v_core::render::Audience {
35-
self.format.audience()
36-
}
37-
}
38-
3933
// ── Command trait impl ──────────────────────────────────────────────────
4034

4135
use o8v_core::command::{Command, CommandContext, CommandError};

o8v/src/commands/check.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@ pub struct Args {
3030
pub format: super::output_format::OutputFormat,
3131
}
3232

33-
impl Args {
34-
pub fn audience(&self) -> o8v_core::render::Audience {
35-
self.format.audience()
36-
}
37-
}
38-
3933
fn parse_limit(s: &str) -> Result<usize, String> {
4034
if s.starts_with('-') {
4135
return Err("must be non-negative".to_string());

o8v/src/commands/fmt.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ pub struct Args {
2828
pub format: super::output_format::OutputFormat,
2929
}
3030

31-
impl Args {
32-
pub fn audience(&self) -> o8v_core::render::Audience {
33-
self.format.audience()
34-
}
35-
}
36-
3731
// ─── Run ────────────────────────────────────────────────────────────────────
3832

3933
/// Run `8v fmt`.

o8v/src/commands/ls.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,6 @@ pub struct Args {
7777
pub format: super::output_format::OutputFormat,
7878
}
7979

80-
impl Args {
81-
pub fn audience(&self) -> o8v_core::render::Audience {
82-
self.format.audience()
83-
}
84-
}
85-
8680
// ─── Internal data structures ─────────────────────────────────────────────────
8781

8882
pub(crate) struct FileNode {

o8v/src/commands/mod.rs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,23 @@ pub(crate) enum Command {
5858
}
5959

6060
impl Command {
61-
/// Audience from CLI flags. MCP overrides this — caller determines default.
62-
fn cli_audience(&self) -> Audience {
61+
/// Resolve audience with a caller-specific default.
62+
///
63+
/// Explicit flags (`--json`, `--plain`) always win, regardless of caller.
64+
/// When no flag is passed, `default` is used — callers set their own default:
65+
/// - CLI → `Audience::Human`
66+
/// - MCP → `Audience::Agent`
67+
fn audience_with_default(&self, default: Audience) -> Audience {
6368
match self {
64-
Command::Build(a) => a.audience(),
65-
Command::Check(a) => a.audience(),
66-
Command::Fmt(a) => a.audience(),
67-
Command::Test(a) => a.audience(),
68-
Command::Read(a) => a.audience(),
69-
Command::Write(a) => a.audience(),
70-
Command::Search(a) => a.audience(),
71-
Command::Ls(a) => a.audience(),
72-
Command::Init(_) | Command::Hooks(_) | Command::Upgrade(_) | Command::Mcp => {
73-
Audience::Human
74-
}
69+
Command::Build(a) => a.format.audience_with_default(default),
70+
Command::Check(a) => a.format.audience_with_default(default),
71+
Command::Fmt(a) => a.format.audience_with_default(default),
72+
Command::Test(a) => a.format.audience_with_default(default),
73+
Command::Read(a) => a.format.audience_with_default(default),
74+
Command::Write(a) => a.format.audience_with_default(default),
75+
Command::Search(a) => a.format.audience_with_default(default),
76+
Command::Ls(a) => a.format.audience_with_default(default),
77+
Command::Init(_) | Command::Hooks(_) | Command::Upgrade(_) | Command::Mcp => default,
7578
}
7679
}
7780

@@ -145,8 +148,8 @@ pub(crate) async fn dispatch_command_with_agent(
145148
ctx.extensions.insert(info);
146149
}
147150
let audience = match caller {
148-
Caller::Mcp => Audience::Agent,
149-
Caller::Cli => command.cli_audience(),
151+
Caller::Mcp => command.audience_with_default(Audience::Agent),
152+
Caller::Cli => command.audience_with_default(Audience::Human),
150153
};
151154
let command_name = command.name();
152155
// Exit codes are CLI-specific — reports describe what happened,

o8v/src/commands/output_format.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) 2026 Soheil Alizadeh / 8Network. All rights reserved.
2+
// Licensed under the Business Source License 1.1 (BSL-1.1).
3+
// See LICENSE file in the project root.
4+
5+
//! Shared output format flags — one struct, all commands.
6+
7+
use o8v_core::render::Audience;
8+
9+
/// Output format flags embedded in every command's Args via `#[command(flatten)]`.
10+
#[derive(clap::Args, Debug, Default)]
11+
pub struct OutputFormat {
12+
/// Plain text output for AI agents and pipes.
13+
#[arg(long, conflicts_with = "json")]
14+
pub plain: bool,
15+
16+
/// JSON output for tools and CI.
17+
#[arg(long, conflicts_with = "plain")]
18+
pub json: bool,
19+
20+
/// Disable colored output.
21+
#[arg(long)]
22+
pub no_color: bool,
23+
}
24+
25+
impl OutputFormat {
26+
/// Resolve audience, using `default` when no explicit flag was passed.
27+
///
28+
/// - `--json` → `Audience::Machine` (always, regardless of caller)
29+
/// - `--plain` → `Audience::Agent` (always, regardless of caller)
30+
/// - no flag → `default` (caller decides: Human for CLI, Agent for MCP)
31+
pub fn audience_with_default(&self, default: Audience) -> Audience {
32+
if self.json {
33+
Audience::Machine
34+
} else if self.plain {
35+
Audience::Agent
36+
} else {
37+
default
38+
}
39+
}
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
use super::*;
45+
use o8v_core::render::Audience;
46+
47+
fn fmt(json: bool, plain: bool, no_color: bool) -> OutputFormat {
48+
OutputFormat { json, plain, no_color }
49+
}
50+
51+
// 1. No flags + Human default → Human
52+
#[test]
53+
fn no_flags_human_default_returns_human() {
54+
let f = fmt(false, false, false);
55+
assert_eq!(f.audience_with_default(Audience::Human), Audience::Human);
56+
}
57+
58+
// 2. No flags + Agent default → Agent
59+
#[test]
60+
fn no_flags_agent_default_returns_agent() {
61+
let f = fmt(false, false, false);
62+
assert_eq!(f.audience_with_default(Audience::Agent), Audience::Agent);
63+
}
64+
65+
// 3. --json + Human default → Machine
66+
#[test]
67+
fn json_flag_human_default_returns_machine() {
68+
let f = fmt(true, false, false);
69+
assert_eq!(f.audience_with_default(Audience::Human), Audience::Machine);
70+
}
71+
72+
// 4. --json + Agent default → Machine
73+
#[test]
74+
fn json_flag_agent_default_returns_machine() {
75+
let f = fmt(true, false, false);
76+
assert_eq!(f.audience_with_default(Audience::Agent), Audience::Machine);
77+
}
78+
79+
// 5. --plain + Human default → Agent
80+
#[test]
81+
fn plain_flag_human_default_returns_agent() {
82+
let f = fmt(false, true, false);
83+
assert_eq!(f.audience_with_default(Audience::Human), Audience::Agent);
84+
}
85+
86+
// 6. --plain + Agent default → Agent
87+
#[test]
88+
fn plain_flag_agent_default_returns_agent() {
89+
let f = fmt(false, true, false);
90+
assert_eq!(f.audience_with_default(Audience::Agent), Audience::Agent);
91+
}
92+
93+
// 7. Default trait → Human audience (all false)
94+
#[test]
95+
fn default_trait_gives_human_audience() {
96+
let f = OutputFormat::default();
97+
assert_eq!(f.audience_with_default(Audience::Human), Audience::Human);
98+
}
99+
100+
// 8. no_color doesn't affect audience
101+
#[test]
102+
fn no_color_does_not_affect_audience() {
103+
let f = fmt(false, false, true);
104+
assert_eq!(f.audience_with_default(Audience::Human), Audience::Human);
105+
let f = fmt(true, false, true);
106+
assert_eq!(f.audience_with_default(Audience::Human), Audience::Machine);
107+
let f = fmt(false, true, true);
108+
assert_eq!(f.audience_with_default(Audience::Human), Audience::Agent);
109+
}
110+
}

o8v/src/commands/read.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ pub struct Args {
2626
pub format: super::output_format::OutputFormat,
2727
}
2828

29-
impl Args {
30-
pub fn audience(&self) -> o8v_core::render::Audience {
31-
self.format.audience()
32-
}
33-
}
34-
3529
// ─── Path Parsing ────────────────────────────────────────────────────────────
3630

3731
/// Parse `path:N-M` into (path, Some((N, M))) or (path, None).

o8v/src/commands/search.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,6 @@ pub struct Args {
6565
pub format: super::output_format::OutputFormat,
6666
}
6767

68-
impl Args {
69-
pub fn audience(&self) -> o8v_core::render::Audience {
70-
self.format.audience()
71-
}
72-
}
73-
7468
// ─── Data structures ─────────────────────────────────────────────────────────
7569

7670
pub struct Match {

o8v/src/commands/test.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@ pub struct Args {
3030
pub format: super::output_format::OutputFormat,
3131
}
3232

33-
impl Args {
34-
pub fn audience(&self) -> o8v_core::render::Audience {
35-
self.format.audience()
36-
}
37-
}
38-
3933
// ── Command trait impl ──────────────────────────────────────────────────
4034

4135
use o8v_core::command::{Command, CommandContext, CommandError};

o8v/src/commands/write.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@ pub struct Args {
5656
pub format: super::output_format::OutputFormat,
5757
}
5858

59-
impl Args {
60-
pub fn audience(&self) -> o8v_core::render::Audience {
61-
self.format.audience()
62-
}
63-
}
64-
6559
// ─── Operation Type ─────────────────────────────────────────────────────────
6660

6761
#[derive(Debug, Clone)]

0 commit comments

Comments
 (0)