Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions pkg/cli/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

// InspectWorkflowMCP inspects MCP servers used by a workflow and lists available tools, resources, and roots
func InspectWorkflowMCP(workflowFile string, serverFilter string, toolFilter string, verbose bool) error {
func InspectWorkflowMCP(workflowFile string, serverFilter string, toolFilter string, beforeTime string, afterTime string, verbose bool) error {
workflowsDir := getWorkflowsDir()

// If no workflow file specified, show available workflow files with MCP configs
Expand Down Expand Up @@ -45,6 +45,19 @@ func InspectWorkflowMCP(workflowFile string, serverFilter string, toolFilter str

if verbose {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Inspecting MCP servers in: %s", workflowPath)))

// Show time filtering information if provided
if beforeTime != "" || afterTime != "" {
fmt.Println(console.FormatInfoMessage("Time filtering enabled:"))
if afterTime != "" {
resolvedAfter, _ := workflow.ResolveRelativeTime(afterTime, time.Now())
fmt.Printf(" • After: %s (resolved to %s)\n", afterTime, resolvedAfter.Format("2006-01-02 15:04:05 UTC"))
}
if beforeTime != "" {
resolvedBefore, _ := workflow.ResolveRelativeTime(beforeTime, time.Now())
fmt.Printf(" • Before: %s (resolved to %s)\n", beforeTime, resolvedBefore.Format("2006-01-02 15:04:05 UTC"))
}
}
}

// Parse the workflow file
Expand Down Expand Up @@ -194,6 +207,8 @@ func NewInspectCommand() *cobra.Command {
var serverFilter string
var toolFilter string
var spawnInspector bool
var beforeTime string
var afterTime string

cmd := &cobra.Command{
Use: "inspect [workflow-file]",
Expand All @@ -203,11 +218,17 @@ func NewInspectCommand() *cobra.Command {
This command starts each MCP server configured in the workflow, queries its capabilities,
and displays the results in a formatted table. It supports stdio, Docker, and HTTP MCP servers.

Time filtering allows you to filter results based on workflow run history using absolute dates
or relative time expressions like "-24h" (24 hours ago) or "-3d" (3 days ago).

Examples:
gh aw inspect # List workflows with MCP servers
gh aw inspect weekly-research # Inspect MCP servers in weekly-research.md
gh aw inspect repomind --server repo-mind # Inspect only the repo-mind server
gh aw inspect weekly-research --server github --tool create_issue # Show details for a specific tool
gh aw inspect weekly-research --after "-24h" # Filter results after 24 hours ago
gh aw inspect weekly-research --before "2024-01-01" # Filter results before specific date
gh aw inspect weekly-research --after "-3d" --before "-1d" # Filter results between 3 and 1 days ago
gh aw inspect weekly-research -v # Verbose output with detailed connection info
gh aw inspect weekly-research --inspector # Launch @modelcontextprotocol/inspector

Expand Down Expand Up @@ -235,17 +256,31 @@ The command will:
return fmt.Errorf("--tool flag requires --server flag to be specified")
}

// Validate time filter formats if provided
if beforeTime != "" {
if _, err := workflow.ResolveRelativeTime(beforeTime, time.Now()); err != nil {
return fmt.Errorf("invalid --before time format: %w", err)
}
}
if afterTime != "" {
if _, err := workflow.ResolveRelativeTime(afterTime, time.Now()); err != nil {
return fmt.Errorf("invalid --after time format: %w", err)
}
}

// Handle spawn inspector flag
if spawnInspector {
return spawnMCPInspector(workflowFile, serverFilter, verbose)
}

return InspectWorkflowMCP(workflowFile, serverFilter, toolFilter, verbose)
return InspectWorkflowMCP(workflowFile, serverFilter, toolFilter, beforeTime, afterTime, verbose)
},
}

cmd.Flags().StringVar(&serverFilter, "server", "", "Filter to inspect only the specified MCP server")
cmd.Flags().StringVar(&toolFilter, "tool", "", "Show detailed information about a specific tool (requires --server)")
cmd.Flags().StringVar(&beforeTime, "before", "", "Filter results before this time (supports absolute dates and relative times like '-24h', '-3d')")
cmd.Flags().StringVar(&afterTime, "after", "", "Filter results after this time (supports absolute dates and relative times like '-24h', '-3d')")
cmd.Flags().BoolP("verbose", "v", false, "Enable verbose output with detailed connection information")
cmd.Flags().BoolVar(&spawnInspector, "inspector", false, "Launch the official @modelcontextprotocol/inspector tool")

Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ This workflow has no MCP servers.`,

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := InspectWorkflowMCP(tt.workflowFile, tt.serverFilter, "", false)
err := InspectWorkflowMCP(tt.workflowFile, tt.serverFilter, "", "", "", false)

if tt.expectError && err == nil {
t.Errorf("Expected error but got none")
Expand Down Expand Up @@ -179,7 +179,7 @@ func TestInspectWorkflowMCPWithToolFilter(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := InspectWorkflowMCP(tt.workflowFile, tt.serverFilter, tt.toolFilter, false)
err := InspectWorkflowMCP(tt.workflowFile, tt.serverFilter, tt.toolFilter, "", "", false)

if tt.expectError && err == nil {
t.Errorf("Expected error but got none")
Expand Down
102 changes: 80 additions & 22 deletions pkg/workflow/time_delta.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,42 @@ type TimeDelta struct {
Hours int
Days int
Minutes int
Sign int // 1 for positive (future), -1 for negative (past)
}

// parseTimeDelta parses a relative time delta string like "+25h", "+3d", "+1d12h30m", etc.
// parseTimeDelta parses a relative time delta string like "+25h", "-24h", "+3d", "-1d12h30m", etc.
// Supported formats:
// - +25h (25 hours)
// - +3d (3 days)
// - +30m (30 minutes)
// - +1d12h (1 day and 12 hours)
// - +2d5h30m (2 days, 5 hours, 30 minutes)
// - +25h (25 hours in the future)
// - -24h (24 hours in the past)
// - +3d (3 days in the future)
// - -3d (3 days in the past)
// - +30m (30 minutes in the future)
// - -30m (30 minutes in the past)
// - +1d12h (1 day and 12 hours in the future)
// - -1d12h30m (1 day, 12 hours, 30 minutes in the past)
func parseTimeDelta(deltaStr string) (*TimeDelta, error) {
if deltaStr == "" {
return nil, fmt.Errorf("empty time delta")
}

// Must start with '+'
if !strings.HasPrefix(deltaStr, "+") {
return nil, fmt.Errorf("time delta must start with '+', got: %s", deltaStr)
var sign int
var prefix string

// Determine sign and remove prefix
if strings.HasPrefix(deltaStr, "+") {
sign = 1
prefix = "+"
deltaStr = deltaStr[1:]
} else if strings.HasPrefix(deltaStr, "-") {
sign = -1
prefix = "-"
deltaStr = deltaStr[1:]
} else {
return nil, fmt.Errorf("time delta must start with '+' or '-', got: %s", deltaStr)
}

// Remove the '+' prefix
deltaStr = deltaStr[1:]

if deltaStr == "" {
return nil, fmt.Errorf("empty time delta after '+'")
return nil, fmt.Errorf("empty time delta after '%s'", prefix)
}

// Parse components using regex
Expand All @@ -45,7 +57,7 @@ func parseTimeDelta(deltaStr string) (*TimeDelta, error) {
matches := pattern.FindAllStringSubmatch(deltaStr, -1)

if len(matches) == 0 {
return nil, fmt.Errorf("invalid time delta format: +%s. Expected format like +25h, +3d, +1d12h30m", deltaStr)
return nil, fmt.Errorf("invalid time delta format: %s%s. Expected format like %s25h, %s3d, %s1d12h30m", prefix, deltaStr, prefix, prefix, prefix)
}

// Check that all characters are consumed by matches
Expand All @@ -54,10 +66,10 @@ func parseTimeDelta(deltaStr string) (*TimeDelta, error) {
consumed += len(match[0])
}
if consumed != len(deltaStr) {
return nil, fmt.Errorf("invalid time delta format: +%s. Extra characters detected", deltaStr)
return nil, fmt.Errorf("invalid time delta format: %s%s. Extra characters detected", prefix, deltaStr)
}

delta := &TimeDelta{}
delta := &TimeDelta{Sign: sign}
seenUnits := make(map[string]bool)

for _, match := range matches {
Expand All @@ -70,17 +82,17 @@ func parseTimeDelta(deltaStr string) (*TimeDelta, error) {

// Check for duplicate units
if seenUnits[unit] {
return nil, fmt.Errorf("duplicate unit '%s' in time delta: +%s", unit, deltaStr)
return nil, fmt.Errorf("duplicate unit '%s' in time delta: %s%s", unit, prefix, deltaStr)
}
seenUnits[unit] = true

value, err := strconv.Atoi(valueStr)
if err != nil {
return nil, fmt.Errorf("invalid number '%s' in time delta: +%s", valueStr, deltaStr)
return nil, fmt.Errorf("invalid number '%s' in time delta: %s%s", valueStr, prefix, deltaStr)
}

if value < 0 {
return nil, fmt.Errorf("negative values not allowed in time delta: +%s", deltaStr)
return nil, fmt.Errorf("negative values not allowed in time delta: %s%s", prefix, deltaStr)
}

switch unit {
Expand All @@ -91,7 +103,7 @@ func parseTimeDelta(deltaStr string) (*TimeDelta, error) {
case "m":
delta.Minutes = value
default:
return nil, fmt.Errorf("unsupported time unit '%s' in time delta: +%s", unit, deltaStr)
return nil, fmt.Errorf("unsupported time unit '%s' in time delta: %s%s", unit, prefix, deltaStr)
}
}

Expand All @@ -114,7 +126,7 @@ func (td *TimeDelta) toDuration() time.Duration {
duration := time.Duration(td.Days) * 24 * time.Hour
duration += time.Duration(td.Hours) * time.Hour
duration += time.Duration(td.Minutes) * time.Minute
return duration
return time.Duration(td.Sign) * duration
}

// String returns a human-readable representation of the TimeDelta
Expand All @@ -132,14 +144,24 @@ func (td *TimeDelta) String() string {
if len(parts) == 0 {
return "0m"
}
return "+" + strings.Join(parts, "")

prefix := "+"
if td.Sign < 0 {
prefix = "-"
}
return prefix + strings.Join(parts, "")
}

// isRelativeStopTime checks if a stop-time value is a relative time delta
func isRelativeStopTime(stopTime string) bool {
return strings.HasPrefix(stopTime, "+")
}

// isRelativeTime checks if a time value is a relative time delta (supports both + and -)
func isRelativeTime(timeStr string) bool {
return strings.HasPrefix(timeStr, "+") || strings.HasPrefix(timeStr, "-")
}

// parseAbsoluteDateTime parses various date-time formats and returns a standardized timestamp
func parseAbsoluteDateTime(dateTimeStr string) (string, error) {
// Try multiple date-time formats in order of preference
Expand Down Expand Up @@ -263,3 +285,39 @@ func resolveStopTime(stopTime string, compilationTime time.Time) (string, error)
// Parse absolute date-time with flexible format support
return parseAbsoluteDateTime(stopTime)
}

// ResolveRelativeTime resolves a relative or absolute time value to an absolute timestamp
// If the time is relative (starts with '+' or '-'), it calculates the absolute time
// from the reference time. Otherwise, it parses the absolute time using various formats.
// Returns the time as a time.Time object instead of a string.
func ResolveRelativeTime(timeStr string, referenceTime time.Time) (time.Time, error) {
if timeStr == "" {
return time.Time{}, fmt.Errorf("empty time value")
}

if isRelativeTime(timeStr) {
// Parse the relative time delta
delta, err := parseTimeDelta(timeStr)
if err != nil {
return time.Time{}, err
}

// Calculate absolute time in UTC
absoluteTime := referenceTime.UTC().Add(delta.toDuration())
return absoluteTime, nil
}

// Parse absolute date-time with flexible format support
absoluteTimeStr, err := parseAbsoluteDateTime(timeStr)
if err != nil {
return time.Time{}, err
}

// Convert back to time.Time
parsedTime, err := time.Parse("2006-01-02 15:04:05", absoluteTimeStr)
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse resolved time: %w", err)
}

return parsedTime.UTC(), nil
}
Loading