Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
+449
View File
@@ -0,0 +1,449 @@
use std::{fmt, path::PathBuf};
use clap::{Args, Subcommand, ValueEnum};
use crate::{
config_file::ConfigFileArgs, environment::EnvironmentCreateArgs, mcp::MCPSpec,
model::ModelArgs, scope::ObjectScope, share::ShareArgs, skill::SkillSpec,
};
/// Output format for agent results.
#[derive(Debug, Copy, Clone, ValueEnum, Eq, PartialEq, Default)]
pub enum OutputFormat {
/// Output as JSON.
#[value(name = "json")]
Json,
/// Output as newline-delimited JSON.
#[value(name = "ndjson")]
Ndjson,
/// Output as human-readable text.
#[default]
#[value(name = "pretty")]
Pretty,
/// Output as plain text.
#[value(name = "text")]
Text,
}
impl fmt::Display for OutputFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.to_possible_value().expect("no values are skipped");
f.write_str(value.get_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Prompt {
PlainText(String),
SavedPrompt(String),
}
impl fmt::Display for Prompt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Prompt::PlainText(text) => write!(f, "Prompt: {text}"),
Prompt::SavedPrompt(id) => write!(f, "Saved Prompt ID: {id}"),
}
}
}
/// Prompt arguments - mutually exclusive prompt or saved-prompt.
/// The required constraint is enforced at the command level via ArgGroup.
#[derive(Debug, Clone, Args)]
#[group(multiple = false)]
pub struct PromptArg {
/// Prompt for the agent to carry out.
#[arg(long = "prompt", short = 'p')]
pub prompt: Option<String>,
/// The saved AI prompt to run, identified by id.
#[arg(long = "saved-prompt")]
pub saved_prompt: Option<String>,
}
impl PromptArg {
pub fn to_prompt(&self) -> Option<Prompt> {
match (self.prompt.as_ref(), self.saved_prompt.as_ref()) {
(Some(prompt), None) => Some(Prompt::PlainText(prompt.clone())),
(None, Some(saved_prompt)) => Some(Prompt::SavedPrompt(saved_prompt.clone())),
_ => None,
}
}
}
/// Shared CLI args for controlling computer use capabilities.
#[derive(Debug, Clone, Args, Default)]
pub struct ComputerUseArgs {
/// Enable computer use capabilities for this agent run.
#[arg(long = "computer-use", conflicts_with = "no_computer_use")]
pub computer_use: bool,
/// Disable computer use capabilities for this agent run.
#[arg(long = "no-computer-use", conflicts_with = "computer_use")]
pub no_computer_use: bool,
}
impl ComputerUseArgs {
/// Returns the computer use override based on CLI flags.
/// - `Some(true)` if `--computer-use` was specified
/// - `Some(false)` if `--no-computer-use` was specified
/// - `None` if neither was specified (use default behavior)
pub fn computer_use_override(&self) -> Option<bool> {
match (self.computer_use, self.no_computer_use) {
(true, false) => Some(true),
(false, true) => Some(false),
_ => None,
}
}
}
/// Hidden variant of [`ComputerUseArgs`] for commands where computer use flags
/// should be accepted but not shown in help output.
#[derive(Debug, Clone, Args, Default)]
pub struct HiddenComputerUseArgs {
/// Enable computer use capabilities for this agent run.
#[arg(long = "computer-use", conflicts_with = "no_computer_use", hide = true)]
pub computer_use: bool,
/// Disable computer use capabilities for this agent run.
#[arg(long = "no-computer-use", conflicts_with = "computer_use", hide = true)]
pub no_computer_use: bool,
}
impl HiddenComputerUseArgs {
pub fn computer_use_override(&self) -> Option<bool> {
match (self.computer_use, self.no_computer_use) {
(true, false) => Some(true),
(false, true) => Some(false),
_ => None,
}
}
}
/// The execution harness for an agent run.
#[derive(Debug, Copy, Clone, ValueEnum, Eq, PartialEq, Default)]
pub enum Harness {
/// Use Warp's built-in MAA infrastructure (default).
#[default]
#[value(name = "oz")]
Oz,
/// Delegate to the `claude` CLI.
#[value(name = "claude", alias = "claude-code")]
Claude,
/// Delegate to the `opencode` CLI.
#[value(name = "opencode", alias = "open-code")]
OpenCode,
/// Delegate to the `gemini` CLI.
#[value(name = "gemini")]
Gemini,
/// A harness produced by a newer client/server that this client doesn't
/// recognize. Surfaced via deserialization fallbacks (e.g. unknown GraphQL
/// enum values, unknown `harness_type` strings); never selectable from the
/// CLI or harness dropdown.
#[value(skip)]
Unknown,
}
impl Harness {
pub fn parse_orchestration_harness(value: &str) -> Option<Self> {
let normalized = value.trim().to_ascii_lowercase().replace('_', "-");
<Self as ValueEnum>::from_str(&normalized, true).ok()
}
pub fn parse_local_child_harness(value: &str) -> Option<Self> {
match Self::parse_orchestration_harness(value) {
Some(harness @ (Self::Claude | Self::OpenCode)) => Some(harness),
Some(Self::Oz) | Some(Self::Gemini) | Some(Self::Unknown) | None => None,
}
}
pub fn display_name(self) -> &'static str {
match self {
Self::Oz => "Oz",
Self::Claude => "Claude Code",
Self::OpenCode => "OpenCode",
Self::Gemini => "Gemini CLI",
Self::Unknown => "Unknown",
}
}
}
impl fmt::Display for Harness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Harness::Oz => "oz",
Harness::Claude => "claude",
Harness::OpenCode => "opencode",
Harness::Gemini => "gemini",
Harness::Unknown => "unknown",
};
f.write_str(name)
}
}
/// Profile subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum AgentProfileCommand {
/// List available agent profiles.
List,
}
/// Agent-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum AgentCommand {
/// Run a new Oz agent.
Run(RunAgentArgs),
/// Dispatch an Oz agent that runs remotely.
RunCloud(RunCloudArgs),
/// Manage agent profiles.
#[command(subcommand)]
Profile(AgentProfileCommand),
/// List all available agents.
List(ListAgentConfigsArgs),
}
#[derive(Debug, Clone, Args)]
#[command(
visible_alias = "r",
group(
clap::ArgGroup::new("prompt_group")
.required(true)
.multiple(true)
.args(["prompt", "saved_prompt", "task_id", "skill"])
)
)]
pub struct RunAgentArgs {
#[command(flatten)]
pub prompt_arg: PromptArg,
#[command(flatten)]
pub model: ModelArgs,
#[command(flatten)]
pub config_file: ConfigFileArgs,
/// Use a skill as the base prompt for the agent.
///
/// Format: `skill_name`, `repo:skill_name`, or `org/repo:skill_name`
///
/// Skills are searched in `.agents/skills/`, `.warp-core/skills/`, `.claude/skills/`, and `.codex/skills/` directories.
/// If a repo is specified, searches only that repo. If org is also specified,
/// validates the repo's git remote matches the expected org.
///
/// When used with --prompt, the skill provides the base context and the prompt is the task.
///
/// To automate a skill on a schedule, use `oz schedule create --skill <SPEC>`.
#[arg(long = "skill", value_name = "SPEC")]
pub skill: Option<SkillSpec>,
/// Name for this agent task.
#[arg(long = "name", short = 'n')]
pub name: Option<String>,
/// Working directory for the agent
#[arg(short = 'C', long = "cwd")]
pub cwd: Option<PathBuf>,
/// Display agent progress in the Warp interface.
#[arg(long = "gui", hide = true)]
pub gui: bool,
#[command(flatten)]
pub share: ShareArgs,
/// MCP servers to start before executing the agent.
///
/// Can be specified as:
/// - A path to a JSON file containing MCP configuration
/// - Inline JSON with MCP server configuration
///
/// Can be specified multiple times to include multiple servers.
#[arg(long = "mcp", value_name = "SPEC")]
pub mcp_specs: Vec<MCPSpec>,
/// LEGACY: MCP servers to start before executing the agent, identified by UUID.
#[arg(long = "mcp-server", value_name = "UUID", hide = true)]
pub mcp_servers: Vec<uuid::Uuid>,
/// Cloud environment to use, identified by ID.
#[arg(long = "environment", short = 'e', value_name = "ID")]
pub environment: Option<String>,
/// Keep the agent's session open after the conversation completes.
///
/// This is useful when you want to keep the session alive for follow-up interactions.
///
/// You can optionally provide a duration (e.g. `--idle-on-complete 10m`).
#[arg(
long = "idle-on-complete",
value_name = "DURATION",
num_args = 0..=1,
default_missing_value = "45m",
hide = true
)]
pub idle_on_complete: Option<humantime::Duration>,
#[command(flatten)]
pub snapshot: SnapshotArgs,
/// Identifier for the task that spawned this agent, used to report progress.
#[arg(long = "task-id", hide = true, conflicts_with_all = ["prompt", "saved_prompt", "file"])]
pub task_id: Option<String>,
/// Whether we are running the agent in a sandboxed environment.
#[arg(long = "sandboxed", hide = true)]
pub sandboxed: bool,
/// IAM role ARN to use for federated AWS Bedrock credentials for this run.
#[arg(long = "bedrock-inference-role", value_name = "ROLE_ARN", hide = true)]
pub bedrock_inference_role: Option<String>,
#[command(flatten)]
pub computer_use: HiddenComputerUseArgs,
/// Continue an existing cloud conversation by ID.
#[arg(long = "conversation", value_name = "ID")]
pub conversation: Option<String>,
/// Agent profile to configure the terminal session.
#[arg(long = "profile", value_name = "ID")]
pub profile: Option<String>,
/// Execution harness for the agent run.
///
/// "oz" (default) uses Warp's built-in agent infrastructure.
/// "claude" delegates to the `claude` CLI.
#[arg(long = "harness", value_name = "HARNESS", default_value_t = Harness::Oz, hide = true)]
pub harness: Harness,
}
impl RunAgentArgs {
/// Combine `mcp_specs` with legacy `mcp_servers` (UUIDs) into a single list.
pub fn all_mcp_specs(&self) -> Vec<MCPSpec> {
let mut specs = self.mcp_specs.clone();
specs.extend(self.mcp_servers.iter().cloned().map(MCPSpec::Uuid));
specs
}
}
#[derive(Debug, Clone, Args)]
pub struct SnapshotArgs {
/// Disable the end-of-run workspace snapshot upload.
#[arg(long = "no-snapshot")]
pub no_snapshot: bool,
/// Maximum time to wait for the end-of-run snapshot upload.
#[arg(long = "snapshot-upload-timeout", value_name = "DURATION")]
pub snapshot_upload_timeout: Option<humantime::Duration>,
/// Maximum time to wait for the declarations script before uploading the snapshot.
#[arg(long = "snapshot-script-timeout", value_name = "DURATION")]
pub snapshot_script_timeout: Option<humantime::Duration>,
}
#[derive(Debug, Clone, Args)]
#[command(
name = "run-cloud",
visible_alias = "ra",
alias = "run-ambient",
group(
clap::ArgGroup::new("prompt_group")
.required(true)
.multiple(true)
.args(["prompt", "saved_prompt", "skill"])
)
)]
pub struct RunCloudArgs {
#[command(flatten)]
pub prompt_arg: PromptArg,
#[command(flatten)]
pub model: ModelArgs,
#[command(flatten)]
pub config_file: ConfigFileArgs,
/// Use a skill as the base prompt for the agent.
///
/// Format: `skill_name`, `repo:skill_name`, or `org/repo:skill_name`
///
/// Skills are searched in `.agents/skills/`, `.warp-core/skills/`, `.claude/skills/`, and `.codex/skills/` directories.
/// If a repo is specified, searches only that repo. If org is also specified,
/// validates the repo's git remote matches the expected org.
///
/// When used with --prompt, the skill provides the base context and the prompt is the task.
///
/// To automate a skill on a schedule, use `oz schedule create --skill <SPEC>`.
#[arg(long = "skill", value_name = "SPEC")]
pub skill: Option<SkillSpec>,
/// Name for this agent task.
#[arg(long = "name", short = 'n')]
pub name: Option<String>,
/// MCP servers to start before executing the agent.
///
/// Can be specified as:
/// - A path to a JSON file containing MCP configuration
/// - Inline JSON with MCP server configuration
///
/// Can be specified multiple times to include multiple servers.
#[arg(long = "mcp", value_name = "SPEC")]
pub mcp_specs: Vec<MCPSpec>,
/// The environment to run this ambient agent in.
#[command(flatten)]
pub environment: EnvironmentCreateArgs,
/// Open the agent's session in Warp once it's available.
#[arg(long = "open")]
pub open: bool,
/// Continue an existing cloud conversation by ID.
#[arg(long = "conversation", value_name = "ID")]
pub conversation: Option<String>,
#[command(flatten)]
pub scope: ObjectScope,
/// Where this job should be hosted. Setting "warp" runs it on Warp's infrastructure. Any other
/// value is treated is a self-hosted job and the value will be matched with the self-hosted
/// worker's name.
#[arg(long = "host", value_name = "WORKER_ID")]
pub worker_host: Option<String>,
/// Path to a file to attach to the agent query.
///
/// Can be specified multiple times to attach multiple files (maximum 5).
///
/// Example: --attach file1.png --attach file2.txt
#[arg(
long = "attach",
value_name = "PATH",
num_args = 1,
action = clap::ArgAction::Append,
value_parser = clap::value_parser!(PathBuf),
)]
pub attachment_paths: Vec<PathBuf>,
#[command(flatten)]
pub computer_use: ComputerUseArgs,
#[command(flatten)]
pub snapshot: SnapshotArgs,
/// Execution harness for the agent run.
///
/// "oz" (default) uses Warp's built-in agent infrastructure.
/// "claude" delegates to the `claude` CLI.
#[arg(long = "harness", value_name = "HARNESS", default_value_t = Harness::Oz, hide = true)]
pub harness: Harness,
/// Name of a managed secret for Claude Code harness authentication.
///
/// Resolved server-side and injected into the agent container.
/// Only valid when --harness is set to "claude".
#[arg(long = "claude-auth-secret", value_name = "NAME", hide = true)]
pub claude_auth_secret: Option<String>,
}
/// Arguments for listing available agents.
#[derive(Debug, Clone, Args)]
pub struct ListAgentConfigsArgs {
/// List skills from a specific GitHub repository.
///
/// Format: `owner/repo` or `https://github.com/owner/repo`
///
/// When provided, lists skills from this repo instead of from your environments.
/// Any environments that include this repo will still be shown in the results.
#[arg(long = "repo", short = 'r', value_name = "REPO")]
pub repo: Option<String>,
}
+56
View File
@@ -0,0 +1,56 @@
use std::path::PathBuf;
use clap::{ArgGroup, Args, Subcommand};
/// Artifact-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum ArtifactCommand {
/// Upload an artifact file.
#[command(hide = true)]
Upload(UploadArtifactArgs),
/// Get artifact metadata.
Get(GetArtifactArgs),
/// Download an artifact file.
Download(DownloadArtifactArgs),
}
#[derive(Debug, Clone, Args)]
#[command(
group(
ArgGroup::new("artifact_association")
.multiple(false)
.args(["run_id", "conversation_id"])
)
)]
pub struct UploadArtifactArgs {
/// Path to the artifact file to upload.
pub path: PathBuf,
/// Associate the uploaded artifact with a run.
#[arg(long = "run-id")]
pub run_id: Option<String>,
/// Associate the uploaded artifact with a conversation.
#[arg(long = "conversation-id")]
pub conversation_id: Option<String>,
/// Description for the uploaded artifact.
#[arg(long = "description")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct DownloadArtifactArgs {
/// UID of the artifact to download.
pub artifact_uid: String,
/// Write the downloaded artifact to a specific file path.
#[arg(long = "out", short = 'o')]
pub out: Option<PathBuf>,
}
#[derive(Debug, Clone, Args)]
pub struct GetArtifactArgs {
/// UID of the artifact to get.
pub artifact_uid: String,
}
+23
View File
@@ -0,0 +1,23 @@
use std::io;
use clap_complete::aot::{Shell, generate};
use crate::{Args, binary_name};
use galaxy_core::channel::ChannelState;
/// Generate shell completions for the Warp CLI and write them to stdout.
pub fn generate_to_stdout(shell: Option<Shell>) -> anyhow::Result<()> {
let shell = match shell.or_else(Shell::from_env) {
Some(s) => s,
None => anyhow::bail!(
"Could not determine shell from environment. Please provide a shell argument."
),
};
let mut cmd = Args::clap_command();
let bin_name =
binary_name().unwrap_or_else(|| ChannelState::channel().cli_command_name().to_string());
generate(shell, &mut cmd, bin_name, &mut io::stdout());
Ok(())
}
+14
View File
@@ -0,0 +1,14 @@
use std::path::PathBuf;
/// Shared CLI args for loading command configuration from a file.
#[derive(Debug, Default, Clone, clap::Args)]
pub struct ConfigFileArgs {
/// Path to a YAML or JSON configuration file.
#[arg(
short = 'f',
long = "file",
value_name = "PATH",
env = "WARP_AGENT_CONFIG_FILE"
)]
pub file: Option<PathBuf>,
}
+135
View File
@@ -0,0 +1,135 @@
use clap::{ArgAction, ArgGroup, Args, Subcommand};
use crate::scope::ObjectScope;
/// Maximum length for environment descriptions.
const MAX_DESCRIPTION_LENGTH: usize = 240;
/// Validates that a description is within the allowed length.
fn validate_description(s: &str) -> Result<String, String> {
let len = s.chars().count();
if len > MAX_DESCRIPTION_LENGTH {
Err(format!(
"Description must be at most {} characters (got {})",
MAX_DESCRIPTION_LENGTH, len
))
} else {
Ok(s.to_string())
}
}
/// Environment-related subcommands.
#[derive(Debug, Clone, Subcommand)]
#[command(group(ArgGroup::new("scope").required(false)))]
#[command(visible_alias = "e")]
pub enum EnvironmentCommand {
/// List cloud environments.
List,
/// Manage base images for cloud environments.
#[command(subcommand)]
Image(ImageCommand),
/// Create a new cloud environment.
Create {
/// Name of the environment
#[arg(long = "name", short = 'n')]
name: String,
/// Description of the environment (max 240 characters)
#[arg(long = "description", value_parser = validate_description)]
description: Option<String>,
/// Docker image to use. Run `warp environment image list` to list suggested dev images.
/// If not specified, you'll be prompted to select from available images.
#[arg(long = "docker-image", short = 'd')]
docker_image: Option<String>,
/// Git repo in format "owner/repo" (can be specified multiple times)
#[arg(long = "repo", short = 'r', action = ArgAction::Append)]
repo: Vec<String>,
/// Accept multiple setup command args to be run after cloning
#[arg(long = "setup-command", short = 'c', action = ArgAction::Append)]
setup_command: Vec<String>,
#[command(flatten)]
scope: ObjectScope,
},
/// Delete a cloud environment.
Delete {
/// ID of the environment to delete
id: String,
/// Force delete without checking for integration usage
#[arg(long, default_value_t = false)]
force: bool,
},
/// Get details of a cloud environment.
Get {
/// ID of the environment to get
id: String,
},
/// Update an existing cloud environment.
Update {
/// ID of the environment to update
id: String,
/// Name of the environment (optional, updates if present)
#[arg(long = "name", short = 'n')]
name: Option<String>,
/// Description of the environment (max 240 characters)
#[arg(
long = "description",
value_parser = validate_description,
conflicts_with = "remove_description",
)]
description: Option<String>,
/// Remove the description from the environment
#[arg(long = "remove-description", conflicts_with = "description")]
remove_description: bool,
/// Docker image to use (optional, updates if present)
#[arg(long = "docker-image", short = 'd')]
docker_image: Option<String>,
/// Git repo in format "owner/repo" to add (can be specified multiple times)
#[arg(long = "repo", short = 'r', action = ArgAction::Append)]
repo: Vec<String>,
/// Setup command to add to the end of the list (can be specified multiple times)
#[arg(long = "setup-command", short = 'c', action = ArgAction::Append)]
setup_command: Vec<String>,
/// Git repo in format "owner/repo" to remove (can be specified multiple times)
#[arg(long, action = ArgAction::Append)]
remove_repo: Vec<String>,
/// Setup command to remove from the list (can be specified multiple times)
#[arg(long, action = ArgAction::Append)]
remove_setup_command: Vec<String>,
/// Force update without checking for integration usage
#[arg(long, default_value_t = false)]
force: bool,
},
}
/// Common arguments for selecting an environment when creating an integration.
#[derive(Args, Clone, Debug)]
#[group(required = false, multiple = false)]
pub struct EnvironmentCreateArgs {
/// Cloud environment to run the agent in.
#[arg(long = "environment", value_name = "ENVIRONMENT_ID", short = 'e')]
pub environment: Option<String>,
/// Do not run the agent in an environment (not recommended).
#[arg(long = "no-environment")]
pub no_environment: bool,
}
/// Common arguments for selecting an environment when updating an integration.
#[derive(Args, Clone, Debug)]
#[group(required = false, multiple = false)]
pub struct EnvironmentUpdateArgs {
/// Cloud environment to run the agent in.
#[arg(long = "environment", value_name = "ENVIRONMENT_ID", short = 'e')]
pub environment: Option<String>,
/// Do not run the agent in an environment (not recommended).
#[arg(long = "remove-environment")]
pub remove_environment: bool,
}
/// Image-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum ImageCommand {
/// List available Warp dev base images from Docker Hub.
List,
}
+75
View File
@@ -0,0 +1,75 @@
use clap::{Args, Subcommand};
/// Federated authentication between Oz and cloud providers.
///
/// Oz supports OIDC federation to allow agents to securely authenticate to other systems
/// using short-lived credentials.
#[derive(Debug, Clone, Subcommand)]
pub enum FederateCommand {
/// Issue an identity token for the current Oz agent. This can only be called within a running Oz agent session.
IssueToken(IssueTokenArgs),
/// Issue an identity token for the current Oz agent, in the format expected by Google Cloud's
/// [executable-sourced credentials](https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#executable-sourced-credentials)
/// mechanism.
#[command(hide = true)]
IssueGcpToken(IssueGcpTokenArgs),
}
#[derive(Debug, Clone, Args)]
#[command(name = "issue-token")]
pub struct IssueTokenArgs {
/// The run ID to issue the token for.
#[arg(long = "run-id")]
pub run_id: String,
/// The audience claim for the identity token.
#[arg(long = "audience")]
pub audience: String,
/// Requested token lifetime (e.g. "1h", "30m").
#[arg(long = "duration", default_value = "1h")]
pub duration: humantime::Duration,
/// Controls how the OIDC token subject is formatted.
///
/// The template consists of a list of claims, which are joined together to
/// form the subject. The default subject template is the principal, such as
/// `user:user-id`.
///
/// Supported components are:
/// - principal (`user:my-user-id`)
/// - scoped_principal (`principal:my-team-id/user:my-user-id`)
/// - email (`email:user@warp.dev`)
/// - teams (`teams:my-team-id`)
/// - environment (`environment:my-environment-id`)
/// - agent_name (`agent_name:my-agent`)
/// - skill_spec (`skill_spec:warpdotdev/repo_path_to_skill`)
/// - run_id (`run_id:abc123`)
/// - host (`host:my-worker-id`)
#[arg(long = "subject-template", num_args = 1..)]
pub subject_template: Option<Vec<String>>,
}
#[derive(Debug, Clone, Args)]
#[command(name = "issue-gcp-token")]
pub struct IssueGcpTokenArgs {
/// The run ID to issue the token for.
#[arg(long = "run-id")]
pub run_id: String,
/// Requested token lifetime (e.g. "1h", "30m").
#[arg(long = "duration", default_value = "1h")]
pub duration: humantime::Duration,
/// The audience for the token request.
#[arg(long, env = "GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE")]
pub audience: String,
/// The requested token type (e.g. "urn:ietf:params:oauth:token-type:id_token").
#[arg(long, env = "GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE")]
pub token_type: String,
/// Optional path to write the token output for caching.
#[arg(long, env = "GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE")]
pub output_file: Option<String>,
}
+78
View File
@@ -0,0 +1,78 @@
use clap::{Args, Subcommand, ValueEnum};
/// Commands to support third-party agent harnesses running within Oz.
///
/// These commands are invoked by external agent harnesses (e.g. Claude Code)
/// during a cloud agent run to interact with Oz platform APIs.
#[derive(Debug, Clone, Args)]
pub struct HarnessSupportArgs {
/// The run ID to associate with harness-support API calls.
#[arg(long = "run-id", env = "OZ_RUN_ID")]
pub run_id: String,
#[command(subcommand)]
pub command: HarnessSupportCommand,
}
#[derive(Debug, Clone, Subcommand)]
pub enum HarnessSupportCommand {
/// Verify connectivity by fetching and displaying the current run.
#[command(hide = true)]
Ping,
/// Report an artifact back to the Oz platform.
ReportArtifact(ReportArtifactArgs),
/// Send a progress notification to the task's originating platform (Slack, Linear, etc.).
NotifyUser(NotifyUserArgs),
/// Report task completion or failure, as well as a summary of the task.
FinishTask(FinishTaskArgs),
}
#[derive(Debug, Clone, Args)]
pub struct ReportArtifactArgs {
#[command(subcommand)]
pub command: ReportArtifactCommand,
}
#[derive(Debug, Clone, Subcommand)]
pub enum ReportArtifactCommand {
/// Report a pull request artifact.
PullRequest(PullRequestArtifactArgs),
}
#[derive(Debug, Clone, Args)]
pub struct PullRequestArtifactArgs {
/// URL of the pull request.
#[arg(long)]
pub url: String,
/// Branch name associated with the pull request.
#[arg(long)]
pub branch: String,
}
#[derive(Debug, Clone, Args)]
pub struct NotifyUserArgs {
/// The message to send as a progress update.
#[arg(long)]
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum TaskStatus {
Success,
Failure,
}
#[derive(Debug, Clone, Args)]
pub struct FinishTaskArgs {
/// Whether the task succeeded or failed.
#[arg(long)]
pub status: TaskStatus,
/// A summary of the task outcome.
#[arg(long)]
pub summary: String,
}
+97
View File
@@ -0,0 +1,97 @@
use clap::{Args, Subcommand};
use crate::{
config_file::ConfigFileArgs,
environment::{EnvironmentCreateArgs, EnvironmentUpdateArgs},
mcp::MCPSpec,
model::ModelArgs,
provider::ProviderType,
};
/// Integration-related subcommands.
#[derive(Debug, Clone, Subcommand)]
#[command(visible_alias = "i")]
pub enum IntegrationCommand {
/// Create a new integration.
Create(CreateIntegrationArgs),
/// Update an integration.
Update(UpdateIntegrationArgs),
/// List simple integrations and their connection status.
List,
}
#[derive(Debug, Clone, Args)]
pub struct CreateIntegrationArgs {
/// Provider to create the integration for.
#[arg(value_enum)]
pub provider: ProviderType,
#[command(flatten)]
pub model: ModelArgs,
#[clap(flatten)]
pub environment: EnvironmentCreateArgs,
#[command(flatten)]
pub config_file: ConfigFileArgs,
/// MCP servers to configure for this integration.
///
/// Can be specified as:
/// - A path to a JSON file containing MCP configuration
/// - Inline JSON with MCP server configuration
///
/// Can be specified multiple times to include multiple servers.
#[arg(long = "mcp", value_name = "SPEC")]
pub mcp_specs: Vec<MCPSpec>,
/// Custom instructions for the integration.
#[arg(long = "prompt", short = 'p')]
pub prompt: Option<String>,
/// Worker host ID for self-hosted workers.
/// If not specified or set to "warp", tasks will run on Warp-hosted workers.
#[arg(long = "host")]
pub worker_host: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct UpdateIntegrationArgs {
/// Provider to update the integration for.
#[arg(value_enum)]
pub provider: ProviderType,
#[command(flatten)]
pub model: ModelArgs,
#[command(flatten)]
pub environment: EnvironmentUpdateArgs,
#[command(flatten)]
pub config_file: ConfigFileArgs,
/// MCP servers to configure for this integration.
///
/// Can be specified as:
/// - A path to a JSON file containing MCP configuration
/// - Inline JSON with MCP server configuration
///
/// Can be specified multiple times to include multiple servers.
#[arg(long = "mcp", value_name = "SPEC")]
pub mcp_specs: Vec<MCPSpec>,
/// Remove MCP servers from this integration by server name.
///
/// This removes the server entry whose key matches `SERVER_NAME`.
#[arg(long = "remove-mcp", value_name = "SERVER_NAME")]
pub remove_mcp: Vec<String>,
/// Custom instructions for the integration.
#[arg(long = "prompt", short = 'p')]
pub prompt: Option<String>,
/// Worker host ID for self-hosted workers.
/// If not specified or set to "warp", tasks will run on Warp-hosted workers.
#[arg(long = "host")]
pub worker_host: Option<String>,
}
+82
View File
@@ -0,0 +1,82 @@
//! Reusable JSON output formatting component.
use std::fmt;
use std::ops::Deref;
use std::sync::Arc;
use clap::Args;
use jaq_all::data::{self, DataKind};
use jaq_all::load::FileReportsDisp;
/// A jq filter, compiled and ready to execute against a [`jaq_json::Val`].
///
/// This wraps the compiled [`jaq_all::data::Filter`] with `Clone` and `Debug`
/// implementations.
#[derive(Clone)]
pub struct JqFilter(Arc<data::Filter>);
impl fmt::Debug for JqFilter {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("JqFilter").field(&"<compiled>").finish()
}
}
impl Deref for JqFilter {
type Target = data::Filter;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// CLI argument bundle with flags relevant for commands that produce
/// JSON output.
///
/// Embed with `#[command(flatten)]` on any command which can produce
/// JSON output, and use the `print_raw_json` utility in the `app` crate
/// to format that output.
#[derive(Clone, Debug, Default, Args)]
pub struct JsonOutput {
/// A filter to select values from the response using jq syntax.
///
/// Example: `--jq '.runs[].creator'
///
/// When set, the result of the filter expression is printed instead of
/// the full JSON output. Top-level scalar outputs are automatically
/// unquoted.
#[arg(long = "jq", value_parser = parse_jq_filter, value_name = "FILTER")]
pub filter: Option<JqFilter>,
}
impl JsonOutput {
/// Returns true if this argument bundle requires JSON output regardless
/// of the user-selected `--output-format`.
///
/// For example, `--jq` runs against JSON, so setting it implies the
/// command must fetch and process JSON even when the user asked for
/// pretty/text output.
pub fn force_json_output(&self) -> bool {
self.filter.is_some()
}
}
/// Parse and compile a jq filter source string.
///
/// Used as a clap `value_parser` so invalid filters (syntax errors, unknown
/// names) fail during argument parsing.
pub fn parse_jq_filter(src: &str) -> Result<JqFilter, String> {
let compiled = jaq_all::compile_with::<DataKind>(src, jaq_all::defs(), data::base_funs(), &[])
.map_err(|reports| {
let detail = reports
.iter()
.map(|report| FileReportsDisp::new(report).to_string())
.collect::<String>();
format!("invalid jq filter `{src}`:\n{detail}")
})?;
Ok(JqFilter(Arc::new(compiled)))
}
#[cfg(test)]
#[path = "json_filter_tests.rs"]
mod tests;
@@ -0,0 +1,72 @@
//! Tests for [`super::parse_jq_filter`] and the clap-flattened [`super::JsonOutput`].
use clap::Parser;
use super::*;
/// Tiny wrapper so we can exercise `JsonOutput` through clap in isolation.
#[derive(Debug, Parser)]
struct TestApp {
#[clap(flatten)]
json_filter: JsonOutput,
}
#[test]
fn parse_jq_filter_accepts_simple_filter() {
parse_jq_filter(".foo").expect("valid filter should compile");
}
#[test]
fn parse_jq_filter_accepts_stdlib_functions() {
// Exercises jaq-std defs/funs: .foo | length
parse_jq_filter(".foo | length").expect("stdlib functions should compile");
}
#[test]
fn parse_jq_filter_rejects_syntax_error() {
let err = parse_jq_filter("@").expect_err("syntax error should be rejected");
assert!(
err.contains("`@`"),
"error should quote the filter source with backticks, got: {err}"
);
}
#[test]
fn parse_jq_filter_rejects_empty_string() {
let err = parse_jq_filter("").expect_err("empty filter should be rejected");
assert!(
err.contains("``"),
"error should quote the (empty) filter source with backticks, got: {err}"
);
}
#[test]
fn parse_jq_filter_rejects_unknown_function() {
let err = parse_jq_filter(".foo | bogus_function_name")
.expect_err("unknown function should be rejected");
assert!(
err.contains("bogus_function_name") || err.contains("jq filter"),
"error should mention the filter or the unknown name, got: {err}"
);
}
#[test]
fn clap_populates_filter_when_jq_is_provided() {
let app = TestApp::try_parse_from(["test", "--jq", ".foo"]).expect("valid --jq parses");
assert!(app.json_filter.filter.is_some());
}
#[test]
fn clap_filter_is_none_by_default() {
let app = TestApp::try_parse_from(["test"]).expect("no --jq parses");
assert!(app.json_filter.filter.is_none());
}
#[test]
fn clap_rejects_invalid_filter_at_parse_time() {
// This is the core fail-fast invariant: an invalid filter fails during
// clap parsing, not at runtime.
let err = TestApp::try_parse_from(["test", "--jq", "@"])
.expect_err("invalid --jq is rejected by clap");
assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
}
+693
View File
@@ -0,0 +1,693 @@
#![cfg_attr(target_family = "wasm", allow(dead_code))]
use std::{env, fmt, path::Path};
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use url::Url;
use galaxy_core::channel::ChannelState;
use galaxy_core::features::FeatureFlag;
use crate::agent::OutputFormat;
#[cfg(windows)]
mod process_handle;
pub mod artifact;
pub mod scope;
pub mod skill;
pub mod agent;
pub mod completions;
pub mod config_file;
pub mod environment;
pub mod federate;
pub mod harness_support;
pub mod integration;
pub mod json_filter;
pub mod mcp;
pub mod model;
pub mod provider;
pub mod schedule;
pub mod secret;
pub mod share;
pub mod task;
pub const OZ_RUN_ID_ENV: &str = "OZ_RUN_ID";
pub const OZ_PARENT_RUN_ID_ENV: &str = "OZ_PARENT_RUN_ID";
pub const OZ_CLI_ENV: &str = "OZ_CLI";
pub const OZ_HARNESS_ENV: &str = "OZ_HARNESS";
pub const SERVER_ROOT_URL_OVERRIDE_ENV: &str = "GALAXY_SERVER_ROOT_URL";
pub const WS_SERVER_URL_OVERRIDE_ENV: &str = "GALAXY_WS_SERVER_URL";
pub const SESSION_SHARING_SERVER_URL_OVERRIDE_ENV: &str = "GALAXY_SESSION_SHARING_SERVER_URL";
/// Options related to the parent process that spawned this Warp instance.
#[derive(Debug, Default, Clone, clap::Args)]
pub struct ParentOpts {
/// The ID of the Warp process that spawned this one.
///
/// Used by codepaths that attempt to detect when the parent Warp process
/// has terminated. Guaranteed to be [`None`] when this is the initial
/// Warp process, but may also be [`None`] for Warp child processes if the
/// child process doesn't need to keep track of its parent.
#[arg(long = "parent-pid", hide = true)]
pub pid: Option<u32>,
/// A handle to our parent process.
///
/// Used on Windows for crash recovery instead of parent_pid, as process
/// IDs can be reused, so a process handle is more robust.
#[cfg(windows)]
#[arg(long = "parent-handle", hide = true)]
pub handle: Option<process_handle::ProcessHandle>,
}
/// Hidden worker args used to scope remote-server proxy/daemon sockets by
/// Warp identity without exposing credentials.
#[derive(Debug, Clone, Default, clap::Args)]
pub struct RemoteServerIdentityArgs {
/// Non-secret identity partition key for the remote-server daemon.
#[arg(long = "identity-key", hide = true)]
pub identity_key: String,
}
/// Global options that apply to all CLI commands.
#[derive(Debug, Default, Clone, clap::Args)]
pub struct GlobalOptions {
/// API key for server authentication.
#[arg(long = "api-key", global = true, env = "GALAXY_API_KEY")]
pub api_key: Option<String>,
/// Set the output format.
#[arg(
long = "output-format",
global = true,
value_enum,
default_value_t = OutputFormat::Pretty,
env = "GALAXY_OUTPUT_FORMAT"
)]
pub output_format: OutputFormat,
}
/// Command-line argument parser for the main Warp binary. This is used across all channels.
#[derive(Debug, Default, Parser, Clone)]
#[command(
name = "oz",
display_name = "Oz",
about = r#"The orchestration platform for cloud agents
The Oz CLI is a tool for running, managing, and orchestrating coding agents at scale.
Use the CLI to:
* Launch and inspect cloud agents
* Schedule cloud agents to run in the future
* Manage the environments that cloud agents run in
* Upload secrets to Oz's secure storage"#
)]
#[clap(args_conflicts_with_subcommands = true)]
pub struct Args {
#[clap(flatten)]
global_options: GlobalOptions,
/// Enable debug mode.
#[arg(long = "debug", global = true, help = "Enable debug logging")]
debug: bool,
/// Override the server root URL.
#[arg(
long = "server-root-url",
global = true,
hide = true,
env = "GALAXY_SERVER_ROOT_URL"
)]
server_root_url: Option<String>,
/// Override the websocket server URL.
#[arg(
long = "ws-server-url",
global = true,
hide = true,
env = "GALAXY_WS_SERVER_URL"
)]
ws_server_url: Option<String>,
/// Override the session sharing server URL.
#[arg(
long = "session-sharing-server-url",
global = true,
hide = true,
env = "GALAXY_SESSION_SHARING_SERVER_URL"
)]
session_sharing_server_url: Option<String>,
#[command(subcommand)]
command: Option<Command>,
#[clap(flatten)]
args: AppArgs,
}
/// Flags for the Warp application. Additional binaries, like test runners, may use this type
/// along with their own flags, or convert their flags into an `AppArgs` value.
#[derive(Debug, Default, clap::Args, Clone)]
pub struct AppArgs {
/// True if this instance of Warp was launched at the end of the auto-update process.
#[arg(long = "finish-update", hide = true)]
pub finish_update: bool,
/// Crash recovery mechanism to use if we detect the parent process terminated.
#[cfg(enable_crash_recovery)]
#[arg(long = "crash-recovery-mechanism", value_enum, requires = "ParentOpts")]
pub crash_recovery_mechanism: Option<RecoveryMechanism>,
/// Options related to the parent process that spawned this Warp instance.
#[clap(flatten)]
pub parent: ParentOpts,
/// URLs to open in Warp.
#[arg(hide = true)]
pub urls: Vec<Url>,
}
impl Args {
/// Parses command-line arguments from the operating environment. May exit early if arguments
/// are incorrectly specified.
pub fn from_env() -> Self {
cfg_if::cfg_if! {
// wasm doesn't have any concept of an environment, so skip parsing and return defaults
if #[cfg(target_family = "wasm")] {
Args::default()
} else {
use clap::FromArgMatches as _;
// Check for disabled commands before parsing to prevent help from showing (e.g.
// `warp environment` should not return help text)
if !FeatureFlag::CloudEnvironments.is_enabled() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "environment" {
eprintln!("error: unrecognized subcommand 'environment'\n");
eprintln!("For more information, try '--help'");
std::process::exit(2);
}
}
if !FeatureFlag::ProviderCommand.is_enabled() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "provider" {
eprintln!("error: unrecognized subcommand 'provider'\n");
eprintln!("For more information, try '--help'");
std::process::exit(2);
}
}
if !FeatureFlag::IntegrationCommand.is_enabled() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "integration" {
eprintln!("error: unrecognized subcommand 'integration'\n");
eprintln!("For more information, try '--help'");
std::process::exit(2);
}
}
if !FeatureFlag::ScheduledAmbientAgents.is_enabled() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "schedule" {
eprintln!("error: unrecognized subcommand 'schedule'\n");
eprintln!("For more information, try '--help'");
std::process::exit(2);
}
}
if !FeatureFlag::WarpManagedSecrets.is_enabled() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "secret" {
eprintln!("error: unrecognized subcommand 'secret'\n");
eprintln!("For more information, try '--help'");
std::process::exit(2);
}
}
if !FeatureFlag::OzIdentityFederation.is_enabled() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "federate" {
eprintln!("error: unrecognized subcommand 'federate'\n");
eprintln!("For more information, try '--help'");
std::process::exit(2);
}
}
if !FeatureFlag::ArtifactCommand.is_enabled() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "artifact" {
eprintln!("error: unrecognized subcommand 'artifact'\n");
eprintln!("For more information, try '--help'");
std::process::exit(2);
}
}
let command = Self::clap_command();
command.try_get_matches()
.and_then(|matches| Self::from_arg_matches(&matches))
.unwrap_or_else(|err| {
// We attach a console to ensure help and error messages are printed
// when using the CLI.
#[cfg(windows)]
galaxy_util::windows::attach_to_parent_console();
err.exit()
})
}
}
}
/// Construct the [`clap::Command`] that backs `Args`.
///
/// IMPORTANT: use this instead of [`CommandFactory::command`], since we customize the command at runtime.
pub fn clap_command() -> clap::Command {
let mut command = <Args as CommandFactory>::command();
// Hide the environment subcommands and --environment flags from help text
if !FeatureFlag::CloudEnvironments.is_enabled() {
command = command.mut_subcommand("environment", |c| c.hide(true));
command = command.mut_subcommand("agent", |agent_cmd| {
agent_cmd
.mut_subcommand("run", |run_cmd| {
run_cmd.mut_arg("environment", |arg| arg.hide(true))
})
.mut_subcommand("run-cloud", |cloud_cmd| {
cloud_cmd.mut_arg("environment", |arg| arg.hide(true))
})
});
}
// Hide the --conversation flag from help text
if !FeatureFlag::CloudConversations.is_enabled() {
command = command.mut_subcommand("agent", |agent_cmd| {
agent_cmd
.mut_subcommand("run", |run_cmd| {
run_cmd.mut_arg("conversation", |arg| arg.hide(true))
})
.mut_subcommand("run-cloud", |cloud_cmd| {
cloud_cmd.mut_arg("conversation", |arg| arg.hide(true))
})
});
}
if !FeatureFlag::AmbientAgentsCommandLine.is_enabled() {
command = command.mut_subcommand("agent", |agent_cmd| {
agent_cmd.mut_subcommand("run-cloud", |c| c.hide(true))
});
}
// Hide the provider subcommand from help text
if !FeatureFlag::ProviderCommand.is_enabled() {
command = command.mut_subcommand("provider", |c| c.hide(true));
}
// Hide the integration subcommand from help text
if !FeatureFlag::IntegrationCommand.is_enabled() {
command = command.mut_subcommand("integration", |c| c.hide(true));
}
// Hide the schedule subcommand from help text.
if !FeatureFlag::ScheduledAmbientAgents.is_enabled() {
command = command.mut_subcommand("schedule", |c| c.hide(true));
}
// Hide the secret subcommand from help text.
if !FeatureFlag::WarpManagedSecrets.is_enabled() {
command = command.mut_subcommand("secret", |c| c.hide(true));
}
// Hide the federate subcommand from help text.
if !FeatureFlag::OzIdentityFederation.is_enabled() {
command = command.mut_subcommand("federate", |c| c.hide(true));
}
// Hide the harness-support subcommand from help text.
if !FeatureFlag::AgentHarness.is_enabled() {
command = command.mut_subcommand("harness-support", |c| c.hide(true));
}
// Hide the conversation subcommand and --conversation flag from help text.
if !FeatureFlag::ConversationApi.is_enabled() {
command = command.mut_subcommand("run", |run_cmd| {
run_cmd
.mut_subcommand("conversation", |c| c.hide(true))
.mut_subcommand("get", |get_cmd| {
get_cmd.mut_arg("conversation", |arg| arg.hide(true))
})
});
}
// Hide the message subcommand from help text.
if !FeatureFlag::OrchestrationV2.is_enabled() {
command = command.mut_subcommand("run", |run_cmd| {
run_cmd.mut_subcommand("message", |c| c.hide(true))
});
}
// Hide the artifact subcommand from help text.
if !FeatureFlag::ArtifactCommand.is_enabled() {
command = command.mut_subcommand("artifact", |c| c.hide(true));
}
// Substitute the actual binary name into help output. Ideally clap would do this for us.
let bin_name =
binary_name().unwrap_or_else(|| ChannelState::channel().cli_command_name().to_string());
command = command.after_help(color_print::cformat!(
r#"<bold><underline>Examples:</underline></bold>
<dim>$</dim> <bold>{bin_name} agent run --prompt "Build anything"</bold>
<dim>$</dim> <bold>{bin_name} mcp list</bold>
<bold><underline>Learn more:</underline></bold>
* Use <bold>{bin_name} help</bold> to learn more about each command
* Read the documentation at https://github.com/user/galaxy-ai
"#
));
command
}
/// The requested subcommand, if any.
pub fn command(&self) -> Option<&Command> {
self.command.as_ref()
}
/// Args for the main Warp application, if not running a subcommand.
pub fn app_args(&self) -> &AppArgs {
&self.args
}
/// Extract the main Warp application args.
pub fn into_app_args(self) -> AppArgs {
self.args
}
/// Returns the global options.
pub fn global_options(&self) -> &GlobalOptions {
&self.global_options
}
/// Returns the API key if provided.
pub fn api_key(&self) -> Option<&String> {
self.global_options.api_key.as_ref()
}
/// Returns the output format.
pub fn output_format(&self) -> OutputFormat {
self.global_options.output_format
}
/// Returns true if debug logging is enabled.
pub fn debug(&self) -> bool {
self.debug
}
pub fn server_root_url(&self) -> Option<&str> {
self.server_root_url.as_deref()
}
pub fn ws_server_url(&self) -> Option<&str> {
self.ws_server_url.as_deref()
}
pub fn session_sharing_server_url(&self) -> Option<&str> {
self.session_sharing_server_url.as_deref()
}
}
/// Warp may spawn several worker processes - mostly servers that support the main application.
///
/// These subcommands run those worker processes, which are bundled into the Warp binary.
#[derive(Debug, Clone, Subcommand)]
pub enum WorkerCommand {
/// Run the terminal server.
#[clap(hide = true)]
#[cfg(unix)]
TerminalServer(TerminalServerArgs),
/// Run this process as the plugin host rather than the main app.
#[cfg(feature = "plugin_host")]
#[clap(long_flag = "plugin-host")]
PluginHost {
#[clap(flatten)]
parent: ParentOpts,
},
/// Run the minidump server.
#[clap(hide = true)]
MinidumpServer {
/// Socket name for the minidump server.
socket_name: std::path::PathBuf,
},
/// Run the remote development server proxy over SSH stdio.
/// Ensures the daemon is running, then bridges its stdin/stdout
/// to the daemon via a Unix domain socket.
#[cfg(not(target_family = "wasm"))]
#[clap(hide = true)]
RemoteServerProxy(RemoteServerIdentityArgs),
/// Run the long-lived remote development server daemon.
/// Listens on a Unix domain socket and accepts multiple concurrent
/// connections from proxy processes.
#[cfg(not(target_family = "wasm"))]
#[clap(hide = true)]
RemoteServerDaemon(RemoteServerIdentityArgs),
/// Run a headless ripgrep search worker.
#[cfg(not(target_family = "wasm"))]
#[clap(hide = true)]
RipgrepSearch {
#[clap(flatten)]
parent: ParentOpts,
#[clap(long = "ignore-case")]
ignore_case: bool,
#[clap(long = "multiline")]
multiline: bool,
/// Search pattern.
pattern: String,
/// Paths to search.
paths: Vec<std::path::PathBuf>,
},
}
/// CLI-related subcommands. The command-line interface to Warp isn't a full SDK (e.g. with language bindings),
/// but it allows scripting some Warp functionality.
#[derive(Debug, Clone, Subcommand)]
pub enum CliCommand {
/// Interact with Oz.
#[command(subcommand)]
Agent(crate::agent::AgentCommand),
/// Manage cloud environments.
#[command(subcommand)]
Environment(crate::environment::EnvironmentCommand),
/// Manage MCP servers.
#[command(subcommand)]
MCP(crate::mcp::MCPCommand),
/// Manage runs.
#[command(subcommand, alias = "task")]
Run(crate::task::TaskCommand),
/// Manage available models.
#[command(subcommand)]
Model(crate::model::ModelCommand),
/// Log in to Warp.
Login,
/// Log out of Warp.
Logout,
/// Print information about the logged-in user.
Whoami,
/// Manage providers.
#[command(subcommand)]
Provider(crate::provider::ProviderCommand),
/// Manage integrations.
#[command(subcommand)]
Integration(crate::integration::IntegrationCommand),
/// Create and manage scheduled Oz agents. Scheduled agents run a user-defined task periodically, according to a cron schedule.
///
/// As a shorthand, the `schedule` command behaves identically to `schedule create`.
Schedule(crate::schedule::ScheduleCommand),
/// Manage secrets.
#[command(subcommand)]
Secret(crate::secret::SecretCommand),
/// Issue and manage federated identity tokens.
#[command(subcommand)]
Federate(crate::federate::FederateCommand),
/// Support commands for agent harnesses to integrate with Oz.
#[command(hide = true)]
HarnessSupport(crate::harness_support::HarnessSupportArgs),
/// Manage artifacts.
#[command(subcommand)]
Artifact(crate::artifact::ArtifactCommand),
}
/// A subcommand of the main Warp application. This includes all [`WorkerCommand`]s as well as app-specific debugging tools.
#[derive(Debug, Clone, Subcommand)]
pub enum Command {
#[clap(flatten)]
Worker(WorkerCommand),
/// Commands that make up the Warp CLI.
#[clap(flatten)]
CommandLine(Box<CliCommand>),
/// Generate shell completions for your shell to stdout.
///
///
/// For bash, add the following to ~/.bashrc:
/// source <(path/to/warp completions bash)
///
/// For zsh, add the following to ~/.zshrc:
/// source <(path/to/warp completions zsh)
///
/// For fish, add the following to ~/.config/fish/config.fish:
/// path/to/warp completions fish | source
///
/// For Powershell, add the following to $PROFILE:
/// path\to\warp | Out-String | Invoke-Expression
///
/// If no shell is provided, this defaults to the shell that Warp was run from.
#[command(verbatim_doc_comment)]
Completions {
/// Shell to generate completions for.
#[arg(value_enum)]
shell: Option<clap_complete::aot::Shell>,
},
/// Print debugging information and exit.
#[clap(long_flag = "dump-debug-info")]
DumpDebugInfo,
/// Print telemetry events in production and exit.
#[clap(long_flag = "print-telemetry-events", hide = true)]
#[cfg(not(target_family = "wasm"))]
PrintTelemetryEvents,
}
impl Command {
/// Whether or not the Command should print to stdout.
pub fn prints_to_stdout(&self) -> bool {
match self {
Command::Worker(_) => false,
Command::CommandLine(_) | Command::DumpDebugInfo => true,
Command::Completions { .. } => true,
#[cfg(not(target_family = "wasm"))]
Command::PrintTelemetryEvents => true,
}
}
}
/// Arguments for the terminal server.
#[cfg(not(windows))]
#[derive(Debug, Clone, Default, clap::Args)]
pub struct TerminalServerArgs {
#[clap(flatten)]
pub parent: ParentOpts,
}
#[derive(Debug, Copy, Clone, clap::ValueEnum)]
pub enum RecoveryMechanism {
#[cfg(target_os = "linux")]
#[value(name = "force-x11")]
X11,
#[value(name = "force-dedicated-gpu")]
DedicatedGpu,
#[value(name = "disable-opengl")]
DisableOpenGL,
#[value(name = "force-vulkan")]
ForceVulkan,
}
impl fmt::Display for RecoveryMechanism {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.to_possible_value().expect("no values are skipped");
f.write_str(value.get_name())
}
}
/// Returns the subcommand name to use for starting the terminal server.
pub fn terminal_server_subcommand() -> String {
<Args as CommandFactory>::command()
.find_subcommand("terminal-server")
.expect("terminal-server subcommand not found")
.get_name()
.to_string()
}
/// Returns the subcommand name to use for starting the installation detection server.
pub fn installation_detection_server_subcommand() -> String {
<Args as CommandFactory>::command()
.find_subcommand("installation-detection-server")
.expect("installation-detection-server subcommand not found")
.get_name()
.to_string()
}
/// Returns the subcommand name to use for starting the ripgrep search worker.
#[cfg(not(target_family = "wasm"))]
pub fn ripgrep_search_subcommand() -> String {
<Args as CommandFactory>::command()
.find_subcommand("ripgrep-search")
.expect("ripgrep-search subcommand not found")
.get_name()
.to_string()
}
/// Returns the flag to use when finishing the auto-update process.
pub fn finish_update_flag() -> String {
let command = <Args as CommandFactory>::command();
let flag = command
.get_arguments()
.find(|arg| arg.get_long() == Some("finish-update"))
.expect("finish-update flag not found")
.get_long()
.unwrap();
format!("--{flag}")
}
/// Returns the flag to use for the dump-debug-info subcommand.
pub fn dump_debug_info_flag() -> String {
let command = <Args as CommandFactory>::command();
let flag = command
.find_subcommand("dump-debug-info")
.expect("dump-debug-info subcommand not found")
.get_long_flag()
.expect("dump-debug-info flag not found");
format!("--{flag}")
}
/// Returns a flag that sets the current process as the parent of a Warp subcommand to spawn.
pub fn parent_flag() -> String {
let command = <Args as CommandFactory>::command();
let flag = command
.get_arguments()
.find(|arg| arg.get_long() == Some("parent-pid"))
.expect("parent-pid flag not found")
.get_long()
.unwrap();
format!("--{flag}={}", std::process::id())
}
/// The name that this binary was invoked as.
pub fn binary_name() -> Option<String> {
// Adapted from https://github.com/clap-rs/clap/blob/2c04acd3607e5c4676477ca14948419bb31c73a1/clap_builder/src/builder/command.rs#L888-L902
// Unfortunately, we can't use Command::get_bin_name because it's not populated until args are parsed.
let arg0 = env::args().next()?;
Path::new(&arg0).file_name()?.to_str().map(|s| s.to_owned())
}
#[cfg(test)]
#[path = "lib_tests.rs"]
mod tests;
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
use std::ffi::OsStr;
use clap::builder::PossibleValue;
use clap::error::ErrorKind;
use clap::{Arg, Command, Subcommand};
/// MCP-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum MCPCommand {
/// List MCP servers.
List,
}
/// Represents an MCP server specification from CLI input.
///
/// This is a lightweight representation - full parsing happens in the app layer
/// using `ParsedTemplatableMCPServerResult::from_user_json`.
#[derive(Debug, Clone)]
pub enum MCPSpec {
/// Existing server by UUID.
Uuid(uuid::Uuid),
/// JSON string (full config, server map, or single server).
/// Parsing deferred to app layer.
Json(String),
}
impl clap::builder::ValueParserFactory for MCPSpec {
type Parser = MCPSpecParser;
fn value_parser() -> Self::Parser {
MCPSpecParser
}
}
#[derive(Copy, Clone)]
pub struct MCPSpecParser;
impl clap::builder::TypedValueParser for MCPSpecParser {
type Value = MCPSpec;
fn parse_ref(
&self,
_cmd: &Command,
_arg: Option<&Arg>,
value: &OsStr,
) -> Result<Self::Value, clap::Error> {
let s = value
.to_str()
.ok_or_else(|| clap::Error::raw(ErrorKind::InvalidUtf8, "Invalid UTF-8 in MCP spec"))?;
// Try UUID first
if let Ok(uuid) = uuid::Uuid::parse_str(s) {
return Ok(MCPSpec::Uuid(uuid));
}
// Check if it's a file path
let path = std::path::Path::new(s);
let json_content = if path.exists() && path.is_file() {
std::fs::read_to_string(path).map_err(|e| {
clap::Error::raw(
ErrorKind::Io,
format!("Failed to read MCP config file '{}': {e}", path.display()),
)
})?
} else {
// Treat as inline JSON
s.to_string()
};
Ok(MCPSpec::Json(json_content))
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
[
PossibleValue::new("<path>").help("Path to a JSON file containing MCP config"),
PossibleValue::new("<json>").help("Inline JSON MCP server configuration"),
]
.into_iter(),
))
}
}
#[cfg(test)]
#[path = "mcp_tests.rs"]
mod tests;
+75
View File
@@ -0,0 +1,75 @@
use super::*;
use clap::builder::TypedValueParser;
use std::ffi::OsStr;
fn parse_mcp_spec(value: &str) -> Result<MCPSpec, clap::Error> {
let cmd = clap::Command::new("test");
let parser = MCPSpecParser;
parser.parse_ref(&cmd, None, OsStr::new(value))
}
#[test]
fn test_parse_uuid() {
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
let result = parse_mcp_spec(uuid_str).unwrap();
match result {
MCPSpec::Uuid(uuid) => assert_eq!(uuid.to_string(), uuid_str),
MCPSpec::Json(_) => panic!("Expected Uuid variant"),
}
}
#[test]
fn test_parse_inline_json_cli_server() {
let json = r#"{"server-name": {"command": "npx", "args": ["-y", "mcp-server"]}}"#;
let result = parse_mcp_spec(json).unwrap();
match result {
MCPSpec::Json(s) => assert_eq!(s, json),
MCPSpec::Uuid(_) => panic!("Expected Json variant"),
}
}
#[test]
fn test_parse_inline_json_single_server() {
let json = r#"{"command": "npx", "args": ["-y", "mcp-server"]}"#;
let result = parse_mcp_spec(json).unwrap();
match result {
MCPSpec::Json(s) => assert_eq!(s, json),
MCPSpec::Uuid(_) => panic!("Expected Json variant"),
}
}
#[test]
fn test_parse_inline_json_sse_server() {
let json = r#"{"url": "http://localhost:3000/mcp", "headers": {"API_KEY": "value"}}"#;
let result = parse_mcp_spec(json).unwrap();
match result {
MCPSpec::Json(s) => assert_eq!(s, json),
MCPSpec::Uuid(_) => panic!("Expected Json variant"),
}
}
#[test]
fn test_parse_inline_json_mcp_servers_wrapper() {
let json = r#"{"mcpServers": {"server-name": {"command": "npx", "args": []}}}"#;
let result = parse_mcp_spec(json).unwrap();
match result {
MCPSpec::Json(s) => assert_eq!(s, json),
MCPSpec::Uuid(_) => panic!("Expected Json variant"),
}
}
#[test]
fn test_uuid_takes_precedence_over_json() {
// A valid UUID should be parsed as UUID, not as JSON
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
let result = parse_mcp_spec(uuid_str).unwrap();
assert!(matches!(result, MCPSpec::Uuid(_)));
}
#[test]
fn test_invalid_uuid_treated_as_json() {
// An invalid UUID that looks like it could be one should be treated as JSON
let invalid_uuid = "not-a-valid-uuid";
let result = parse_mcp_spec(invalid_uuid).unwrap();
assert!(matches!(result, MCPSpec::Json(_)));
}
+16
View File
@@ -0,0 +1,16 @@
use clap::{Args, Subcommand};
/// Model-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum ModelCommand {
/// List available models.
List,
}
/// Shared CLI args for selecting a base model.
#[derive(Debug, Clone, Args, Default)]
pub struct ModelArgs {
/// Override the base model used by this command. Use `warp model list` to see available models.
#[arg(long = "model", value_name = "MODEL_ID")]
pub model: Option<String>,
}
+24
View File
@@ -0,0 +1,24 @@
use std::{ffi::c_void, str::FromStr};
use windows::Win32::Foundation::HANDLE;
/// A Windows process handle. This wraps the [`HANDLE`] type to support parsing with `clap`.
#[derive(Clone, Copy, Debug)]
pub struct ProcessHandle(isize);
impl ProcessHandle {
pub fn into_inner(self) -> HANDLE {
HANDLE(self.0 as *mut c_void)
}
}
impl FromStr for ProcessHandle {
type Err = String;
fn from_str(raw: &str) -> Result<Self, Self::Err> {
let pid = raw
.parse::<isize>()
.map_err(|e| format!("invalid parent handle: {e}"))?;
Ok(Self(pid))
}
}
+58
View File
@@ -0,0 +1,58 @@
use clap::{ArgGroup, Args, Subcommand, ValueEnum};
/// Provider-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum ProviderCommand {
Setup(SetupArgs),
List,
}
// If we want these at the top level, we can also set provider as a top level subcommand:
#[derive(Debug, Clone, ValueEnum)]
#[value(rename_all = "snake_case")]
pub enum ProviderType {
Linear,
Slack,
}
impl ProviderType {
pub fn name(&self) -> String {
match self {
ProviderType::Linear => String::from("Linear"),
ProviderType::Slack => String::from("Slack"),
}
}
pub fn slug(&self) -> String {
// add a mapping of provider types to slugs if needed
self.name().to_lowercase()
}
pub fn allowed_in_team_context(&self) -> bool {
match self {
ProviderType::Linear => true,
ProviderType::Slack => true,
}
}
pub fn allowed_in_personal_context(&self) -> bool {
match self {
ProviderType::Linear => false,
ProviderType::Slack => false,
}
}
}
#[derive(Debug, Clone, Args)]
#[command(group(ArgGroup::new("scope").required(false)))]
pub struct SetupArgs {
/// The type of provider to setup.
pub provider_type: ProviderType,
/// Setup provider for a team
#[arg(long, group = "scope")]
pub team: bool,
/// Setup provider for a personal account
#[arg(long, conflicts_with = "team", group = "scope")]
pub personal: bool,
}
+217
View File
@@ -0,0 +1,217 @@
use clap::{Args, Subcommand};
use crate::{
config_file::ConfigFileArgs,
environment::{EnvironmentCreateArgs, EnvironmentUpdateArgs},
mcp::MCPSpec,
model::ModelArgs,
scope::ObjectScope,
skill::SkillSpec,
};
/// `ScheduleCommand` has a slightly unusual definition because we allow `oz schedule` as
// a shorthand for `oz schedule create`.
#[derive(Debug, Clone, Args)]
#[clap(args_conflicts_with_subcommands = true)]
pub struct ScheduleCommand {
#[clap(subcommand)]
subcommand: Option<ScheduleSubcommand>,
#[clap(flatten)]
create: Option<CreateScheduleArgs>,
}
impl ScheduleCommand {
/// Get the specific scheduling subcommand. Returns `None` if using the `oz schedule` creation shorthand.
pub fn subcommand(&self) -> Option<&ScheduleSubcommand> {
self.subcommand.as_ref()
}
/// Convert into the specific scheduling subcommand to run.
pub fn into_subcommand(self) -> ScheduleSubcommand {
if let Some(create) = self.create {
ScheduleSubcommand::Create(create)
} else if let Some(cmd) = self.subcommand {
cmd
} else {
panic!("Either subcommand or create args are required");
}
}
}
/// Schedule-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum ScheduleSubcommand {
/// Create a scheduled Oz agent.
Create(CreateScheduleArgs),
/// List scheduled Oz agents.
List,
/// Get a scheduled Oz agent's configuration.
Get(GetScheduleArgs),
/// Update a scheduled Oz agent.
Update(UpdateScheduleArgs),
/// Pause a scheduled Oz agent.
///
/// A paused agent still exists, but will not run according to its schedule.
Pause(PauseScheduleArgs),
/// Unpause a scheduled Oz agent.
///
/// The agent will resume executing on its previously-configured schedule.
#[command(alias = "resume")]
Unpause(UnpauseScheduleArgs),
/// Delete a scheduled Oz agent.
Delete(DeleteScheduleArgs),
}
#[derive(Debug, Clone, Args)]
#[command(
group(
clap::ArgGroup::new("prompt_group")
.required(true)
.multiple(true)
.args(["prompt", "skill"])
)
)]
pub struct CreateScheduleArgs {
/// Name of the scheduled agent.
#[arg(long = "name")]
pub name: String,
/// Cron schedule expression (e.g., "0 9 * * 1" for 9 AM every Monday).
#[arg(long = "cron")]
pub cron: String,
#[command(flatten)]
pub model: ModelArgs,
#[command(flatten)]
pub environment: EnvironmentCreateArgs,
#[command(flatten)]
pub config_file: ConfigFileArgs,
#[command(flatten)]
pub scope: ObjectScope,
/// MCP servers to configure for this schedule.
///
/// Can be specified as:
/// - A path to a JSON file containing MCP configuration
/// - Inline JSON with MCP server configuration
///
/// Can be specified multiple times to include multiple servers.
#[arg(long = "mcp", value_name = "SPEC")]
pub mcp_specs: Vec<MCPSpec>,
/// Prompt for what the scheduled agent should do.
#[arg(long = "prompt", short = 'p')]
pub prompt: Option<String>,
/// Automate a skill to run on a schedule.
///
/// Format: `repo:skill_name` or `org/repo:skill_name`
///
/// Skills are searched in `.agents/skills/`, `.warp-core/skills/`, `.claude/skills/`, and `.codex/skills/` directories.
/// The skill is resolved at runtime in the agent's cloud environment.
///
/// When used with --prompt, the skill provides the base context and the prompt is the user task.
/// This is useful for running recurring workflows like code reviews, dependency updates, or reports.
#[arg(long = "skill", value_name = "SPEC")]
pub skill: Option<SkillSpec>,
/// Where this job should be hosted.
///
/// Setting "warp" (or omitting this flag) runs it on Warp's infrastructure.
/// Any other value is treated as a self-hosted job and the value will be matched
/// with the self-hosted worker's name.
#[arg(long = "host", value_name = "WORKER_ID")]
pub worker_host: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct PauseScheduleArgs {
/// ID of the schedule to pause.
pub schedule_id: String,
}
#[derive(Debug, Clone, Args)]
pub struct UnpauseScheduleArgs {
/// ID of the schedule to unpause.
pub schedule_id: String,
}
#[derive(Debug, Clone, Args)]
pub struct UpdateScheduleArgs {
/// ID of the schedule to update.
pub schedule_id: String,
/// Update the scheduled agent name.
#[arg(long = "name")]
pub name: Option<String>,
/// Update the cron schedule on which the agent is executed.
#[arg(long = "cron")]
pub cron: Option<String>,
#[command(flatten)]
pub model: ModelArgs,
#[command(flatten)]
pub environment: EnvironmentUpdateArgs,
#[command(flatten)]
pub config_file: ConfigFileArgs,
/// MCP servers to configure for this schedule.
///
/// Can be specified as:
/// - A path to a JSON file containing MCP configuration
/// - Inline JSON with MCP server configuration
///
/// Can be specified multiple times to include multiple servers.
#[arg(long = "mcp", value_name = "SPEC")]
pub mcp_specs: Vec<MCPSpec>,
/// Remove MCP servers from this schedule by server name.
///
/// This removes the server entry whose key matches `SERVER_NAME`.
#[arg(long = "remove-mcp", value_name = "SERVER_NAME")]
pub remove_mcp: Vec<String>,
/// Update the scheduled agent's prompt.
#[arg(long = "prompt", short = 'p')]
pub prompt: Option<String>,
/// Update the skill used as the base prompt for the scheduled agent.
///
/// Format: `skill_name`, `repo:skill_name`, or `org/repo:skill_name`
///
/// Skills are searched in `.agents/skills/`, `.warp-core/skills/`, `.claude/skills/`, and `.codex/skills/` directories.
/// The skill is resolved at runtime in the agent's cloud environment.
#[arg(long = "skill", value_name = "SPEC", conflicts_with = "remove_skill")]
pub skill: Option<SkillSpec>,
/// Remove the skill from this scheduled agent.
#[arg(long = "remove-skill", conflicts_with = "skill")]
pub remove_skill: bool,
/// Where this job should be hosted.
///
/// Setting "warp" runs it on Warp's infrastructure.
/// Any other value is treated as a self-hosted job and the value will be matched
/// with the self-hosted worker's name.
#[arg(long = "host", value_name = "WORKER_ID")]
pub worker_host: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct DeleteScheduleArgs {
/// ID of the schedule to delete.
pub schedule_id: String,
}
#[derive(Debug, Clone, Args)]
pub struct GetScheduleArgs {
/// ID of the schedule to get.
pub schedule_id: String,
}
+13
View File
@@ -0,0 +1,13 @@
use clap::Args;
/// Common args for scoping objects to team or personal drives.
#[derive(Args, Debug, Clone)]
#[group(required = false, multiple = false)]
pub struct ObjectScope {
/// Create at the team level.
#[arg(long, group = "scope")]
pub team: bool,
/// Create as private to your account.
#[arg(long, conflicts_with = "team", group = "scope")]
pub personal: bool,
}
+203
View File
@@ -0,0 +1,203 @@
use std::{fmt, path::PathBuf};
use clap::{Args, Subcommand, ValueEnum};
use crate::scope::ObjectScope;
/// Secret-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum SecretCommand {
/// Create a new secret.
///
/// Use `oz secret create anthropic api-key <NAME>` to create a Claude/Anthropic auth secret.
Create(CreateSecretArgs),
/// Delete a secret.
Delete(DeleteSecretArgs),
/// Update a secret.
///
/// This command supports changing the value (via the `--value` or `--value-file` flags) or the description.
/// Moving or renaming secrets is not currently supported.
Update(UpdateSecretArgs),
/// List secrets.
List(ListSecretsArgs),
}
#[derive(Debug, Clone, Args)]
#[command(args_conflicts_with_subcommands = true)]
pub struct CreateSecretArgs {
/// Provider-specific creation subcommand.
#[command(subcommand)]
pub provider: Option<CreateProvider>,
// --- Fields below are only used when no subcommand is given (generic create). ---
/// Name of the secret to create.
pub name: Option<String>,
#[arg(long = "type", short = 't', default_value_t = Default::default())]
pub secret_type: SecretType,
#[clap(flatten)]
pub value: ValueArgs,
/// Description of the secret.
#[arg(long = "description", short = 'd')]
pub description: Option<String>,
#[clap(flatten)]
pub scope: ObjectScope,
}
/// Provider-specific secret creation subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum CreateProvider {
/// Create a Claude/Anthropic auth secret.
Anthropic(AnthropicCreateArgs),
}
#[derive(Debug, Clone, Args)]
pub struct AnthropicCreateArgs {
#[command(subcommand)]
pub method: AnthropicMethod,
}
/// Anthropic credential type.
#[derive(Debug, Clone, Subcommand)]
pub enum AnthropicMethod {
/// Direct Anthropic API key.
#[command(name = "api-key")]
ApiKey(AnthropicApiKeyArgs),
/// Anthropic API key via Amazon Bedrock.
#[command(name = "bedrock-api-key")]
BedrockApiKey(BedrockApiKeyArgs),
/// Anthropic Bedrock authentication via AWS access keys.
#[command(name = "bedrock-access-key")]
BedrockAccessKey(BedrockAccessKeyArgs),
}
/// Fields shared by all provider-specific secret creation subcommands.
#[derive(Debug, Clone, Args)]
pub struct CommonSecretCreateArgs {
/// Name of the secret.
pub name: String,
/// Description of the secret.
#[arg(long = "description", short = 'd')]
pub description: Option<String>,
#[clap(flatten)]
pub scope: ObjectScope,
}
/// Arguments for creating an Anthropic API key secret.
#[derive(Debug, Clone, Args)]
pub struct AnthropicApiKeyArgs {
#[clap(flatten)]
pub common: CommonSecretCreateArgs,
#[clap(flatten)]
pub value: ValueArgs,
}
/// Arguments for creating an Anthropic Bedrock API key secret.
#[derive(Debug, Clone, Args)]
pub struct BedrockApiKeyArgs {
#[clap(flatten)]
pub common: CommonSecretCreateArgs,
/// Bedrock API key. If not provided, prompts interactively.
#[arg(long = "bedrock-api-key")]
pub bedrock_api_key: Option<String>,
/// AWS region for the Bedrock endpoint. If not provided, prompts interactively.
#[arg(long = "region")]
pub region: Option<String>,
}
/// Arguments for creating an Anthropic Bedrock access key secret.
#[derive(Debug, Clone, Args)]
pub struct BedrockAccessKeyArgs {
#[clap(flatten)]
pub common: CommonSecretCreateArgs,
/// AWS access key ID. If not provided, prompts interactively.
#[arg(long = "access-key-id")]
pub access_key_id: Option<String>,
/// AWS secret access key. If not provided, prompts interactively.
#[arg(long = "secret-access-key")]
pub secret_access_key: Option<String>,
/// AWS session token. If not provided, prompts interactively.
#[arg(long = "session-token")]
pub session_token: Option<String>,
/// AWS region for the Bedrock endpoint. If not provided, prompts interactively.
#[arg(long = "region")]
pub region: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct DeleteSecretArgs {
/// Name of the secret to delete.
pub name: String,
/// Delete without asking for confirmation.
#[arg(long, default_value_t = false)]
pub force: bool,
#[clap(flatten)]
pub scope: ObjectScope,
}
#[derive(Debug, Clone, Args)]
pub struct UpdateSecretArgs {
/// Name of the secret to update.
pub name: String,
/// Prompt for a new value for the secret.
#[arg(long = "value", conflicts_with = "value_file")]
pub value: bool,
#[clap(flatten)]
pub value_args: ValueArgs,
/// New description for the secret. If omitted, the description is not changed.
#[arg(long = "description", short = 'd')]
pub description: Option<String>,
#[clap(flatten)]
pub scope: ObjectScope,
}
#[derive(Debug, Clone, Args)]
pub struct ListSecretsArgs {
// TODO: consider flags to filter secrets.
}
#[derive(Debug, Clone, Args)]
pub struct ValueArgs {
/// File to read the secret value from. If not provided, the secret value will be read from
/// standard input.
#[arg(long = "value-file", short = 'f')]
pub value_file: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
#[value(rename_all = "kebab-case")]
pub enum SecretType {
#[default]
RawValue,
AnthropicApiKey,
// Not exposed via the CLI `--type` flag; constructed internally for provider subcommands.
#[value(skip)]
AnthropicBedrockApiKey,
}
impl fmt::Display for SecretType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SecretType::RawValue => write!(f, "raw-value"),
SecretType::AnthropicApiKey => write!(f, "anthropic-api-key"),
SecretType::AnthropicBedrockApiKey => write!(f, "anthropic-bedrock-api-key"),
}
}
}
+160
View File
@@ -0,0 +1,160 @@
use std::ffi::OsStr;
use std::fmt;
use std::str::FromStr;
use clap::builder::{EnumValueParser, PossibleValue};
use clap::error::ErrorKind;
use clap::{Arg, Args, Command, ValueEnum};
/// Arguments for sharing a session or other object.
#[derive(Debug, Clone, Args)]
pub struct ShareArgs {
/// Share the agent's session
///
/// Learn more at https://docs.warp.dev/knowledge-and-collaboration/session-sharing
#[arg(long = "share", value_name = "RECIPIENTS", num_args=0..=1)]
pub share: Option<Vec<ShareRequest>>,
}
impl ShareArgs {
/// Returns `true` if the session should be shared.
pub fn is_shared(&self) -> bool {
self.share.is_some()
}
}
/// An individual sharing request, identifying:
/// * Who to share with
/// * Their permission level
#[derive(Debug, Clone)]
pub struct ShareRequest {
pub subject: ShareSubject,
pub access_level: ShareAccessLevel,
}
impl fmt::Display for ShareRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.subject {
ShareSubject::Team => write!(f, "team:{}", self.access_level)?,
ShareSubject::Public => write!(f, "public:{}", self.access_level)?,
ShareSubject::User { email } => write!(f, "{email}:{}", self.access_level)?,
}
Ok(())
}
}
impl clap::builder::ValueParserFactory for ShareRequest {
type Parser = ShareRequestParser;
fn value_parser() -> Self::Parser {
ShareRequestParser
}
}
#[derive(Copy, Clone)]
pub struct ShareRequestParser;
impl clap::builder::TypedValueParser for ShareRequestParser {
type Value = ShareRequest;
fn parse_ref(
&self,
cmd: &Command,
arg: Option<&Arg>,
value: &OsStr,
) -> Result<Self::Value, clap::Error> {
let value_str = value
.to_str()
.ok_or_else(|| clap::Error::raw(ErrorKind::InvalidUtf8, "Invalid share recipient"))?;
// If there's a `:`, treat the first part as the subject and the second as the access level. Otherwise, default to `view` access.
let (subject_str, level_str) = match value_str.split_once(':') {
Some((subject, level)) => (subject, Some(level)),
None => (value_str, None),
};
let subject = ShareSubject::from_str(subject_str)?;
let access_level = match level_str {
Some(level) => EnumValueParser::new().parse_ref(cmd, arg, OsStr::new(level))?,
None => ShareAccessLevel::View,
};
Ok(ShareRequest {
subject,
access_level,
})
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
[
PossibleValue::new("team:view")
.help("Share with your team, view-only")
.alias("team"),
PossibleValue::new("team:edit").help("Share with your team, with edit access"),
PossibleValue::new("public:view")
.help("Share with anyone who has the link, view-only")
.alias("public"),
PossibleValue::new("public:edit")
.help("Share with anyone who has the link, with edit access"),
PossibleValue::new("<user@email.com>:view")
.help("Share with <user@email.com>, view-only")
.alias("<user@email.com>"),
PossibleValue::new("<user@email.com>:edit")
.help("Share with <user@email.com>, with edit access"),
]
.into_iter(),
))
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum ShareAccessLevel {
View,
Edit,
}
impl fmt::Display for ShareAccessLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ShareAccessLevel::View => write!(f, "view"),
ShareAccessLevel::Edit => write!(f, "edit"),
}
}
}
#[derive(Debug, Clone)]
pub enum ShareSubject {
/// Share with everyone on the caller's current team.
Team,
/// Share with anyone who has the link (anyone-with-link ACL).
/// Subject to the workspace-level anyone-with-link sharing setting.
Public,
/// Share with an individual user by email.
User { email: String },
}
impl FromStr for ShareSubject {
type Err = clap::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"team" => Ok(ShareSubject::Team),
"public" => Ok(ShareSubject::Public),
email if email.contains('@') => Ok(ShareSubject::User {
email: email.to_string(),
}),
other => Err(clap::Error::raw(
ErrorKind::InvalidValue,
format!(
"Cannot share with '{other}'. Expected 'team', 'public', or an email address"
),
)),
}
}
}
#[cfg(test)]
#[path = "share_tests.rs"]
mod tests;
+114
View File
@@ -0,0 +1,114 @@
use super::*;
use clap::builder::TypedValueParser;
use std::ffi::OsStr;
fn parse_share_request(value: &str) -> Result<ShareRequest, clap::Error> {
let cmd = clap::Command::new("test");
let parser = ShareRequestParser;
parser.parse_ref(&cmd, None, OsStr::new(value))
}
#[test]
fn test_parse_team_default() {
let result = parse_share_request("team").unwrap();
assert!(matches!(result.subject, ShareSubject::Team));
assert!(matches!(result.access_level, ShareAccessLevel::View));
}
#[test]
fn test_parse_team_view() {
let result = parse_share_request("team:view").unwrap();
assert!(matches!(result.subject, ShareSubject::Team));
assert!(matches!(result.access_level, ShareAccessLevel::View));
}
#[test]
fn test_parse_team_edit() {
let result = parse_share_request("team:edit").unwrap();
assert!(matches!(result.subject, ShareSubject::Team));
assert!(matches!(result.access_level, ShareAccessLevel::Edit));
}
#[test]
fn test_parse_user_default() {
let result = parse_share_request("ben@warp.dev").unwrap();
match result.subject {
ShareSubject::User { email } => assert_eq!(email, "ben@warp.dev"),
_ => panic!("Expected User subject"),
}
assert!(matches!(result.access_level, ShareAccessLevel::View));
}
#[test]
fn test_parse_user_view() {
let result = parse_share_request("ben@warp.dev:view").unwrap();
match result.subject {
ShareSubject::User { email } => assert_eq!(email, "ben@warp.dev"),
_ => panic!("Expected User subject"),
}
assert!(matches!(result.access_level, ShareAccessLevel::View));
}
#[test]
fn test_parse_user_edit() {
let result = parse_share_request("ben@warp.dev:edit").unwrap();
match result.subject {
ShareSubject::User { email } => assert_eq!(email, "ben@warp.dev"),
_ => panic!("Expected User subject"),
}
assert!(matches!(result.access_level, ShareAccessLevel::Edit));
}
#[test]
fn test_parse_invalid_format() {
let result = parse_share_request("invalid");
assert!(result.is_err());
}
#[test]
fn test_parse_invalid_access_level() {
let result = parse_share_request("team:invalid");
assert!(result.is_err());
}
#[test]
fn test_parse_public_default() {
let result = parse_share_request("public").unwrap();
assert!(matches!(result.subject, ShareSubject::Public));
assert!(matches!(result.access_level, ShareAccessLevel::View));
}
#[test]
fn test_parse_public_view() {
let result = parse_share_request("public:view").unwrap();
assert!(matches!(result.subject, ShareSubject::Public));
assert!(matches!(result.access_level, ShareAccessLevel::View));
}
#[test]
fn test_parse_public_edit() {
let result = parse_share_request("public:edit").unwrap();
assert!(matches!(result.subject, ShareSubject::Public));
assert!(matches!(result.access_level, ShareAccessLevel::Edit));
}
#[test]
fn test_parse_public_invalid_access_level() {
let result = parse_share_request("public:invalid");
assert!(result.is_err());
}
#[test]
fn test_public_request_display() {
let request = ShareRequest {
subject: ShareSubject::Public,
access_level: ShareAccessLevel::View,
};
assert_eq!(format!("{request}"), "public:view");
let request = ShareRequest {
subject: ShareSubject::Public,
access_level: ShareAccessLevel::Edit,
};
assert_eq!(format!("{request}"), "public:edit");
}
+192
View File
@@ -0,0 +1,192 @@
use std::path::Path;
use std::{fmt, str::FromStr};
/// A skill specifier that can reference a skill in a specific repo or search the current directory.
///
/// The skill identifier (after the optional `repo:` or `org/repo:` prefix) can be either:
/// - A **simple skill name** - searched across skill directories with precedence (`.agents/skills/`, `.warp-core/skills/`, `.claude/skills/`, `.codex/skills/`)
/// - A **full path to SKILL.md** - resolved directly without precedence
///
/// # Formats
/// - `skill_name` - Simple name, search current directory
/// - `skill_path` - Full path (e.g., `.claude/skills/foo/SKILL.md`)
/// - `repo:skill_name` - Simple name in specific repo
/// - `repo:skill_path` - Full path in specific repo
/// - `org/repo:skill_name` - Simple name with org and repo
/// - `org/repo:skill_path` - Full path with org and repo
///
/// # Examples
///
/// Simple skill names (searched with directory precedence):
/// ```ignore
/// code-review // searches .agents/skills/, .warp-core/skills/, .claude/skills/, .codex/skills/
/// warp-internal:code-review // searches in "warp-internal" repo
/// warpdotdev/warp-internal:code-review // searches in specific org/repo
/// ```
///
/// Full paths (resolved directly, no precedence):
/// ```ignore
/// .agents/skills/my-skill/SKILL.md // directly resolves this path
/// warp-server:.claude/skills/deploy/SKILL.md // exact path in "warp-server" repo
/// warpdotdev/warp-internal:.claude/skills/code-review/SKILL.md // exact path in org/repo
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkillSpec {
/// Optional GitHub organization (e.g., "warpdotdev" in "warpdotdev/warp-internal:code-review")
pub org: Option<String>,
/// Optional repository name (e.g., "warp-internal")
pub repo: Option<String>,
/// The skill identifier - either a simple name or a full path to SKILL.md.
///
/// - **Simple name** (e.g., `"code-review"`): Searched across `.agents/skills/`, `.warp-core/skills/`, `.claude/skills/`, `.codex/skills/`
/// in precedence order. The name is used to construct paths like `.claude/skills/code-review/SKILL.md`.
///
/// - **Full path** (e.g., `".claude/skills/code-review/SKILL.md"`): Resolved directly without precedence.
/// Detected by presence of path separators (e.g., `/` or `\`).
///
/// Use [`is_full_path()`](Self::is_full_path) to distinguish between the two formats.
pub skill_identifier: String,
}
impl SkillSpec {
/// Create a new skill spec with org and repo qualifiers.
pub fn with_org_and_repo(org: String, repo: String, skill_identifier: String) -> Self {
Self {
org: Some(org),
repo: Some(repo),
skill_identifier,
}
}
/// Create a new skill spec with a repo qualifier.
pub fn with_repo(repo: String, skill_identifier: String) -> Self {
Self {
org: None,
repo: Some(repo),
skill_identifier,
}
}
/// Create a new skill spec without any qualifier.
pub fn without_repo(skill_identifier: String) -> Self {
Self {
org: None,
repo: None,
skill_identifier,
}
}
/// Returns true if `skill_identifier` is a full path, false if it's a simple skill name.
///
/// A full path contains path separators (`/` or `\`), such as:
/// - `.claude/skills/deploy/SKILL.md`
/// - `.agents/skills/my-skill/SKILL.md`
///
/// A simple skill name has no path separators, such as:
/// - `code-review`
/// - `deploy`
///
/// Full paths are resolved directly, while simple names are searched across
/// skill directories in precedence order (`.agents/skills/`, `.warp-core/skills/`, `.claude/skills/`, `.codex/skills/`).
///
/// Uses cross-platform path semantics via [`std::path::Path`].
pub fn is_full_path(&self) -> bool {
let path = Path::new(&self.skill_identifier);
// A path with multiple components (e.g., "foo/bar" or "foo\\bar") is a full path.
// A single component (e.g., "code-review") is just a name.
path.components().count() > 1
}
/// Extracts the displayable skill name from this spec.
///
/// # Returns
/// - For path-style identifiers (e.g., `.agents/skills/slack-triage/SKILL.md`): returns the parent directory name
/// - For simple names: returns the name as-is
/// - For invalid paths: falls back to file stem or the identifier itself
pub fn skill_name(&self) -> String {
let skill_identifier = self.skill_identifier.trim();
let path = Path::new(skill_identifier);
if path.components().count() > 1 {
if let Some(skill_name) = path
.parent()
.and_then(Path::file_name)
.and_then(|name| name.to_str())
{
return skill_name.to_string();
}
if let Some(file_stem) = path.file_stem().and_then(|stem| stem.to_str()) {
return file_stem.to_string();
}
}
skill_identifier.to_string()
}
}
impl FromStr for SkillSpec {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err("Skill specifier cannot be empty".to_string());
}
// Check for [qualifier:]skill_identifier format
if let Some((qualifier, skill_identifier)) = s.split_once(':') {
let qualifier = qualifier.trim();
let skill_identifier = skill_identifier.trim();
if qualifier.is_empty() {
return Err(
"Qualifier cannot be empty in 'repo:skill_identifier' format".to_string(),
);
}
if skill_identifier.is_empty() {
return Err("Skill identifier cannot be empty".to_string());
}
// Check for org/repo format in qualifier
if let Some((org, repo)) = qualifier.split_once('/') {
let org = org.trim();
let repo = repo.trim();
if org.is_empty() {
return Err("Organization cannot be empty".to_string());
}
if repo.is_empty() {
return Err("Repository name cannot be empty".to_string());
}
Ok(Self::with_org_and_repo(
org.to_string(),
repo.to_string(),
skill_identifier.to_string(),
))
} else {
Ok(Self::with_repo(
qualifier.to_string(),
skill_identifier.to_string(),
))
}
} else {
Ok(Self::without_repo(s.to_string()))
}
}
}
impl fmt::Display for SkillSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (&self.org, &self.repo) {
(Some(org), Some(repo)) => write!(f, "{}/{}:{}", org, repo, self.skill_identifier),
(None, Some(repo)) => write!(f, "{}:{}", repo, self.skill_identifier),
_ => write!(f, "{}", self.skill_identifier),
}
}
}
#[cfg(test)]
#[path = "skill_tests.rs"]
mod tests;
+142
View File
@@ -0,0 +1,142 @@
use super::*;
#[test]
fn test_parse_simple_skill_name() {
let spec: SkillSpec = "code-review".parse().unwrap();
assert_eq!(spec.org, None);
assert_eq!(spec.repo, None);
assert_eq!(spec.skill_identifier, "code-review");
assert!(!spec.is_full_path());
}
#[test]
fn test_parse_repo_qualified() {
let spec: SkillSpec = "warp-internal:code-review".parse().unwrap();
assert_eq!(spec.org, None);
assert_eq!(spec.repo, Some("warp-internal".to_string()));
assert_eq!(spec.skill_identifier, "code-review");
assert!(!spec.is_full_path());
}
#[test]
fn test_parse_org_repo_qualified() {
let spec: SkillSpec = "warpdotdev/warp-internal:code-review".parse().unwrap();
assert_eq!(spec.org, Some("warpdotdev".to_string()));
assert_eq!(spec.repo, Some("warp-internal".to_string()));
assert_eq!(spec.skill_identifier, "code-review");
assert!(!spec.is_full_path());
}
#[test]
fn test_parse_full_path_with_org_repo() {
let spec: SkillSpec = "warpdotdev/warp-internal:.claude/skills/deploy/SKILL.md"
.parse()
.unwrap();
assert_eq!(spec.org, Some("warpdotdev".to_string()));
assert_eq!(spec.repo, Some("warp-internal".to_string()));
assert_eq!(spec.skill_identifier, ".claude/skills/deploy/SKILL.md");
assert!(spec.is_full_path());
}
#[test]
fn test_parse_full_path_with_repo() {
let spec: SkillSpec = "warp-server:.agents/skills/test/SKILL.md".parse().unwrap();
assert_eq!(spec.org, None);
assert_eq!(spec.repo, Some("warp-server".to_string()));
assert_eq!(spec.skill_identifier, ".agents/skills/test/SKILL.md");
assert!(spec.is_full_path());
}
#[test]
fn test_display_simple_name() {
let spec = SkillSpec::without_repo("code-review".to_string());
assert_eq!(spec.to_string(), "code-review");
}
#[test]
fn test_display_repo_qualified() {
let spec = SkillSpec::with_repo("warp-internal".to_string(), "code-review".to_string());
assert_eq!(spec.to_string(), "warp-internal:code-review");
}
#[test]
fn test_display_org_repo_qualified() {
let spec = SkillSpec::with_org_and_repo(
"warpdotdev".to_string(),
"warp-internal".to_string(),
"code-review".to_string(),
);
assert_eq!(spec.to_string(), "warpdotdev/warp-internal:code-review");
}
#[test]
fn test_display_full_path() {
let spec = SkillSpec::with_org_and_repo(
"warpdotdev".to_string(),
"warp-internal".to_string(),
".claude/skills/deploy/SKILL.md".to_string(),
);
assert_eq!(
spec.to_string(),
"warpdotdev/warp-internal:.claude/skills/deploy/SKILL.md"
);
}
#[test]
fn test_is_full_path_with_slash() {
let spec = SkillSpec::without_repo(".claude/skills/deploy/SKILL.md".to_string());
assert!(spec.is_full_path());
}
#[test]
fn test_is_not_full_path_single_component_md() {
// A single component (even with .md extension) is treated as a skill name, not a full path
let spec = SkillSpec::without_repo("something.md".to_string());
assert!(!spec.is_full_path());
}
#[test]
fn test_is_not_full_path() {
let spec = SkillSpec::without_repo("code-review".to_string());
assert!(!spec.is_full_path());
}
#[test]
fn test_parse_empty_fails() {
let result: Result<SkillSpec, _> = "".parse();
assert!(result.is_err());
}
#[test]
fn test_parse_empty_qualifier_fails() {
let result: Result<SkillSpec, _> = ":code-review".parse();
assert!(result.is_err());
}
#[test]
fn test_parse_empty_path_fails() {
let result: Result<SkillSpec, _> = "warp-internal:".parse();
assert!(result.is_err());
}
#[test]
fn test_skill_name_simple_name() {
let spec: SkillSpec = "feedback-triage-bot".parse().unwrap();
assert_eq!(spec.skill_name(), "feedback-triage-bot");
}
#[test]
fn test_skill_name_repo_qualified_name() {
let spec: SkillSpec = "warpdotdev/feedback-triage-bot:feedback-triage-bot"
.parse()
.unwrap();
assert_eq!(spec.skill_name(), "feedback-triage-bot");
}
#[test]
fn test_skill_name_repo_qualified_path() {
let spec: SkillSpec = "warpdotdev/feedback-triage-bot:.agents/skills/slack-triage/SKILL.md"
.parse()
.unwrap();
assert_eq!(spec.skill_name(), "slack-triage");
}
+317
View File
@@ -0,0 +1,317 @@
use chrono::{DateTime, Utc};
use clap::{Args, Subcommand, ValueEnum};
use crate::json_filter::JsonOutput;
/// Task-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum TaskCommand {
/// List ambient agent tasks.
List(ListTasksArgs),
/// Get status of a specific ambient agent task.
Get(TaskGetArgs),
/// Retrieve the conversation for a specific run or conversation.
#[command(subcommand)]
Conversation(ConversationCommand),
/// Messages sent to and from runs.
#[command(subcommand)]
Message(MessageCommand),
}
/// Conversation-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum ConversationCommand {
/// Get a conversation by conversation ID.
Get(ConversationGetArgs),
}
/// Message-related subcommands.
#[derive(Debug, Clone, Subcommand)]
pub enum MessageCommand {
/// Watch for new messages delivered to a run.
Watch(MessageWatchArgs),
/// Send a message from one run to one or more recipient runs.
Send(MessageSendArgs),
/// List inbox message headers for a run.
List(MessageListArgs),
/// Read a full message body.
Read(MessageReadArgs),
/// Mark a message as delivered.
#[command(alias = "delivered")]
MarkDelivered(MessageDeliveredArgs),
}
#[derive(Debug, Clone, Args)]
pub struct ConversationGetArgs {
/// The conversation ID to retrieve.
pub conversation_id: String,
}
#[derive(Debug, Clone, Args)]
pub struct MessageSendArgs {
/// Recipient run ID. Repeat the flag to send to multiple recipients.
#[arg(long = "to", required = true, num_args = 1.., value_delimiter = ',')]
pub to: Vec<String>,
/// Message subject.
#[arg(long = "subject")]
pub subject: String,
/// Message body.
#[arg(long = "body")]
pub body: String,
/// Sender run ID.
#[arg(long = "sender-run-id")]
pub sender_run_id: String,
}
#[derive(Debug, Clone, Args)]
pub struct MessageListArgs {
/// The run ID whose inbox should be listed.
pub run_id: String,
/// Only return unread messages.
#[arg(long = "unread")]
pub unread: bool,
/// Only return messages sent at or after this RFC3339 timestamp.
#[arg(long = "since")]
pub since: Option<String>,
/// Maximum number of messages to return (default: 50).
#[arg(
short = 'L',
long = "limit",
default_value = "50",
value_parser = clap::value_parser!(i32).range(1..)
)]
pub limit: i32,
}
#[derive(Debug, Clone, Args)]
pub struct MessageWatchArgs {
/// The run ID whose inbox should be watched.
pub run_id: String,
/// Resume after this event sequence (inclusive cursor for reconnects).
#[arg(
long = "since-sequence",
default_value = "0",
value_parser = clap::value_parser!(i64).range(0..)
)]
pub since_sequence: i64,
}
#[derive(Debug, Clone, Args)]
pub struct MessageReadArgs {
/// The message ID to read.
pub message_id: String,
}
#[derive(Debug, Clone, Args)]
pub struct MessageDeliveredArgs {
/// The message ID to mark as delivered.
pub message_id: String,
}
#[derive(Debug, Clone, Args)]
pub struct ListTasksArgs {
/// Maximum number of tasks to return (default: 10).
#[arg(short = 'L', long = "limit", default_value = "10")]
pub limit: i32,
/// Filter by run state. Repeat the flag to match any of multiple states.
#[arg(long = "state", value_enum, value_name = "STATE")]
pub state: Vec<RunStateArg>,
/// Filter by run source.
#[arg(long = "source", value_enum, value_name = "SOURCE")]
pub source: Option<RunSourceArg>,
/// Filter by where the run executed.
#[arg(long = "execution-location", value_enum, value_name = "LOC")]
pub execution_location: Option<ExecutionLocationArg>,
/// Filter by creator ID.
#[arg(long = "creator", value_name = "UID")]
pub creator: Option<String>,
/// Filter by environment ID.
#[arg(long = "environment", value_name = "ENV_ID")]
pub environment: Option<String>,
/// Filter by skill specification (e.g. `owner/repo:path/to/SKILL.md`).
#[arg(long = "skill", value_name = "SPEC")]
pub skill: Option<String>,
/// Filter to runs created by a specific scheduled agent.
#[arg(long = "schedule", value_name = "SCHEDULE_ID")]
pub schedule: Option<String>,
/// Filter to descendants of a specific run.
#[arg(long = "ancestor-run", value_name = "RUN_ID")]
pub ancestor_run: Option<String>,
/// Filter by agent config name.
#[arg(long = "name", value_name = "NAME")]
pub name: Option<String>,
/// Filter by model ID.
#[arg(long = "model", value_name = "MODEL_ID")]
pub model: Option<String>,
/// Filter by produced artifact type.
#[arg(long = "artifact-type", value_enum, value_name = "TYPE")]
pub artifact_type: Option<ArtifactTypeArg>,
/// Only include runs created after the given timestamp.
#[arg(long = "created-after", value_name = "RFC3339", value_parser = parse_rfc3339)]
pub created_after: Option<DateTime<Utc>>,
/// Only include runs created before the given timestamp.
#[arg(long = "created-before", value_name = "RFC3339", value_parser = parse_rfc3339)]
pub created_before: Option<DateTime<Utc>>,
/// Only include runs updated after the given timestamp.
#[arg(long = "updated-after", value_name = "RFC3339", value_parser = parse_rfc3339)]
pub updated_after: Option<DateTime<Utc>>,
/// Fuzzy search across run title, prompt, and skill spec.
#[arg(short = 'q', long = "query", value_name = "TEXT")]
pub query: Option<String>,
/// Sort field.
#[arg(long = "sort-by", value_enum, value_name = "FIELD")]
pub sort_by: Option<RunSortByArg>,
/// Sort direction.
#[arg(long = "sort-order", value_enum, value_name = "DIR")]
pub sort_order: Option<RunSortOrderArg>,
/// Opaque pagination cursor from a previous list response.
///
/// When using `--cursor`, `--sort-by` and `--sort-order` must match the
/// values used to obtain the cursor.
#[arg(long = "cursor", value_name = "CURSOR")]
pub cursor: Option<String>,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
/// Parse an RFC 3339 timestamp into a UTC `DateTime`.
fn parse_rfc3339(s: &str) -> Result<DateTime<Utc>, String> {
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| format!("invalid RFC 3339 timestamp '{s}': {e}"))
}
/// Run state values accepted by `--state`. Repeatable; multiple values match any of them.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum RunStateArg {
#[value(name = "queued")]
Queued,
#[value(name = "pending")]
Pending,
#[value(name = "claimed")]
Claimed,
#[value(name = "in-progress")]
InProgress,
#[value(name = "succeeded")]
Succeeded,
#[value(name = "failed")]
Failed,
#[value(name = "error")]
Error,
#[value(name = "blocked")]
Blocked,
#[value(name = "cancelled")]
Cancelled,
}
/// Run source values accepted by `--source`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum RunSourceArg {
#[value(name = "api")]
Api,
#[value(name = "cli")]
Cli,
#[value(name = "slack")]
Slack,
#[value(name = "linear")]
Linear,
#[value(name = "scheduled-agent")]
ScheduledAgent,
#[value(name = "web-app")]
WebApp,
#[value(name = "cloud-mode")]
CloudMode,
#[value(name = "github-action")]
GitHubAction,
#[value(name = "interactive")]
Interactive,
}
/// Execution-location values accepted by `--execution-location`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ExecutionLocationArg {
#[value(name = "local")]
Local,
#[value(name = "remote")]
Remote,
}
/// Artifact-type values accepted by `--artifact-type`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ArtifactTypeArg {
#[value(name = "plan")]
Plan,
#[value(name = "pull-request")]
PullRequest,
#[value(name = "screenshot")]
Screenshot,
#[value(name = "file")]
File,
}
/// Sort-by values accepted by `--sort-by`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum RunSortByArg {
#[value(name = "updated-at")]
UpdatedAt,
#[value(name = "created-at")]
CreatedAt,
#[value(name = "title")]
Title,
#[value(name = "agent")]
Agent,
}
/// Sort-order values accepted by `--sort-order`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum RunSortOrderArg {
#[value(name = "asc")]
Asc,
#[value(name = "desc")]
Desc,
}
#[derive(Debug, Clone, Args)]
pub struct TaskGetArgs {
/// The task ID to get status for.
pub task_id: String,
/// Retrieve the conversation for this run instead of the run status.
#[arg(long = "conversation")]
pub conversation: bool,
/// JSON formatting configuration.
#[command(flatten)]
pub json_output: JsonOutput,
}
#[cfg(test)]
#[path = "task_tests.rs"]
mod tests;
+168
View File
@@ -0,0 +1,168 @@
//! Tests for `ListTasksArgs` clap parsing.
use chrono::TimeZone;
use clap::Parser;
use super::*;
/// Tiny wrapper so we can parse `ListTasksArgs` in isolation, without wiring up the whole Args
/// type from `lib.rs`.
#[derive(Debug, Parser)]
struct TestApp {
#[clap(flatten)]
args: ListTasksArgs,
}
fn parse(argv: &[&str]) -> TestApp {
let mut full = vec!["test"];
full.extend_from_slice(argv);
TestApp::try_parse_from(full).expect("parse succeeds")
}
fn parse_err(argv: &[&str]) -> clap::Error {
let mut full = vec!["test"];
full.extend_from_slice(argv);
TestApp::try_parse_from(full).expect_err("parse fails")
}
#[test]
fn defaults_match_pre_change_behavior() {
let TestApp { args } = parse(&[]);
assert_eq!(args.limit, 10);
assert!(args.state.is_empty());
assert!(args.source.is_none());
assert!(args.execution_location.is_none());
assert!(args.creator.is_none());
assert!(args.environment.is_none());
assert!(args.skill.is_none());
assert!(args.schedule.is_none());
assert!(args.ancestor_run.is_none());
assert!(args.name.is_none());
assert!(args.model.is_none());
assert!(args.artifact_type.is_none());
assert!(args.created_after.is_none());
assert!(args.created_before.is_none());
assert!(args.updated_after.is_none());
assert!(args.query.is_none());
assert!(args.sort_by.is_none());
assert!(args.sort_order.is_none());
assert!(args.cursor.is_none());
}
#[test]
fn state_flag_is_repeatable() {
let TestApp { args } = parse(&["--state", "failed", "--state", "error"]);
assert_eq!(args.state, vec![RunStateArg::Failed, RunStateArg::Error]);
}
#[test]
fn all_filter_flags_parse() {
let TestApp { args } = parse(&[
"--limit",
"42",
"--state",
"in-progress",
"--source",
"api",
"--execution-location",
"remote",
"--creator",
"user-uid",
"--environment",
"env-123",
"--skill",
"owner/repo:SKILL.md",
"--schedule",
"sched-1",
"--ancestor-run",
"run-parent",
"--name",
"nightly",
"--model",
"claude-4-5",
"--artifact-type",
"pull-request",
"--created-after",
"2026-04-01T00:00:00Z",
"--created-before",
"2026-04-02T00:00:00Z",
"--updated-after",
"2026-04-03T12:30:00Z",
"-q",
"oz run",
"--sort-by",
"created-at",
"--sort-order",
"asc",
"--cursor",
"abcd==",
]);
assert_eq!(args.limit, 42);
assert_eq!(args.state, vec![RunStateArg::InProgress]);
assert_eq!(args.source, Some(RunSourceArg::Api));
assert_eq!(args.execution_location, Some(ExecutionLocationArg::Remote));
assert_eq!(args.creator.as_deref(), Some("user-uid"));
assert_eq!(args.environment.as_deref(), Some("env-123"));
assert_eq!(args.skill.as_deref(), Some("owner/repo:SKILL.md"));
assert_eq!(args.schedule.as_deref(), Some("sched-1"));
assert_eq!(args.ancestor_run.as_deref(), Some("run-parent"));
assert_eq!(args.name.as_deref(), Some("nightly"));
assert_eq!(args.model.as_deref(), Some("claude-4-5"));
assert_eq!(args.artifact_type, Some(ArtifactTypeArg::PullRequest));
assert_eq!(
args.created_after,
Some(Utc.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap())
);
assert_eq!(
args.created_before,
Some(Utc.with_ymd_and_hms(2026, 4, 2, 0, 0, 0).unwrap())
);
assert_eq!(
args.updated_after,
Some(Utc.with_ymd_and_hms(2026, 4, 3, 12, 30, 0).unwrap())
);
assert_eq!(args.query.as_deref(), Some("oz run"));
assert_eq!(args.sort_by, Some(RunSortByArg::CreatedAt));
assert_eq!(args.sort_order, Some(RunSortOrderArg::Asc));
assert_eq!(args.cursor.as_deref(), Some("abcd=="));
}
#[test]
fn invalid_state_is_rejected() {
let err = parse_err(&["--state", "bogus"]);
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
}
#[test]
fn invalid_sort_by_is_rejected() {
let err = parse_err(&["--sort-by", "random"]);
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
}
#[test]
fn invalid_execution_location_is_rejected() {
let err = parse_err(&["--execution-location", "moon"]);
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
}
#[test]
fn invalid_artifact_type_is_rejected() {
let err = parse_err(&["--artifact-type", "poem"]);
assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue);
}
#[test]
fn invalid_created_after_timestamp_is_rejected() {
let err = parse_err(&["--created-after", "not-a-timestamp"]);
assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
}
#[test]
fn timestamps_are_converted_to_utc() {
// Non-UTC offsets should be normalized to UTC in the parsed value.
let TestApp { args } = parse(&["--updated-after", "2026-04-03T12:30:00+02:00"]);
assert_eq!(
args.updated_after,
Some(Utc.with_ymd_and_hms(2026, 4, 3, 10, 30, 0).unwrap())
);
}