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:
2026-07-17 20:15:56 -04:00
parent 74b210535e
commit a3f062b522
6 changed files with 246 additions and 12 deletions
+39 -3
View File
@@ -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(),