F6: kimi-cli command parity — kigi acp, mcp auth, --mcp-config-file
- `kigi acp`: top-level alias for the stdio ACP server (kimi-cli `acp`).
AgentArgs.mode is now optional — bare `kigi agent` and `kigi acp` both
default to stdio at dispatch; `kigi acp <mode>` is rejected.
- `kigi mcp auth <name>`: authorize an OAuth-enabled remote MCP server
(kimi-cli `mcp auth`). Reuses the doctor's interactive connection path:
starts the named server with OauthInteractivity::Interactive (browser
flow when required), completes the handshake, and reports the tool
count. Stdio servers and unknown names fail with actionable errors.
- `--mcp-config-file <PATH>` (repeatable, global): extra MCP config files
in the .mcp.json shape ({"mcpServers": {...}}), kimi-cli semantics.
Files are validated at parse time (fail fast on unreadable/invalid
JSON), carried across the TUI -> shell boundary via
KIGI_MCP_CONFIG_FILES, and merged at HIGHEST priority — an explicitly
passed file overrides every config scope. Covered by loader unit tests
and verified live: an injected server surfaces in the session's
x.ai/mcp/servers_updated notification via both the flag and the env.
The stale bare-agent-requires-mode CLI test is re-contracted to the new
default-to-stdio behavior.
This commit is contained in:
@@ -7,6 +7,8 @@ use std::path::PathBuf;
|
||||
/// Top-level commands for the pager binary.
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Run the ACP server over stdio (alias of `agent stdio`; kimi-cli parity)
|
||||
Acp(Box<AgentArgs>),
|
||||
/// Run Kigi without the interactive UI
|
||||
Agent(Box<AgentArgs>),
|
||||
/// Show the configuration Kigi discovers for this directory
|
||||
@@ -270,9 +272,10 @@ pub struct AgentArgs {
|
||||
/// Override the public xAI API base URL.
|
||||
#[arg(long = "xai-api-base-url")]
|
||||
pub xai_api_base_url: Option<String>,
|
||||
/// Agent runtime mode
|
||||
/// Agent runtime mode. Optional: bare `kigi agent` (and the `kigi acp`
|
||||
/// alias) default to stdio.
|
||||
#[command(subcommand)]
|
||||
pub mode: AgentCmd,
|
||||
pub mode: Option<AgentCmd>,
|
||||
}
|
||||
impl AgentArgs {
|
||||
/// Canonicalized `--plugin-dir` paths, warning to stderr and skipping
|
||||
@@ -387,6 +390,15 @@ pub struct PagerArgs {
|
||||
/// Working directory.
|
||||
#[arg(long)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// MCP config file to load (`{"mcpServers": {...}}`). Repeat the option
|
||||
/// for multiple files; entries override config.toml-scoped servers.
|
||||
#[arg(
|
||||
long = "mcp-config-file",
|
||||
value_name = "PATH",
|
||||
global = true,
|
||||
value_hint = ValueHint::FilePath
|
||||
)]
|
||||
pub mcp_config_file: Vec<PathBuf>,
|
||||
/// Use a custom leader socket path instead of the default `~/.kigi/leader.sock`.
|
||||
#[arg(
|
||||
long = "leader-socket",
|
||||
@@ -923,6 +935,30 @@ mod tests {
|
||||
let err = PagerArgs::try_parse_from(["grok", "--minimal", "--fullscreen"]).unwrap_err();
|
||||
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
|
||||
}
|
||||
/// kimi-cli parity (F6): bare `kigi acp` runs the stdio ACP server, and
|
||||
/// bare `kigi agent` defaults to stdio at dispatch (mode is optional).
|
||||
#[test]
|
||||
fn acp_alias_and_bare_agent_default_to_stdio() {
|
||||
let args = PagerArgs::try_parse_from(["kigi", "acp"]).unwrap();
|
||||
let Some(Command::Acp(agent)) = args.command else {
|
||||
panic!("expected acp subcommand");
|
||||
};
|
||||
assert!(agent.mode.is_none(), "acp must not carry a runtime mode");
|
||||
|
||||
let args = PagerArgs::try_parse_from(["kigi", "agent"]).unwrap();
|
||||
let Some(Command::Agent(agent)) = args.command else {
|
||||
panic!("expected agent subcommand");
|
||||
};
|
||||
assert!(agent.mode.is_none(), "bare agent defaults to stdio");
|
||||
|
||||
// Explicit modes still parse under `agent`.
|
||||
let args = PagerArgs::try_parse_from(["kigi", "agent", "stdio"]).unwrap();
|
||||
let Some(Command::Agent(agent)) = args.command else {
|
||||
panic!("expected agent subcommand");
|
||||
};
|
||||
assert!(matches!(agent.mode, Some(AgentCmd::Stdio)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_plugin_dir_repeatable_and_canonicalized() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -948,7 +984,7 @@ mod tests {
|
||||
panic!("expected agent subcommand");
|
||||
};
|
||||
assert_eq!(agent.plugin_dirs, vec![dir.clone(), file, missing]);
|
||||
assert!(matches!(agent.mode, AgentCmd::Stdio));
|
||||
assert!(matches!(agent.mode, Some(AgentCmd::Stdio)));
|
||||
assert!(agent.no_leader);
|
||||
assert_eq!(
|
||||
agent.canonical_plugin_dirs(),
|
||||
|
||||
@@ -1359,12 +1359,15 @@ mod tests {
|
||||
assert!(args.no_leader);
|
||||
assert!(matches!(args.command, Some(Command::Agent(_))));
|
||||
}
|
||||
/// The `agent` subcommand requires an explicit mode (stdio|serve|leader);
|
||||
/// the old bare `agent` headless form was removed.
|
||||
/// Bare `agent` parses with no mode and defaults to stdio at dispatch
|
||||
/// (kimi-cli parity, F6 — the `acp` alias relies on the same default).
|
||||
#[test]
|
||||
fn cli_bare_agent_subcommand_requires_mode() {
|
||||
assert!(try_parse_pager(&["grok-pager", "--leader", "agent"]).is_err());
|
||||
assert!(try_parse_pager(&["grok-pager", "agent"]).is_err());
|
||||
fn cli_bare_agent_subcommand_defaults_to_stdio() {
|
||||
let args = try_parse_pager(&["grok-pager", "agent"]).unwrap();
|
||||
let Some(Command::Agent(agent)) = args.command else {
|
||||
panic!("expected agent subcommand");
|
||||
};
|
||||
assert!(agent.mode.is_none(), "bare agent carries no explicit mode");
|
||||
}
|
||||
#[test]
|
||||
fn leader_defaults_off_without_config() {
|
||||
|
||||
@@ -80,6 +80,11 @@ pub enum McpCommand {
|
||||
#[arg(short = 's', long, value_enum)]
|
||||
scope: Option<McpScope>,
|
||||
},
|
||||
/// Authorize with an OAuth-enabled remote MCP server
|
||||
Auth {
|
||||
/// Name of the MCP server to authorize
|
||||
name: String,
|
||||
},
|
||||
/// Diagnose MCP server configuration and connectivity
|
||||
Doctor {
|
||||
/// Emit machine-readable JSON output
|
||||
@@ -142,10 +147,30 @@ pub async fn run(mcp_args: McpArgs) -> Result<()> {
|
||||
McpCommand::List { json } => run_list(json),
|
||||
McpCommand::Add(args) => run_add(args).await,
|
||||
McpCommand::Remove { name, scope } => run_remove(&name, scope).await,
|
||||
McpCommand::Auth { name } => run_auth(&name).await,
|
||||
McpCommand::Doctor { json, name } => run_doctor(json, name).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// kimi-cli `mcp auth` parity: start the named remote server with the
|
||||
/// interactive OAuth flow and report the tool count on success.
|
||||
async fn run_auth(name: &str) -> Result<()> {
|
||||
let cwd = current_dir_or_exit();
|
||||
println!("Authorizing with '{name}'...");
|
||||
println!("A browser window will open if authorization is required.");
|
||||
match kigi_shell::mcp_doctor::run_auth(&cwd, name).await {
|
||||
Ok(tool_count) => {
|
||||
println!("Successfully authorized with '{name}'.");
|
||||
println!("Available tools: {tool_count}");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Authorization failed: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_list(json: bool) -> Result<()> {
|
||||
// Include project-scoped servers (nearest definition wins), matching what
|
||||
// a session started in this directory would load from config.toml files.
|
||||
|
||||
Reference in New Issue
Block a user