From 04d99b897784ee6df1a9803654dd00b5bfed8a00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:21:37 +0000 Subject: [PATCH 1/3] Initial plan From a8262c445251e4c6c457e73d9a54a5c2b06560c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:28:10 +0000 Subject: [PATCH 2/3] feat(cli): support positional get instance and multi-token query Co-authored-by: mbe24 <7420624+mbe24@users.noreply.github.com> --- README.md | 10 +++--- docs/commands/get.md | 13 ++++++-- src/cmd/get.rs | 78 ++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 56 ++++++++++++++++++++++++++++++- 4 files changed, 141 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5aed986..69bec05 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,10 @@ Use this when you run `99problems` directly in a terminal to fetch context from 99problems get --repo github/gitignore --id 2402 --type pr --include-review-comments # Search GitLab issues -99problems get --platform gitlab -q "repo:veloren/veloren is:issue state:closed terrain" +99problems get --platform gitlab -q repo:veloren/veloren is:issue state:closed terrain # Fetch Jira issue by key -99problems get --platform jira --id CLOUD-12817 +99problems get jira --id CLOUD-12817 # Fetch Bitbucket Cloud PR by ID 99problems get --platform bitbucket --deployment cloud --repo workspace/repo_slug --id 1 --type pr @@ -87,13 +87,13 @@ Use this when you run `99problems` directly in a terminal to fetch context from 99problems get --platform bitbucket --deployment selfhosted --url https://bitbucket.mycompany.com --repo PROJECT/repo_slug --id 1 # Stream as JSON Lines for pipelines -99problems get -q "repo:github/gitignore is:issue state:open" --output-mode stream --format jsonl +99problems get -q repo:github/gitignore is:issue state:open --output-mode stream --format jsonl ``` ## Commands ```text -99problems get [OPTIONS] Fetch issue and pull request conversations +99problems get [INSTANCE] [OPTIONS] Fetch issue and pull request conversations 99problems skill init [OPTIONS] Scaffold the canonical Agent Skill 99problems config Inspect and edit .99problems configuration 99problems completions Generate shell completion scripts @@ -157,7 +157,7 @@ token = "pat_or_bearer_token" Bitbucket support is pull-request only; when `--type` is omitted, `99problems` defaults to PRs. For Bitbucket Cloud, use an app-password, repository access token, or workspace-level access token (premium feature) in `token`. -Selection order: `--instance` -> single configured instance -> `default_instance`. +Selection order: positional `INSTANCE`/`--instance` -> single configured instance -> `default_instance`. ### Telemetry diff --git a/docs/commands/get.md b/docs/commands/get.md index 9e606e8..6088815 100644 --- a/docs/commands/get.md +++ b/docs/commands/get.md @@ -7,7 +7,7 @@ Fetch issue or pull-request conversations from configured providers. Search mode: ```bash -99problems get -q "repo:owner/repo is:issue state:open" +99problems get -q repo:owner/repo is:issue state:open ``` ID mode: @@ -16,14 +16,21 @@ ID mode: 99problems get --repo owner/repo --id 1842 --type issue ``` +With positional instance alias: + +```bash +99problems get jira -i CLOUD-12817 +``` + ## Core Inputs -- `-q, --query`: raw provider query string. +- `-q, --query`: raw provider query string (accepts multiple tokens until the next flag). - `-i, --id`: fetch one issue/PR directly. - `-r, --repo`: provider repo/project shorthand. - `-t, --type`: `issue` or `pr`. - `-p, --platform`: direct platform selection. -- `-I, --instance`: select configured instance alias. +- ``: optional positional configured instance alias. +- `-I, --instance`: select configured instance alias (backward compatible with positional form). ## Query Shorthand Flags diff --git a/src/cmd/get.rs b/src/cmd/get.rs index 42e9a4c..3c52509 100644 --- a/src/cmd/get.rs +++ b/src/cmd/get.rs @@ -132,13 +132,17 @@ impl ResolvedOutputFormat { #[allow(clippy::struct_excessive_bools)] #[command( next_line_help = true, - after_help = "Examples:\n 99problems get --repo schemaorg/schemaorg --id 1842\n 99problems get --repo github/gitignore --id 2402 --type pr --include-review-comments\n 99problems get -q \"repo:owner/repo state:open label:bug\" --output-mode stream --format jsonl" + after_help = "Examples:\n 99problems get jira -i CLOUD-12817\n 99problems get --repo schemaorg/schemaorg --id 1842\n 99problems get --repo github/gitignore --id 2402 --type pr --include-review-comments\n 99problems get -q repo:owner/repo state:open label:bug --output-mode stream --format jsonl" )] pub(crate) struct GetArgs { + /// Named instance alias from .99problems ([instances.]) as first positional argument + #[arg(index = 1, value_name = "INSTANCE")] + pub(crate) instance_positional: Option, + /// Full search query (same syntax as the platform's web UI search bar) /// e.g. "state:closed Event repo:owner/repo" - #[arg(short = 'q', long)] - pub(crate) query: Option, + #[arg(short = 'q', long, num_args = 1..)] + pub(crate) query: Option>, /// Shorthand for adding "repo:owner/repo" to the query (alias: --project) #[arg(short = 'r', long, visible_alias = "project")] @@ -425,8 +429,9 @@ fn default_platform_host(platform: &str) -> &'static str { } fn load_config_for_get(args: &GetArgs) -> Result { + let instance = resolve_instance_alias(args)?; if args.platform.is_none() - && args.instance.is_none() + && instance.is_none() && args.url.is_none() && args.deployment.is_none() && args.kind.is_none() @@ -441,7 +446,7 @@ fn load_config_for_get(args: &GetArgs) -> Result { Config::load_with_options(ResolveOptions { platform: args.platform.as_ref().map(Platform::as_str), - instance: args.instance.as_deref(), + instance, url: args.url.as_deref(), kind: args.kind.as_ref().map(ContentType::as_str), deployment: args.deployment.as_ref().map(DeploymentType::as_str), @@ -495,6 +500,25 @@ fn emit_get_warnings(cfg: &Config, args: &GetArgs) -> Result<()> { Ok(()) } +fn resolve_instance_alias(args: &GetArgs) -> Result> { + match ( + args.instance_positional.as_deref(), + args.instance.as_deref(), + ) { + (Some(positional), Some(flag)) if positional != flag => Err(AppError::usage(format!( + "Conflicting instance values: positional instance '{positional}' does not match --instance '{flag}'. Use either `99problems get ...` or `99problems get --instance ...`." + )) + .into()), + (Some(positional), _) => Ok(Some(positional)), + (None, Some(flag)) => Ok(Some(flag)), + (None, None) => Ok(None), + } +} + +fn join_query_tokens(args: &GetArgs) -> Option { + args.query.as_ref().map(|tokens| tokens.join(" ")) +} + fn build_source_for_platform(cfg: &Config, telemetry_active: bool) -> Result> { match cfg.platform.as_str() { "github" => Ok(Box::new(GitHubSource::new(telemetry_active)?)), @@ -558,7 +582,7 @@ fn build_fetch_request(cfg: &Config, args: &GetArgs) -> Result { } let query = Query::build( - args.query.clone(), + join_query_tokens(args), effective_kind, repo, state, @@ -792,6 +816,7 @@ mod tests { fn args() -> GetArgs { GetArgs { + instance_positional: None, query: None, repo: Some("owner/repo".into()), state: None, @@ -944,7 +969,7 @@ mod tests { args.id = None; args.no_body = true; args.repo = Some("CPQ".into()); - args.query = Some("architectural".into()); + args.query = Some(vec!["architectural".into()]); let req = build_fetch_request(&cfg, &args).unwrap(); assert!(!req.include_body); } @@ -995,4 +1020,43 @@ mod tests { let cfg = bitbucket_config("selfhosted", "pr", true); assert_eq!(trace_deployment_for_platform(&cfg), Some("dc")); } + + #[test] + fn resolve_instance_alias_uses_positional_value() { + let mut args = args(); + args.instance_positional = Some("jira".into()); + assert_eq!(resolve_instance_alias(&args).unwrap(), Some("jira")); + } + + #[test] + fn resolve_instance_alias_allows_matching_positional_and_flag_values() { + let mut args = args(); + args.instance_positional = Some("jira".into()); + args.instance = Some("jira".into()); + assert_eq!(resolve_instance_alias(&args).unwrap(), Some("jira")); + } + + #[test] + fn resolve_instance_alias_rejects_conflicting_values() { + let mut args = args(); + args.instance_positional = Some("jira".into()); + args.instance = Some("gitlab".into()); + let err = resolve_instance_alias(&args).unwrap_err().to_string(); + assert!(err.contains("Conflicting instance values")); + assert!(err.contains("does not match")); + } + + #[test] + fn join_query_tokens_combines_values_with_spaces() { + let mut args = args(); + args.query = Some(vec![ + "is:issue".into(), + "state:open".into(), + "architectural".into(), + ]); + assert_eq!( + join_query_tokens(&args).as_deref(), + Some("is:issue state:open architectural") + ); + } } diff --git a/src/main.rs b/src/main.rs index 5a7b544..18b515d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,7 @@ enum ErrorFormat { subcommand_required = true, arg_required_else_help = true, next_line_help = true, - after_help = "Examples:\n 99problems get --repo schemaorg/schemaorg --id 1842\n 99problems get -q \"repo:github/gitignore is:pr 2402\" --include-review-comments\n 99problems skill init\n 99problems man --output docs/man", + after_help = "Examples:\n 99problems get github --id 1842 --repo schemaorg/schemaorg\n 99problems get -q repo:github/gitignore is:pr 2402 --include-review-comments\n 99problems skill init\n 99problems man --output docs/man", version )] struct Cli { @@ -163,6 +163,7 @@ mod tests { .expect("expected get subcommand to parse"); match cli.command { Commands::Get(args) => { + assert_eq!(args.instance_positional.as_deref(), None); assert_eq!(args.repo.as_deref(), Some("owner/repo")); assert_eq!(args.id.as_deref(), Some("1")); } @@ -181,6 +182,7 @@ mod tests { .expect("expected got alias to parse"); match cli.command { Commands::Get(args) => { + assert_eq!(args.instance_positional.as_deref(), None); assert_eq!(args.repo.as_deref(), Some("owner/repo")); assert_eq!(args.id.as_deref(), Some("2")); } @@ -314,4 +316,56 @@ mod tests { } } } + + #[test] + fn parses_get_with_positional_instance_alias() { + let cli = Cli::try_parse_from(["99problems", "get", "jira", "-i", "25"]) + .expect("expected positional instance alias to parse"); + match cli.command { + Commands::Get(args) => { + assert_eq!(args.instance_positional.as_deref(), Some("jira")); + assert_eq!(args.instance.as_deref(), None); + assert_eq!(args.id.as_deref(), Some("25")); + } + Commands::Skill(_) + | Commands::Config(_) + | Commands::Completions { .. } + | Commands::Man(_) => { + panic!("expected get command") + } + } + } + + #[test] + fn parses_get_query_as_unquoted_multi_token_value() { + let cli = Cli::try_parse_from([ + "99problems", + "get", + "-q", + "is:issue", + "state:open", + "architectural", + "--no-comments", + ]) + .expect("expected multi-token query to parse"); + match cli.command { + Commands::Get(args) => { + assert_eq!( + args.query, + Some(vec![ + "is:issue".to_string(), + "state:open".to_string(), + "architectural".to_string() + ]) + ); + assert!(args.no_comments); + } + Commands::Skill(_) + | Commands::Config(_) + | Commands::Completions { .. } + | Commands::Man(_) => { + panic!("expected get command") + } + } + } } From 87a86235e32e70a40acb02c427525e61f876859d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:31:56 +0000 Subject: [PATCH 3/3] chore: add helper docs for get instance and query parsing Co-authored-by: mbe24 <7420624+mbe24@users.noreply.github.com> --- src/cmd/get.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cmd/get.rs b/src/cmd/get.rs index 3c52509..0fb48b1 100644 --- a/src/cmd/get.rs +++ b/src/cmd/get.rs @@ -500,6 +500,9 @@ fn emit_get_warnings(cfg: &Config, args: &GetArgs) -> Result<()> { Ok(()) } +/// Resolve instance alias from positional `` or `--instance`. +/// +/// If both are provided they must match; otherwise a usage error is returned. fn resolve_instance_alias(args: &GetArgs) -> Result> { match ( args.instance_positional.as_deref(), @@ -515,6 +518,7 @@ fn resolve_instance_alias(args: &GetArgs) -> Result> { } } +/// Join parsed `--query` tokens back into a single provider query string. fn join_query_tokens(args: &GetArgs) -> Option { args.query.as_ref().map(|tokens| tokens.join(" ")) }