From 4820344e596c907f21dc537c4dcd05e4c42b1031 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 05:00:53 +0000 Subject: [PATCH 1/2] Initial plan From 4e093022c6ee7510ed3f01dce2a74b46011e997a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 05:05:27 +0000 Subject: [PATCH 2/2] Remove unused pkg/slashcommand package Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- pkg/slashcommand/README.md | 159 --------------- pkg/slashcommand/example_test.go | 44 ---- pkg/slashcommand/parser.go | 155 -------------- pkg/slashcommand/parser_test.go | 338 ------------------------------- 4 files changed, 696 deletions(-) delete mode 100644 pkg/slashcommand/README.md delete mode 100644 pkg/slashcommand/example_test.go delete mode 100644 pkg/slashcommand/parser.go delete mode 100644 pkg/slashcommand/parser_test.go diff --git a/pkg/slashcommand/README.md b/pkg/slashcommand/README.md deleted file mode 100644 index 4a6151fa..00000000 --- a/pkg/slashcommand/README.md +++ /dev/null @@ -1,159 +0,0 @@ -# slashcommand - -Package `slashcommand` provides a parser for slash commands commonly used in AI coding assistants. - -## Overview - -This package parses slash commands using bash-like argument parsing. The parser can find slash commands anywhere in the input text, not just at the beginning: -``` -/task-name arg1 "arg 2" arg3 -``` - -The parser extracts: -- **Task name**: The command identifier (without the leading `/`) -- **Arguments**: Positional arguments accessed via `$ARGUMENTS`, `$1`, `$2`, `$3`, etc. -- **Found status**: Boolean indicating whether a slash command was found - -Arguments are parsed like bash: -- Quoted arguments (single or double quotes) can contain spaces -- Quotes are removed from parsed arguments -- Escape sequences are supported in double quotes (`\"`) -- Arguments are extracted until end of line - -## Installation - -```bash -go get github.com/kitproj/coding-context-cli/pkg/slashcommand -``` - -## Usage - -```go -import "github.com/kitproj/coding-context-cli/pkg/slashcommand" - -// Parse a simple command -taskName, params, found, err := slashcommand.ParseSlashCommand("/fix-bug") -// taskName: "fix-bug" -// params: map[] -// found: true - -// Parse a command with arguments -taskName, params, found, err := slashcommand.ParseSlashCommand("/fix-bug 123") -// taskName: "fix-bug" -// params: map["ARGUMENTS": "123", "1": "123"] -// found: true - -// Parse a command with quoted arguments -taskName, params, found, err := slashcommand.ParseSlashCommand(`/code-review "Fix login bug" high`) -// taskName: "code-review" -// params: map["ARGUMENTS": "\"Fix login bug\" high", "1": "Fix login bug", "2": "high"] -// found: true - -// Command found in middle of text -taskName, params, found, err := slashcommand.ParseSlashCommand("Please /deploy production now") -// taskName: "deploy" -// params: map["ARGUMENTS": "production now", "1": "production", "2": "now"] -// found: true - -// No command found -taskName, params, found, err := slashcommand.ParseSlashCommand("No command here") -// taskName: "" -// params: nil -// found: false -``` - -## Command Format - -### Basic Structure -``` -/task-name arg1 "arg 2" arg3 ... -``` - -### Argument Parsing Rules -1. Slash commands can appear **anywhere** in the input text -2. Task name comes immediately after the `/` (no spaces) -3. Arguments are extracted until end of line (newline stops argument collection) -4. Arguments can be quoted with single (`'`) or double (`"`) quotes -5. Quoted arguments can contain spaces -6. Quotes are removed from parsed arguments -7. Double quotes support escape sequences: `\"` -8. Single quotes preserve everything literally (no escapes) -9. Text before the `/` is ignored (prefix lost) -10. Text after a newline is ignored (suffix lost) - -### Returned Parameters -The `params` map contains: -- `ARGUMENTS`: The full argument string (with quotes preserved) -- `1`, `2`, `3`, etc.: Individual positional arguments (with quotes removed) - -### Valid Examples -``` -/fix-bug # No arguments -/fix-bug 123 # Single argument: $1 = "123" -/deploy staging v1.2.3 # Two arguments: $1 = "staging", $2 = "v1.2.3" -/code-review "PR #42" # Quoted argument: $1 = "PR #42" -/echo 'He said "hello"' # Single quotes preserve quotes: $1 = "He said \"hello\"" -/echo "He said \"hello\"" # Escaped quotes in double quotes: $1 = "He said \"hello\"" -Please /fix-bug 123 today # Command in middle: task = "fix-bug", $1 = "123", $2 = "today" -Text /deploy prod\nNext line # Arguments stop at newline: task = "deploy", $1 = "prod" -``` - -### Cases with No Command Found -``` -fix-bug # Missing leading /: found = false -No command here # No slash: found = false -``` - -## Error Handling - -The parser returns errors only for malformed commands (e.g., unclosed quotes). If no slash command is found, the function returns `found=false` without an error. - -```go -// No command found - not an error -_, _, found, err := slashcommand.ParseSlashCommand("No command here") -// found: false, err: nil - -// Unclosed quote - returns error -_, _, found, err := slashcommand.ParseSlashCommand(`/fix-bug "unclosed`) -// found: false, err: "unclosed quote in arguments" -``` - -## API - -### ParseSlashCommand - -```go -func ParseSlashCommand(command string) (taskName string, params map[string]string, found bool, err error) -``` - -Parses a slash command string and extracts the task name and arguments. The function searches for a slash command anywhere in the input text. - -**Parameters:** -- `command` (string): The text that may contain a slash command - -**Returns:** -- `taskName` (string): The task name without the leading `/` -- `params` (map[string]string): Contains `ARGUMENTS` (full arg string) and `1`, `2`, `3`, etc. (positional args) -- `found` (bool): True if a slash command was found, false otherwise -- `err` (error): Error if the command format is invalid (e.g., unclosed quotes) - -## Testing - -The package includes comprehensive tests covering: -- Commands without arguments -- Commands with single and multiple arguments -- Quoted arguments (both single and double quotes) -- Escaped quotes -- Empty quoted arguments -- Commands embedded in text (prefix/suffix text) -- Commands with newlines -- Edge cases and error conditions - -Run tests with: -```bash -go test -v ./pkg/slashcommand -``` - -## License - -This package is part of the [coding-context-cli](https://github.com/kitproj/coding-context-cli) project and is licensed under the MIT License. diff --git a/pkg/slashcommand/example_test.go b/pkg/slashcommand/example_test.go deleted file mode 100644 index e2b46642..00000000 --- a/pkg/slashcommand/example_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package slashcommand_test - -import ( - "fmt" - - "github.com/kitproj/coding-context-cli/pkg/slashcommand" -) - -func ExampleParseSlashCommand() { - // Parse a simple command without parameters - taskName, params, found, err := slashcommand.ParseSlashCommand("/fix-bug") - if err != nil { - fmt.Printf("Error: %v\n", err) - return - } - if found { - fmt.Printf("Task: %s, Params: %v\n", taskName, params) - } - - // Parse a command with single argument - taskName, params, found, err = slashcommand.ParseSlashCommand("/fix-bug 123") - if err != nil { - fmt.Printf("Error: %v\n", err) - return - } - if found { - fmt.Printf("Task: %s, $1: %s\n", taskName, params["1"]) - } - - // Parse a command with multiple arguments - taskName, params, found, err = slashcommand.ParseSlashCommand(`/implement-feature "User Login" high`) - if err != nil { - fmt.Printf("Error: %v\n", err) - return - } - if found { - fmt.Printf("Task: %s, $1: %s, $2: %s\n", taskName, params["1"], params["2"]) - } - - // Output: - // Task: fix-bug, Params: map[] - // Task: fix-bug, $1: 123 - // Task: implement-feature, $1: User Login, $2: high -} diff --git a/pkg/slashcommand/parser.go b/pkg/slashcommand/parser.go deleted file mode 100644 index 6e667f35..00000000 --- a/pkg/slashcommand/parser.go +++ /dev/null @@ -1,155 +0,0 @@ -package slashcommand - -import ( - "fmt" - "strings" -) - -// ParseSlashCommand parses a slash command string and extracts the task name and parameters. -// It searches for a slash command anywhere in the input string, not just at the beginning. -// The expected format is: /task-name arg1 "arg 2" arg3 -// -// The function will find the slash command even if it's embedded in other text. For example: -// - "Please /fix-bug 123 today" -> taskName: "fix-bug", params: {"ARGUMENTS": "123 today", "1": "123", "2": "today"}, found: true -// - "Some text /code-review" -> taskName: "code-review", params: {}, found: true -// -// Arguments are parsed like Bash: -// - Quoted arguments can contain spaces -// - Both single and double quotes are supported -// - Quotes are removed from the parsed arguments -// - Arguments are extracted until end of line -// -// Examples: -// - "/fix-bug 123" -> taskName: "fix-bug", params: {"ARGUMENTS": "123", "1": "123"}, found: true -// - "/code-review \"PR #42\" high" -> taskName: "code-review", params: {"ARGUMENTS": "\"PR #42\" high", "1": "PR #42", "2": "high"}, found: true -// - "no command here" -> taskName: "", params: nil, found: false -// -// Returns: -// - taskName: the task name (without the leading slash) -// - params: a map containing: -// - "ARGUMENTS": the full argument string (with quotes preserved) -// - "1", "2", "3", etc.: positional arguments (with quotes removed) -// - found: true if a slash command was found, false otherwise -// - err: an error if the command format is invalid (e.g., unclosed quotes) -func ParseSlashCommand(command string) (taskName string, params map[string]string, found bool, err error) { - // Find the slash command anywhere in the string - slashIdx := strings.Index(command, "/") - if slashIdx == -1 { - return "", nil, false, nil - } - - // Extract from the slash onwards - command = command[slashIdx+1:] - - if command == "" { - return "", nil, false, nil - } - - // Find the task name (first word after the slash) - // Task name ends at first whitespace or newline - endIdx := strings.IndexAny(command, " \t\n\r") - if endIdx == -1 { - // No arguments, just the task name (rest of the string) - return command, make(map[string]string), true, nil - } - - taskName = command[:endIdx] - - // Extract arguments until end of line - restOfString := command[endIdx:] - newlineIdx := strings.IndexAny(restOfString, "\n\r") - var argsString string - if newlineIdx == -1 { - // No newline, use everything - argsString = strings.TrimSpace(restOfString) - } else { - // Only use up to the newline - argsString = strings.TrimSpace(restOfString[:newlineIdx]) - } - - params = make(map[string]string) - - // Store the full argument string (with quotes preserved) - if argsString != "" { - params["ARGUMENTS"] = argsString - } - - // If there are no arguments, return early - if argsString == "" { - return taskName, params, true, nil - } - - // Parse positional arguments using bash-like parsing - args, err := parseBashArgs(argsString) - if err != nil { - return "", nil, false, err - } - - // Add positional arguments as $1, $2, $3, etc. - for i, arg := range args { - params[fmt.Sprintf("%d", i+1)] = arg - } - - return taskName, params, true, nil -} - -// parseBashArgs parses a string into arguments like bash does, respecting quoted values -func parseBashArgs(s string) ([]string, error) { - var args []string - var current strings.Builder - inQuotes := false - quoteChar := byte(0) - escaped := false - justClosedQuotes := false - - for i := 0; i < len(s); i++ { - ch := s[i] - - if escaped { - current.WriteByte(ch) - escaped = false - continue - } - - if ch == '\\' && inQuotes && quoteChar == '"' { - // Only recognize escape in double quotes - escaped = true - continue - } - - if (ch == '"' || ch == '\'') && !inQuotes { - // Start of quoted string - inQuotes = true - quoteChar = ch - justClosedQuotes = false - } else if ch == quoteChar && inQuotes { - // End of quoted string - mark that we just closed quotes - inQuotes = false - quoteChar = 0 - justClosedQuotes = true - } else if (ch == ' ' || ch == '\t') && !inQuotes { - // Whitespace outside quotes - end of argument - if current.Len() > 0 || justClosedQuotes { - args = append(args, current.String()) - current.Reset() - justClosedQuotes = false - } - } else { - // Regular character - current.WriteByte(ch) - justClosedQuotes = false - } - } - - // Add the last argument - if current.Len() > 0 || justClosedQuotes { - args = append(args, current.String()) - } - - // Check for unclosed quotes - if inQuotes { - return nil, fmt.Errorf("unclosed quote in arguments") - } - - return args, nil -} diff --git a/pkg/slashcommand/parser_test.go b/pkg/slashcommand/parser_test.go deleted file mode 100644 index a877df78..00000000 --- a/pkg/slashcommand/parser_test.go +++ /dev/null @@ -1,338 +0,0 @@ -package slashcommand - -import ( - "reflect" - "testing" -) - -func TestParseSlashCommand(t *testing.T) { - tests := []struct { - name string - command string - wantFound bool - wantTask string - wantParams map[string]string - wantErr bool - errContains string - }{ - { - name: "simple command without parameters", - command: "/fix-bug", - wantFound: true, - wantTask: "fix-bug", - wantParams: map[string]string{}, - wantErr: false, - }, - { - name: "command with single unquoted argument", - command: "/fix-bug 123", - wantFound: true, - wantTask: "fix-bug", - wantParams: map[string]string{ - "ARGUMENTS": "123", - "1": "123", - }, - wantErr: false, - }, - { - name: "command with multiple unquoted arguments", - command: "/implement-feature login high urgent", - wantFound: true, - wantTask: "implement-feature", - wantParams: map[string]string{ - "ARGUMENTS": "login high urgent", - "1": "login", - "2": "high", - "3": "urgent", - }, - wantErr: false, - }, - { - name: "command with double-quoted argument containing spaces", - command: `/code-review "Fix authentication bug in login flow"`, - wantFound: true, - wantTask: "code-review", - wantParams: map[string]string{ - "ARGUMENTS": `"Fix authentication bug in login flow"`, - "1": "Fix authentication bug in login flow", - }, - wantErr: false, - }, - { - name: "command with single-quoted argument containing spaces", - command: `/code-review 'Fix authentication bug'`, - wantFound: true, - wantTask: "code-review", - wantParams: map[string]string{ - "ARGUMENTS": `'Fix authentication bug'`, - "1": "Fix authentication bug", - }, - wantErr: false, - }, - { - name: "command with mixed quoted and unquoted arguments", - command: `/deploy "staging server" v1.2.3 --force`, - wantFound: true, - wantTask: "deploy", - wantParams: map[string]string{ - "ARGUMENTS": `"staging server" v1.2.3 --force`, - "1": "staging server", - "2": "v1.2.3", - "3": "--force", - }, - wantErr: false, - }, - { - name: "command with extra whitespace", - command: ` /fix-bug 123 "high priority" `, - wantFound: true, - wantTask: "fix-bug", - wantParams: map[string]string{ - "ARGUMENTS": `123 "high priority"`, - "1": "123", - "2": "high priority", - }, - wantErr: false, - }, - { - name: "missing leading slash", - command: "fix-bug", - wantFound: false, - wantTask: "", - wantParams: nil, - wantErr: false, - }, - { - name: "empty command", - command: "/", - wantFound: false, - wantTask: "", - wantParams: nil, - wantErr: false, - }, - { - name: "empty string", - command: "", - wantFound: false, - wantTask: "", - wantParams: nil, - wantErr: false, - }, - { - name: "unclosed double quote", - command: `/fix-bug "unclosed`, - wantFound: false, - wantTask: "", - wantParams: nil, - wantErr: true, - errContains: "unclosed quote", - }, - { - name: "unclosed single quote", - command: `/fix-bug 'unclosed`, - wantFound: false, - wantTask: "", - wantParams: nil, - wantErr: true, - errContains: "unclosed quote", - }, - { - name: "task name with hyphens", - command: "/implement-new-feature", - wantFound: true, - wantTask: "implement-new-feature", - wantParams: map[string]string{}, - wantErr: false, - }, - { - name: "task name with underscores", - command: "/fix_critical_bug", - wantFound: true, - wantTask: "fix_critical_bug", - wantParams: map[string]string{}, - wantErr: false, - }, - { - name: "empty quoted argument", - command: `/fix-bug ""`, - wantFound: true, - wantTask: "fix-bug", - wantParams: map[string]string{ - "ARGUMENTS": `""`, - "1": "", - }, - wantErr: false, - }, - { - name: "argument with special characters", - command: `/deploy https://example.com/api/v1`, - wantFound: true, - wantTask: "deploy", - wantParams: map[string]string{ - "ARGUMENTS": "https://example.com/api/v1", - "1": "https://example.com/api/v1", - }, - wantErr: false, - }, - { - name: "argument with numbers", - command: `/review 12345`, - wantFound: true, - wantTask: "review", - wantParams: map[string]string{ - "ARGUMENTS": "12345", - "1": "12345", - }, - wantErr: false, - }, - { - name: "multiple arguments with various spacing", - command: `/task a b c`, - wantFound: true, - wantTask: "task", - wantParams: map[string]string{ - "ARGUMENTS": "a b c", - "1": "a", - "2": "b", - "3": "c", - }, - wantErr: false, - }, - { - name: "escaped quote in double quotes", - command: `/echo "He said \"hello\""`, - wantFound: true, - wantTask: "echo", - wantParams: map[string]string{ - "ARGUMENTS": `"He said \"hello\""`, - "1": `He said "hello"`, - }, - wantErr: false, - }, - { - name: "single quotes preserve everything", - command: `/echo 'He said "hello"'`, - wantFound: true, - wantTask: "echo", - wantParams: map[string]string{ - "ARGUMENTS": `'He said "hello"'`, - "1": `He said "hello"`, - }, - wantErr: false, - }, - // New test cases for finding slash commands anywhere in the string - { - name: "command in middle of string", - command: "Please /fix-bug 123 today", - wantFound: true, - wantTask: "fix-bug", - wantParams: map[string]string{ - "ARGUMENTS": "123 today", - "1": "123", - "2": "today", - }, - wantErr: false, - }, - { - name: "command with prefix text", - command: "Hey can you /deploy production", - wantFound: true, - wantTask: "deploy", - wantParams: map[string]string{ - "ARGUMENTS": "production", - "1": "production", - }, - wantErr: false, - }, - { - name: "command with text before and on same line", - command: "Some text /code-review \"PR #42\" high more text", - wantFound: true, - wantTask: "code-review", - wantParams: map[string]string{ - "ARGUMENTS": `"PR #42" high more text`, - "1": "PR #42", - "2": "high", - "3": "more", - "4": "text", - }, - wantErr: false, - }, - { - name: "command followed by newline", - command: "Text before /fix-bug 123\nText after on next line", - wantFound: true, - wantTask: "fix-bug", - wantParams: map[string]string{ - "ARGUMENTS": "123", - "1": "123", - }, - wantErr: false, - }, - { - name: "command in middle without arguments", - command: "Some prefix /task-name and suffix", - wantFound: true, - wantTask: "task-name", - wantParams: map[string]string{ - "ARGUMENTS": "and suffix", - "1": "and", - "2": "suffix", - }, - wantErr: false, - }, - { - name: "command at end without args", - command: "Please run /deploy", - wantFound: true, - wantTask: "deploy", - wantParams: map[string]string{}, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotTask, gotParams, gotFound, err := ParseSlashCommand(tt.command) - - if (err != nil) != tt.wantErr { - t.Errorf("ParseSlashCommand() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if tt.wantErr && tt.errContains != "" { - if err == nil || !contains(err.Error(), tt.errContains) { - t.Errorf("ParseSlashCommand() error = %v, want error containing %q", err, tt.errContains) - } - return - } - - if gotFound != tt.wantFound { - t.Errorf("ParseSlashCommand() gotFound = %v, want %v", gotFound, tt.wantFound) - } - - if gotTask != tt.wantTask { - t.Errorf("ParseSlashCommand() gotTask = %v, want %v", gotTask, tt.wantTask) - } - - if !reflect.DeepEqual(gotParams, tt.wantParams) { - t.Errorf("ParseSlashCommand() gotParams = %v, want %v", gotParams, tt.wantParams) - } - }) - } -} - -// contains is a helper function to check if a string contains a substring -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || len(substr) == 0 || - (len(s) > 0 && len(substr) > 0 && stringContains(s, substr))) -} - -func stringContains(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -}