Commit d77e9da

bryfry <bryon@fryer.io>
2025-09-16 14:44:23
refactor
1 parent 5197804
cmd/callbacks.go
@@ -23,7 +23,7 @@ func init() {
 }
 
 func runCallbacks(cmd *cobra.Command, args []string) error {
-	if err := validateAuth(); err != nil {
+	if err := validateConfig(); err != nil {
 		return err
 	}
 
cmd/exec.go
@@ -1,103 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"fmt"
-	"go-mythic/pkg/mythic"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/spf13/cobra"
-)
-
-var execCmd = &cobra.Command{
-	Use:   "exec <callback_id> <command> [args...]",
-	Short: "Execute a command on a specific callback",
-	Long:  "Execute a command on the specified callback and wait for the response. Supports commands like help, ps, sleep, etc.",
-	Args:  cobra.MinimumNArgs(2),
-	RunE:  runExec,
-}
-
-var (
-	waitTime int
-)
-
-func init() {
-	rootCmd.AddCommand(execCmd)
-	execCmd.Flags().IntVarP(&waitTime, "wait", "w", 30, "Maximum time to wait for response (seconds)")
-}
-
-func runExec(cmd *cobra.Command, args []string) error {
-	if err := validateAuth(); err != nil {
-		return err
-	}
-
-	callbackID, err := strconv.Atoi(args[0])
-	if err != nil {
-		return fmt.Errorf("invalid callback ID: %s", args[0])
-	}
-
-	command := args[1]
-	params := ""
-	if len(args) > 2 {
-		params = strings.Join(args[2:], " ")
-	}
-
-	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
-	ctx := context.Background()
-
-	// Verify callback exists and is active
-	callbacks, err := client.GetActiveCallbacks(ctx)
-	if err != nil {
-		return fmt.Errorf("failed to get callbacks: %w", err)
-	}
-
-	var targetCallback *mythic.Callback
-	for _, callback := range callbacks {
-		if callback.DisplayID == callbackID {
-			targetCallback = &callback
-			break
-		}
-	}
-
-	if targetCallback == nil {
-		return fmt.Errorf("callback %d not found or not active", callbackID)
-	}
-
-	fmt.Printf("Executing '%s %s' on callback %d (%s@%s)\n", 
-		command, params, callbackID, targetCallback.User, targetCallback.Host)
-
-	// Create the task
-	task, err := client.CreateTask(ctx, targetCallback.ID, command, params)
-	if err != nil {
-		return fmt.Errorf("failed to create task: %w", err)
-	}
-
-	fmt.Printf("Task %d created, waiting for response...\n", task.DisplayID)
-
-	// Poll for response
-	for i := 0; i < waitTime; i++ {
-		time.Sleep(1 * time.Second)
-		
-		updatedTask, err := client.GetTaskResponse(ctx, task.ID)
-		if err != nil {
-			return fmt.Errorf("failed to get task response: %w", err)
-		}
-
-		if updatedTask.Status == "completed" || updatedTask.Status == "error" {
-			fmt.Printf("\nTask Status: %s\n", updatedTask.Status)
-			if updatedTask.Response != "" {
-				fmt.Printf("Response:\n%s\n", updatedTask.Response)
-			}
-			return nil
-		}
-
-		if i > 0 && i%5 == 0 {
-			fmt.Printf(".")
-		}
-	}
-
-	fmt.Printf("\nTimeout waiting for response after %d seconds\n", waitTime)
-	return nil
-}
\ No newline at end of file
cmd/files.go
@@ -0,0 +1,333 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"go-mythic/pkg/mythic/api"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	outputDir    string
+	showPayloads bool
+	showDownloads bool
+	showScreenshots bool
+)
+
+var filesCmd = &cobra.Command{
+	Use:     "files",
+	Aliases: []string{"file"},
+	Short:   "Manage files",
+	Long:    "List and download files (payloads, downloads, screenshots) from Mythic. Defaults to listing all files if no subcommand is provided.",
+	RunE:    runFilesList, // Default to list command
+}
+
+
+var filesDownloadCmd = &cobra.Command{
+	Use:   "download <file_uuid_or_task_id>",
+	Short: "Download a file by UUID or task ID",
+	Long:  "Download a file from Mythic using either its UUID or the task ID that downloaded the file. When using task ID, the UUID will be extracted from the task response.",
+	Args:  cobra.ExactArgs(1),
+	RunE:  runFilesDownload,
+}
+
+func init() {
+	rootCmd.AddCommand(filesCmd)
+	filesCmd.AddCommand(filesDownloadCmd)
+
+	// Add filter flags to files command
+	filesCmd.Flags().BoolVarP(&showPayloads, "payload", "p", false, "Show only payload files")
+	filesCmd.Flags().BoolVarP(&showDownloads, "downloads", "d", false, "Show only downloaded files")
+	filesCmd.Flags().BoolVarP(&showScreenshots, "screenshots", "s", false, "Show only screenshot files")
+
+	filesDownloadCmd.Flags().StringVarP(&outputDir, "output", "o", ".", "Output directory for downloaded files")
+}
+
+func runFilesList(cmd *cobra.Command, args []string) error {
+	if err := validateConfig(); err != nil {
+		return err
+	}
+
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	files, err := client.GetAllFiles(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get files: %w", err)
+	}
+
+	// Apply filters if any are specified
+	if showPayloads || showDownloads || showScreenshots {
+		var filteredFiles []mythic.File
+		for _, file := range files {
+			if (showPayloads && file.IsPayload) ||
+				(showDownloads && file.IsDownloadFromAgent && !file.IsPayload && !file.IsScreenshot) ||
+				(showScreenshots && file.IsScreenshot) {
+				filteredFiles = append(filteredFiles, file)
+			}
+		}
+		files = filteredFiles
+	}
+
+	if len(files) == 0 {
+		fmt.Println("No files found")
+		return nil
+	}
+
+	// Determine filter description
+	filterDesc := ""
+	if showPayloads || showDownloads || showScreenshots {
+		var filters []string
+		if showPayloads {
+			filters = append(filters, "payloads")
+		}
+		if showDownloads {
+			filters = append(filters, "downloads")
+		}
+		if showScreenshots {
+			filters = append(filters, "screenshots")
+		}
+		filterDesc = fmt.Sprintf(" (%s)", strings.Join(filters, ", "))
+	}
+
+	fmt.Printf("Files%s (%d total):\n\n", filterDesc, len(files))
+
+	// Create a tab writer for formatted output
+	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+	fmt.Fprintln(w, "UUID\tTYPE\tFILENAME\tREMOTE PATH\tHOST\tSIZE\tCOMPLETE\tTASK\tTIMESTAMP")
+	fmt.Fprintln(w, "----\t----\t--------\t-----------\t----\t----\t--------\t----\t---------")
+
+	for _, file := range files {
+		// Show completion status
+		status := "✓"
+		if !file.Complete {
+			status = fmt.Sprintf("%d/%d", file.ChunksReceived, file.TotalChunks)
+		}
+
+		// Format timestamp
+		timestamp := file.Timestamp
+		if timestamp != "" {
+			if t, err := time.Parse(time.RFC3339, timestamp); err == nil {
+				timestamp = t.Format("2006-01-02 15:04")
+			}
+		}
+
+		// Truncate paths if too long
+		filename := file.Filename
+		if len(filename) > 25 {
+			filename = filename[:22] + "..."
+		}
+
+		remotePath := file.FullRemotePath
+		if len(remotePath) > 40 {
+			remotePath = "..." + remotePath[len(remotePath)-37:]
+		}
+
+		// Show file size info (chunks can give us a rough idea)
+		sizeInfo := "unknown"
+		if file.TotalChunks > 0 {
+			sizeInfo = fmt.Sprintf("~%dKB", file.TotalChunks*api.EstimatedChunkSizeKB)
+		}
+
+		// Determine file type
+		fileType := "download"
+		if file.IsPayload {
+			fileType = "payload"
+		} else if file.IsScreenshot {
+			fileType = "screenshot"
+		}
+
+		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s\n",
+			file.AgentFileID[:api.UUIDDisplayLength]+"...", // Show first 8 chars of UUID
+			fileType,
+			filename,
+			remotePath,
+			file.CallbackHost,
+			sizeInfo,
+			status,
+			file.TaskDisplayID,
+			timestamp,
+		)
+	}
+
+	w.Flush()
+
+	fmt.Println("\nTo download a file: go-mythic files download <uuid>")
+	fmt.Println("To view full UUIDs: go-mythic files | grep -v UUID")
+
+	return nil
+}
+
+func runFilesDownload(cmd *cobra.Command, args []string) error {
+	if err := validateConfig(); err != nil {
+		return err
+	}
+
+	input := args[0]
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Longer timeout for downloads
+	defer cancel()
+
+	var fileUUID string
+
+	// Determine if input is a UUID (36 chars with hyphens) or a task ID (numeric)
+	if len(input) == api.UUIDLength && regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`).MatchString(input) {
+		// Input looks like a UUID
+		fileUUID = input
+		fmt.Printf("Using provided UUID: %s\n", fileUUID)
+	} else if taskID, err := strconv.Atoi(input); err == nil {
+		// Input is numeric, treat as task ID
+		fmt.Printf("Looking up task %d for file UUID...\n", taskID)
+
+		// Get the task response
+		task, err := client.GetTaskResponse(ctx, taskID)
+		if err != nil {
+			return fmt.Errorf("failed to get task %d: %w", taskID, err)
+		}
+
+		// Extract UUIDs from the response
+		uuids := extractUUIDFromTaskResponse(task.Response)
+		if len(uuids) == 0 {
+			return fmt.Errorf("no file UUIDs found in task %d response. Raw response:\n%s", taskID, task.Response)
+		}
+
+		if len(uuids) > 1 {
+			fmt.Printf("Found multiple UUIDs in task response:\n")
+			for i, uuid := range uuids {
+				fmt.Printf("  %d: %s\n", i+1, uuid)
+			}
+			fmt.Printf("Using first UUID: %s\n", uuids[0])
+		}
+
+		fileUUID = uuids[0]
+		fmt.Printf("Extracted UUID: %s\n", fileUUID)
+	} else {
+		return fmt.Errorf("input '%s' is neither a valid UUID nor a numeric task ID", input)
+	}
+
+	// Get file metadata to get the original filename
+	files, err := client.GetAllFiles(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get file metadata: %w", err)
+	}
+
+	var targetFile *mythic.File
+	for _, file := range files {
+		if file.AgentFileID == fileUUID {
+			targetFile = &file
+			break
+		}
+	}
+
+	if targetFile == nil {
+		return fmt.Errorf("file with UUID %s not found", fileUUID)
+	}
+
+	if !targetFile.Complete {
+		fmt.Printf("Warning: File is not fully downloaded (%d/%d chunks)\n",
+			targetFile.ChunksReceived, targetFile.TotalChunks)
+	}
+
+	fmt.Printf("Downloading: %s\n", targetFile.Filename)
+	fmt.Printf("From: %s (%s@%s)\n", targetFile.FullRemotePath, targetFile.CallbackUser, targetFile.CallbackHost)
+
+	// Download the file
+	data, err := client.DownloadFile(ctx, fileUUID)
+	if err != nil {
+		return fmt.Errorf("failed to download file: %w", err)
+	}
+
+	// Determine output filename
+	outputFilename := targetFile.Filename
+	if outputFilename == "" {
+		outputFilename = fileUUID + ".bin"
+	}
+
+	outputPath := filepath.Join(outputDir, outputFilename)
+
+	// Handle file conflicts
+	counter := 1
+	originalPath := outputPath
+	for {
+		if _, err := os.Stat(outputPath); os.IsNotExist(err) {
+			break
+		}
+		ext := filepath.Ext(originalPath)
+		base := originalPath[:len(originalPath)-len(ext)]
+		outputPath = fmt.Sprintf("%s_%d%s", base, counter, ext)
+		counter++
+	}
+
+	// Write file
+	err = os.WriteFile(outputPath, data, 0644)
+	if err != nil {
+		return fmt.Errorf("failed to write file: %w", err)
+	}
+
+	fmt.Printf("Downloaded %d bytes to: %s\n", len(data), outputPath)
+
+	if targetFile.MD5 != "" {
+		fmt.Printf("MD5: %s\n", targetFile.MD5)
+	}
+	if targetFile.SHA1 != "" {
+		fmt.Printf("SHA1: %s\n", targetFile.SHA1)
+	}
+
+	return nil
+}
+
+// extractUUIDFromTaskResponse attempts to extract file UUIDs from task response text
+func extractUUIDFromTaskResponse(response string) []string {
+	// Common UUID patterns in Mythic responses
+	patterns := []string{
+		// Standard UUID format
+		`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`,
+		// Sometimes UUIDs appear without hyphens
+		`[0-9a-f]{32}`,
+		// Look for specific download-related patterns
+		`(?i)(?:file\s+(?:uuid|id|identifier):\s*)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`,
+		`(?i)(?:downloaded.*?file.*?)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`,
+		`(?i)(?:uuid:\s*)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`,
+	}
+
+	var uuids []string
+	seenUUIDs := make(map[string]bool)
+
+	for _, pattern := range patterns {
+		re := regexp.MustCompile(pattern)
+		matches := re.FindAllStringSubmatch(response, -1)
+
+		for _, match := range matches {
+			var uuid string
+			if len(match) > 1 {
+				uuid = match[1] // Captured group
+			} else {
+				uuid = match[0] // Full match
+			}
+
+			// Normalize UUID format (add hyphens if missing)
+			if len(uuid) == api.UUIDLengthNoHyphens && !regexp.MustCompile(`-`).MatchString(uuid) {
+				uuid = fmt.Sprintf("%s-%s-%s-%s-%s",
+					uuid[0:8], uuid[8:12], uuid[12:16], uuid[16:20], uuid[20:32])
+			}
+
+			// Only add if it's a valid UUID format and we haven't seen it before
+			if len(uuid) == api.UUIDLength && !seenUUIDs[uuid] {
+				uuids = append(uuids, uuid)
+				seenUUIDs[uuid] = true
+			}
+		}
+	}
+
+	return uuids
+}
\ No newline at end of file
cmd/grep.go
@@ -0,0 +1,266 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"os"
+	"regexp"
+	"strings"
+	"sync"
+	"text/tabwriter"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	grepCallbackID      int
+	grepCaseInsensitive bool
+	grepLimit           int
+	grepRegex           bool
+	grepInvert          bool
+	grepWorkers         int
+)
+
+var grepCmd = &cobra.Command{
+	Use:   "grep <pattern>",
+	Short: "Search through task outputs",
+	Long: `Search through task response outputs for a specific pattern.
+Shows a table of matching tasks with their commands instead of full output.
+This is like grep for Mythic task results - find specific strings, commands, or data across all your task history.`,
+	Args: cobra.ExactArgs(1),
+	RunE: runGrep,
+}
+
+func init() {
+	rootCmd.AddCommand(grepCmd)
+	grepCmd.Flags().IntVarP(&grepCallbackID, "callback", "c", 0, "Search only tasks from specific callback ID")
+	grepCmd.Flags().BoolVarP(&grepCaseInsensitive, "ignore-case", "i", false, "Case insensitive search")
+	grepCmd.Flags().IntVarP(&grepLimit, "limit", "l", 0, "Maximum number of tasks to search (0 = no limit)")
+	grepCmd.Flags().BoolVarP(&grepRegex, "regexp", "E", false, "Treat pattern as regular expression")
+	grepCmd.Flags().BoolVarP(&grepInvert, "invert-match", "v", false, "Show tasks that don't match")
+	grepCmd.Flags().IntVarP(&grepWorkers, "workers", "w", 10, "Number of parallel workers for searching")
+}
+
+type taskResult struct {
+	task  mythic.Task
+	match bool
+	err   error
+}
+
+func runGrep(cmd *cobra.Command, args []string) error {
+	if err := validateConfig(); err != nil {
+		return err
+	}
+
+	pattern := args[0]
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+
+	// Use shorter timeout for getting task lists
+	listCtx, listCancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer listCancel()
+
+	var tasks []mythic.Task
+	var err error
+
+	if grepCallbackID > 0 {
+		// Search tasks for specific callback
+		tasks, err = client.GetCallbackTasks(listCtx, grepCallbackID)
+		if err != nil {
+			return fmt.Errorf("failed to get tasks for callback %d: %w", grepCallbackID, err)
+		}
+		fmt.Printf("Searching tasks from callback %d for pattern: %s\n", grepCallbackID, pattern)
+	} else {
+		// Get all tasks from all callbacks
+		callbacks, err := client.GetActiveCallbacks(listCtx)
+		if err != nil {
+			return fmt.Errorf("failed to get callbacks: %w", err)
+		}
+
+		fmt.Printf("Searching all tasks for pattern: %s\n", pattern)
+
+		for _, callback := range callbacks {
+			callbackTasks, err := client.GetCallbackTasks(listCtx, callback.ID)
+			if err != nil {
+				fmt.Printf("Warning: Failed to get tasks for callback %d: %v\n", callback.DisplayID, err)
+				continue
+			}
+			tasks = append(tasks, callbackTasks...)
+		}
+	}
+
+	if len(tasks) == 0 {
+		fmt.Println("No tasks found to search")
+		return nil
+	}
+
+	// Apply limit if specified
+	if grepLimit > 0 && len(tasks) > grepLimit {
+		// Take the most recent tasks
+		tasks = tasks[len(tasks)-grepLimit:]
+	}
+
+	fmt.Printf("Searching through %d tasks with %d workers...\n\n", len(tasks), grepWorkers)
+
+	// Prepare pattern for matching
+	var matcher func(string) bool
+	if grepRegex {
+		regexPattern := pattern
+		if grepCaseInsensitive {
+			regexPattern = "(?i)" + pattern
+		}
+		re, err := regexp.Compile(regexPattern)
+		if err != nil {
+			return fmt.Errorf("invalid regular expression: %w", err)
+		}
+		matcher = re.MatchString
+	} else {
+		searchPattern := pattern
+		if grepCaseInsensitive {
+			searchPattern = strings.ToLower(pattern)
+		}
+		matcher = func(text string) bool {
+			searchText := text
+			if grepCaseInsensitive {
+				searchText = strings.ToLower(text)
+			}
+			return strings.Contains(searchText, searchPattern)
+		}
+	}
+
+	// Use worker pool to process tasks in parallel
+	results := processTasksWithWorkerPool(client, tasks, matcher, grepWorkers)
+
+	var matchingTasks []mythic.Task
+	processedTasks := 0
+	errorCount := 0
+
+	for result := range results {
+		processedTasks++
+		if result.err != nil {
+			errorCount++
+			if errorCount <= 5 { // Only show first 5 errors
+				fmt.Printf("Warning: Failed to get response for task %d: %v\n", result.task.DisplayID, result.err)
+			}
+			continue
+		}
+
+		// Apply invert logic
+		matches := result.match
+		if grepInvert {
+			matches = !matches
+		}
+
+		if matches {
+			matchingTasks = append(matchingTasks, result.task)
+		}
+	}
+
+	if errorCount > 5 {
+		fmt.Printf("Warning: Suppressed %d additional errors\n", errorCount-5)
+	}
+
+	if len(matchingTasks) == 0 {
+		fmt.Printf("No matches found in %d tasks searched\n", processedTasks)
+		return nil
+	}
+
+	// Show results in table format
+	showMatchingTasksTable(matchingTasks)
+
+	fmt.Printf("\nFound %d matches in %d tasks searched", len(matchingTasks), processedTasks)
+	if errorCount > 0 {
+		fmt.Printf(" (%d errors)", errorCount)
+	}
+	fmt.Println()
+	return nil
+}
+
+func processTasksWithWorkerPool(client *mythic.Client, tasks []mythic.Task, matcher func(string) bool, numWorkers int) <-chan taskResult {
+	taskChan := make(chan mythic.Task, len(tasks))
+	resultChan := make(chan taskResult, len(tasks))
+
+	// Start workers
+	var wg sync.WaitGroup
+	for i := 0; i < numWorkers; i++ {
+		wg.Add(1)
+		go worker(&wg, client, taskChan, resultChan, matcher)
+	}
+
+	// Send tasks to workers
+	go func() {
+		for _, task := range tasks {
+			taskChan <- task
+		}
+		close(taskChan)
+	}()
+
+	// Close results channel when all workers are done
+	go func() {
+		wg.Wait()
+		close(resultChan)
+	}()
+
+	return resultChan
+}
+
+func worker(wg *sync.WaitGroup, client *mythic.Client, taskChan <-chan mythic.Task, resultChan chan<- taskResult, matcher func(string) bool) {
+	defer wg.Done()
+
+	for task := range taskChan {
+		// Use per-request timeout to avoid global timeouts
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
+		fullTask, err := client.GetTaskResponse(ctx, task.ID)
+		cancel()
+
+		if err != nil {
+			resultChan <- taskResult{task: task, match: false, err: err}
+			continue
+		}
+
+		if fullTask.Response == "" {
+			resultChan <- taskResult{task: task, match: false, err: nil}
+			continue
+		}
+
+		// Check if response matches pattern
+		matches := matcher(fullTask.Response)
+		resultChan <- taskResult{task: *fullTask, match: matches, err: nil}
+	}
+}
+
+func showMatchingTasksTable(tasks []mythic.Task) {
+	// Create a tab writer for formatted output
+	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+	fmt.Fprintln(w, "TASK ID\tCOMMAND\tPARAMS\tSTATUS\tCALLBACK\tTIMESTAMP")
+	fmt.Fprintln(w, "-------\t-------\t------\t------\t--------\t---------")
+
+	for _, task := range tasks {
+		// Truncate params if too long
+		params := task.Params
+		if len(params) > 50 {
+			params = params[:47] + "..."
+		}
+
+		// Format timestamp
+		timestamp := task.Timestamp
+		if timestamp != "" {
+			if t, err := time.Parse(time.RFC3339, timestamp); err == nil {
+				timestamp = t.Format("2006-01-02 15:04")
+			}
+		}
+
+		fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%d\t%s\n",
+			task.DisplayID,
+			task.Command,
+			params,
+			task.Status,
+			task.CallbackID,
+			timestamp,
+		)
+	}
+
+	w.Flush()
+}
\ No newline at end of file
cmd/interact.go
@@ -5,10 +5,10 @@ import (
 	"context"
 	"fmt"
 	"go-mythic/pkg/mythic"
+	"go-mythic/pkg/mythic/api"
 	"os"
 	"strconv"
 	"strings"
-	"time"
 
 	"github.com/spf13/cobra"
 )
@@ -26,7 +26,7 @@ func init() {
 }
 
 func runInteract(cmd *cobra.Command, args []string) error {
-	if err := validateAuth(); err != nil {
+	if err := validateConfig(); err != nil {
 		return err
 	}
 
@@ -39,21 +39,9 @@ func runInteract(cmd *cobra.Command, args []string) error {
 	ctx := context.Background()
 
 	// Verify callback exists and is active
-	callbacks, err := client.GetActiveCallbacks(ctx)
+	targetCallback, err := api.FindActiveCallback(ctx, client, callbackID)
 	if err != nil {
-		return fmt.Errorf("failed to get callbacks: %w", err)
-	}
-
-	var targetCallback *mythic.Callback
-	for _, callback := range callbacks {
-		if callback.DisplayID == callbackID {
-			targetCallback = &callback
-			break
-		}
-	}
-
-	if targetCallback == nil {
-		return fmt.Errorf("callback %d not found or not active", callbackID)
+		return err
 	}
 
 	agentType := targetCallback.Payload.PayloadType.Name
@@ -107,39 +95,25 @@ func executeCommand(ctx context.Context, client *mythic.Client, callbackID int,
 		params = strings.Join(parts[1:], " ")
 	}
 
-	// Create the task
-	task, err := client.CreateTask(ctx, callbackID, command, params)
-	if err != nil {
-		return fmt.Errorf("failed to create task: %w", err)
+	// Configure polling for interactive mode
+	config := api.TaskPollConfig{
+		TimeoutSeconds: int(api.DefaultTaskTimeout.Seconds()),
+		PollInterval:   api.DefaultPollInterval,
+		ShowProgress:   true,
+		RawOutput:      false,
 	}
 
-	fmt.Printf("Task %d created\n", task.DisplayID)
-
-	// Poll for response
-	for i := 0; i < 30; i++ { // Poll for up to 30 seconds
-		time.Sleep(1 * time.Second)
-		
-		updatedTask, err := client.GetTaskResponse(ctx, task.ID)
-		if err != nil {
-			return fmt.Errorf("failed to get task response: %w", err)
-		}
-
-		if updatedTask.Status == "completed" || updatedTask.Status == "error" {
-			if updatedTask.Response != "" {
-				fmt.Printf("Response:\n%s\n", updatedTask.Response)
-			} else {
-				fmt.Printf("Task completed with status: %s\n", updatedTask.Status)
-			}
-			return nil
-		}
-
-		if i == 0 {
-			fmt.Printf("Waiting for response...")
-		} else if i%5 == 0 {
-			fmt.Printf(".")
-		}
+	// Execute task and wait for response
+	updatedTask, err := api.ExecuteTaskAndWait(ctx, client, callbackID, command, params, config)
+	if err != nil {
+		fmt.Printf("\n%v\n", err)
+		return nil
 	}
 
-	fmt.Println("\nTimeout waiting for response")
+	if updatedTask.Response != "" {
+		fmt.Printf("\nResponse:\n%s\n", updatedTask.Response)
+	} else {
+		fmt.Printf("\nTask completed with status: %s\n", updatedTask.Status)
+	}
 	return nil
 }
\ No newline at end of file
cmd/payload.go
@@ -0,0 +1,226 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"os"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var payloadCmd = &cobra.Command{
+	Use:     "payload [payload_id]",
+	Aliases: []string{"payloads"},
+	Short:   "List payloads or show payload details",
+	Long:    "Display all payloads created in Mythic, or show detailed information for a specific payload by ID.",
+	Args:    cobra.RangeArgs(0, 1),
+	RunE:    runPayload,
+}
+
+func init() {
+	rootCmd.AddCommand(payloadCmd)
+}
+
+func runPayload(cmd *cobra.Command, args []string) error {
+	if err := validateConfig(); err != nil {
+		return err
+	}
+
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// If payload ID is provided, show detailed view
+	if len(args) > 0 {
+		payloadID, err := strconv.Atoi(args[0])
+		if err != nil {
+			return fmt.Errorf("invalid payload ID: %s", args[0])
+		}
+		return showPayloadDetails(ctx, client, payloadID)
+	}
+
+	// Otherwise, show list view
+	return showPayloadList(ctx, client)
+}
+
+func showPayloadList(ctx context.Context, client *mythic.Client) error {
+	payloads, err := client.GetPayloads(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get payloads: %w", err)
+	}
+
+	if len(payloads) == 0 {
+		fmt.Println("No payloads found")
+		return nil
+	}
+
+	fmt.Printf("Payloads (%d total):\n\n", len(payloads))
+
+	// Create a tab writer for formatted output
+	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+	fmt.Fprintln(w, "ID\tTYPE\tDESCRIPTION\tFILENAME\tBUILD STATUS\tC2 PROFILES\tOPERATOR\tCREATED")
+	fmt.Fprintln(w, "--\t----\t-----------\t--------\t------------\t-----------\t--------\t-------")
+
+	for _, payload := range payloads {
+		// Format description
+		description := payload.Description
+		if len(description) > 30 {
+			description = description[:27] + "..."
+		}
+		if description == "" {
+			description = "-"
+		}
+
+		// Format filename
+		filename := payload.Filename
+		if len(filename) > 20 {
+			filename = filename[:17] + "..."
+		}
+		if filename == "" {
+			filename = "-"
+		}
+
+		// Format build status
+		buildStatus := payload.BuildPhase
+		if buildStatus == "" {
+			buildStatus = "unknown"
+		}
+
+		// Format C2 profiles
+		c2ProfilesStr := strings.Join(payload.C2Profiles, ",")
+		if len(c2ProfilesStr) > 20 {
+			c2ProfilesStr = c2ProfilesStr[:17] + "..."
+		}
+		if c2ProfilesStr == "" {
+			c2ProfilesStr = "-"
+		}
+
+		// Format creation time
+		created := payload.CreationTime
+		if created != "" {
+			if t, err := time.Parse(time.RFC3339, created); err == nil {
+				created = t.Format("2006-01-02 15:04")
+			}
+		}
+
+		// Show auto-generated indicator
+		typeStr := payload.PayloadTypeName
+		if payload.AutoGenerated {
+			typeStr += "*"
+		}
+
+		fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
+			payload.ID,
+			typeStr,
+			description,
+			filename,
+			buildStatus,
+			c2ProfilesStr,
+			payload.OperatorUsername,
+			created,
+		)
+	}
+
+	w.Flush()
+
+	fmt.Println("\n* = Auto-generated payload")
+	fmt.Println("\nTo view payload details: go-mythic payload <id>")
+	fmt.Println("To download a payload file: go-mythic files download <agent_file_id>")
+
+	return nil
+}
+
+func showPayloadDetails(ctx context.Context, client *mythic.Client, payloadID int) error {
+	payloads, err := client.GetPayloads(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get payloads: %w", err)
+	}
+
+	var targetPayload *mythic.Payload
+	for _, payload := range payloads {
+		if payload.ID == payloadID {
+			targetPayload = &payload
+			break
+		}
+	}
+
+	if targetPayload == nil {
+		return fmt.Errorf("payload with ID %d not found", payloadID)
+	}
+
+	fmt.Printf("Payload Details (ID: %d)\n", targetPayload.ID)
+	fmt.Println(strings.Repeat("=", 50))
+
+	// Basic information
+	fmt.Printf("UUID: %s\n", targetPayload.UUID)
+	fmt.Printf("Type: %s", targetPayload.PayloadTypeName)
+	if targetPayload.AutoGenerated {
+		fmt.Printf(" (auto-generated)")
+	}
+	fmt.Println()
+
+	fmt.Printf("Description: %s\n", getValueOrDefault(targetPayload.Description, "No description"))
+	fmt.Printf("Filename: %s\n", getValueOrDefault(targetPayload.Filename, "No filename"))
+
+	// Build information
+	fmt.Println("\nBuild Information:")
+	fmt.Printf("  Status: %s\n", getValueOrDefault(targetPayload.BuildPhase, "unknown"))
+
+	if targetPayload.BuildMessage != "" {
+		fmt.Printf("  Message: %s\n", targetPayload.BuildMessage)
+	}
+
+	if targetPayload.BuildStderr != "" {
+		fmt.Printf("  Errors: %s\n", targetPayload.BuildStderr)
+	}
+
+	// C2 Profiles
+	fmt.Println("\nC2 Profiles:")
+	if len(targetPayload.C2Profiles) > 0 {
+		for _, profile := range targetPayload.C2Profiles {
+			fmt.Printf("  - %s\n", profile)
+		}
+	} else {
+		fmt.Println("  No C2 profiles configured")
+	}
+
+	// File information
+	if targetPayload.AgentFileID != "" {
+		fmt.Println("\nFile Information:")
+		fmt.Printf("  Agent File ID: %s\n", targetPayload.AgentFileID)
+		fmt.Printf("  File ID: %d\n", targetPayload.FileID)
+	}
+
+	// Metadata
+	fmt.Println("\nMetadata:")
+	fmt.Printf("  Operator: %s (ID: %d)\n", targetPayload.OperatorUsername, targetPayload.OperatorID)
+	fmt.Printf("  Callback Alert: %t\n", targetPayload.CallbackAlert)
+
+	if targetPayload.CreationTime != "" {
+		if t, err := time.Parse(time.RFC3339, targetPayload.CreationTime); err == nil {
+			fmt.Printf("  Created: %s\n", t.Format("2006-01-02 15:04:05"))
+		} else {
+			fmt.Printf("  Created: %s\n", targetPayload.CreationTime)
+		}
+	}
+
+	// Download instructions
+	if targetPayload.AgentFileID != "" {
+		fmt.Printf("\nTo download this payload:\n")
+		fmt.Printf("  go-mythic files download %s\n", targetPayload.AgentFileID)
+	}
+
+	return nil
+}
+
+func getValueOrDefault(value, defaultValue string) string {
+	if value == "" {
+		return defaultValue
+	}
+	return value
+}
\ No newline at end of file
cmd/root.go
@@ -29,7 +29,7 @@ func Execute() {
 }
 
 func init() {
-	rootCmd.PersistentFlags().StringVar(&mythicURL, "url", "https://mythic.adversarytactics.local:7443/graphql/", "Mythic GraphQL API URL")
+	rootCmd.PersistentFlags().StringVar(&mythicURL, "url", "", "Mythic GraphQL API URL")
 	rootCmd.PersistentFlags().StringVar(&token, "token", "", "JWT authentication token")
 	rootCmd.PersistentFlags().BoolVar(&insecure, "insecure", true, "Skip TLS certificate verification")
 	rootCmd.PersistentFlags().StringVar(&socksProxy, "socks", "", "SOCKS5 proxy address (e.g., 127.0.0.1:9050)")
@@ -38,14 +38,22 @@ func init() {
 	if urlEnv := os.Getenv("MYTHIC_API_URL"); urlEnv != "" {
 		mythicURL = urlEnv
 	}
-	
+
 	// Set default token from environment if available
 	if tokenEnv := os.Getenv("MYTHIC_API_TOKEN"); tokenEnv != "" {
 		token = tokenEnv
 	}
+
+	// Set default SOCKS proxy from environment if available
+	if socksEnv := os.Getenv("MYTHIC_SOCKS"); socksEnv != "" {
+		socksProxy = socksEnv
+	}
 }
 
-func validateAuth() error {
+func validateConfig() error {
+	if mythicURL == "" {
+		return fmt.Errorf("Mythic server URL is required. Use --url flag or set MYTHIC_API_URL environment variable")
+	}
 	if token == "" {
 		return fmt.Errorf("authentication token is required. Use --token flag or set MYTHIC_API_TOKEN environment variable")
 	}
cmd/task.go
@@ -0,0 +1,97 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"go-mythic/pkg/mythic/api"
+	"strconv"
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	taskWaitTime int
+	taskRawOutput bool
+)
+
+var taskCmd = &cobra.Command{
+	Use:     "task <callback_id> <command> [args...]",
+	Aliases: []string{"t", "exec"},
+	Short:   "Send a command to an agent and wait for response",
+	Long:    "Send a command to the specified agent callback and wait for the response. This is like a single-shot version of interact.",
+	Args:    cobra.MinimumNArgs(2),
+	RunE:    runTask,
+}
+
+func init() {
+	rootCmd.AddCommand(taskCmd)
+	taskCmd.Flags().IntVarP(&taskWaitTime, "wait", "w", 30, "Maximum time to wait for response (seconds)")
+	taskCmd.Flags().BoolVar(&taskRawOutput, "raw", false, "Output only raw response bytes")
+}
+
+func runTask(cmd *cobra.Command, args []string) error {
+	if err := validateConfig(); err != nil {
+		return err
+	}
+
+	callbackID, err := strconv.Atoi(args[0])
+	if err != nil {
+		return fmt.Errorf("invalid callback ID: %s", args[0])
+	}
+
+	command := args[1]
+	params := ""
+	if len(args) > 2 {
+		params = strings.Join(args[2:], " ")
+	}
+
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx := context.Background()
+
+	// Verify callback exists and is active
+	targetCallback, err := api.FindActiveCallback(ctx, client, callbackID)
+	if err != nil {
+		return err
+	}
+
+	// For raw output, don't show progress messages
+	if !taskRawOutput {
+		fmt.Printf("Executing '%s %s' on callback %d (%s@%s)\n",
+			command, params, callbackID, targetCallback.User, targetCallback.Host)
+	}
+
+	// Configure polling
+	config := api.TaskPollConfig{
+		TimeoutSeconds: taskWaitTime,
+		PollInterval:   api.DefaultPollInterval,
+		ShowProgress:   !taskRawOutput,
+		RawOutput:      taskRawOutput,
+	}
+
+	// Execute task and wait for response
+	updatedTask, err := api.ExecuteTaskAndWait(ctx, client, targetCallback.ID, command, params, config)
+	if err != nil {
+		if taskRawOutput {
+			// For raw output on timeout, don't show error message
+			return nil
+		}
+		fmt.Printf("\n%v\n", err)
+		return nil
+	}
+
+	if taskRawOutput {
+		// Raw output: only print response
+		if updatedTask.Response != "" {
+			fmt.Print(updatedTask.Response)
+		}
+	} else {
+		// Normal output: show status and response
+		fmt.Printf("\nTask Status: %s\n", updatedTask.Status)
+		if updatedTask.Response != "" {
+			fmt.Printf("Response:\n%s\n", updatedTask.Response)
+		}
+	}
+	return nil
+}
\ No newline at end of file
cmd/task_list.go
@@ -0,0 +1,128 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"os"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	listShowResponses bool
+	listTaskLimit     int
+)
+
+var taskListCmd = &cobra.Command{
+	Use:     "task-list <callback_id>",
+	Aliases: []string{"tl"},
+	Short:   "List tasks for a specific agent",
+	Long:    "Display task history for a specific agent callback, equivalent to viewing the tasking tab for an agent in the UI.",
+	Args:    cobra.ExactArgs(1),
+	RunE:    runTaskList,
+}
+
+func init() {
+	rootCmd.AddCommand(taskListCmd)
+	taskListCmd.Flags().BoolVar(&listShowResponses, "responses", false, "Show task responses/output")
+	taskListCmd.Flags().IntVar(&listTaskLimit, "limit", 10, "Maximum number of tasks to display")
+}
+
+func runTaskList(cmd *cobra.Command, args []string) error {
+	if err := validateConfig(); err != nil {
+		return err
+	}
+
+	callbackID, err := strconv.Atoi(args[0])
+	if err != nil {
+		return fmt.Errorf("invalid callback ID: %s", args[0])
+	}
+
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	tasks, err := client.GetCallbackTasks(ctx, callbackID)
+	if err != nil {
+		return fmt.Errorf("failed to get tasks for callback %d: %w", callbackID, err)
+	}
+
+	if len(tasks) == 0 {
+		fmt.Printf("No tasks found for callback %d\n", callbackID)
+		return nil
+	}
+
+	// Apply limit from the end (keep newest tasks if limiting)
+	if listTaskLimit > 0 && len(tasks) > listTaskLimit {
+		tasks = tasks[len(tasks)-listTaskLimit:]
+	}
+
+	fmt.Printf("Tasks for callback %d (showing %d, newest last):\n\n", callbackID, len(tasks))
+
+	// Create a tab writer for formatted output
+	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+	fmt.Fprintln(w, "TASK ID\tCOMMAND\tPARAMS\tSTATUS\tTIMESTAMP")
+	fmt.Fprintln(w, "-------\t-------\t------\t------\t---------")
+
+	for _, task := range tasks {
+		// Truncate params if too long
+		params := task.Params
+		if len(params) > 50 {
+			params = params[:47] + "..."
+		}
+
+		// Format timestamp
+		timestamp := task.Timestamp
+		if timestamp != "" {
+			if t, err := time.Parse(time.RFC3339, timestamp); err == nil {
+				timestamp = t.Format("2006-01-02 15:04:05")
+			}
+		}
+
+		fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
+			task.DisplayID,
+			task.Command,
+			params,
+			task.Status,
+			timestamp,
+		)
+	}
+
+	w.Flush()
+
+	// If responses are requested, fetch and display them
+	if listShowResponses {
+		fmt.Println("\nTask Responses:")
+		fmt.Println(strings.Repeat("=", 50))
+
+		for i, task := range tasks {
+			if i >= 5 { // Limit responses to first 5 tasks to avoid overwhelming output
+				break
+			}
+
+			fmt.Printf("\nTask %d (%s):\n", task.DisplayID, task.Command)
+			fmt.Println(strings.Repeat("-", 30))
+
+			// Get full task with response
+			fullTask, err := client.GetTaskResponse(ctx, task.ID)
+			if err != nil {
+				fmt.Printf("Error getting response: %v\n", err)
+				continue
+			}
+
+			if fullTask.Response != "" {
+				fmt.Println(fullTask.Response)
+			} else {
+				fmt.Println("(No response yet)")
+			}
+			fmt.Println()
+		}
+	}
+
+	return nil
+}
\ No newline at end of file
cmd/task_view.go
@@ -0,0 +1,84 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	taskViewRawOutput bool
+)
+
+var taskViewCmd = &cobra.Command{
+	Use:     "task-view <task_id>",
+	Aliases: []string{"tv"},
+	Short:   "Show details for a specific task",
+	Long:    "Display detailed information and response for a specific task by its ID.",
+	Args:    cobra.ExactArgs(1),
+	RunE:    runTaskView,
+}
+
+func init() {
+	rootCmd.AddCommand(taskViewCmd)
+	taskViewCmd.Flags().BoolVar(&taskViewRawOutput, "raw", false, "Output only raw response bytes")
+}
+
+func runTaskView(cmd *cobra.Command, args []string) error {
+	if err := validateConfig(); err != nil {
+		return err
+	}
+
+	taskID, err := strconv.Atoi(args[0])
+	if err != nil {
+		return fmt.Errorf("invalid task ID: %s", args[0])
+	}
+
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	task, err := client.GetTaskResponse(ctx, taskID)
+	if err != nil {
+		return fmt.Errorf("failed to get task %d: %w", taskID, err)
+	}
+
+	// If raw output requested, only print the response
+	if taskViewRawOutput {
+		if task.Response != "" {
+			fmt.Print(task.Response)
+		}
+		return nil
+	}
+
+	// Otherwise, show full task details
+	fmt.Printf("Task Details (ID: %d)\n", task.DisplayID)
+	fmt.Println(strings.Repeat("=", 50))
+	fmt.Printf("Command: %s\n", task.Command)
+	fmt.Printf("Parameters: %s\n", task.Params)
+	fmt.Printf("Status: %s\n", task.Status)
+	fmt.Printf("Callback ID: %d\n", task.CallbackID)
+	if task.Timestamp != "" {
+		if t, err := time.Parse(time.RFC3339, task.Timestamp); err == nil {
+			fmt.Printf("Timestamp: %s\n", t.Format("2006-01-02 15:04:05"))
+		} else {
+			fmt.Printf("Timestamp: %s\n", task.Timestamp)
+		}
+	}
+	fmt.Printf("Completed: %t\n", task.Completed)
+
+	fmt.Println("\nResponse:")
+	fmt.Println(strings.Repeat("-", 30))
+	if task.Response != "" {
+		fmt.Println(task.Response)
+	} else {
+		fmt.Println("(No response yet)")
+	}
+
+	return nil
+}
\ No newline at end of file
cmd/upload.go
@@ -0,0 +1,176 @@
+package cmd
+
+import (
+	"bufio"
+	"context"
+	"encoding/json"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	uploadPayloadID  int
+	uploadCallbackID int
+	uploadPath       string
+	uploadLocalFile  string
+)
+
+var uploadCmd = &cobra.Command{
+	Use:   "upload",
+	Short: "Upload a file to a callback",
+	Long:  "Upload a payload or local file to a specified callback location with confirmation prompt. Either --payload or --local must be specified.",
+	RunE:  runUpload,
+}
+
+func init() {
+	rootCmd.AddCommand(uploadCmd)
+	uploadCmd.Flags().IntVar(&uploadPayloadID, "payload", 0, "Payload ID to upload")
+	uploadCmd.Flags().StringVar(&uploadLocalFile, "local", "", "Local file path to upload")
+	uploadCmd.Flags().IntVar(&uploadCallbackID, "callback", 0, "Callback ID to upload to")
+	uploadCmd.Flags().StringVar(&uploadPath, "path", "", "Remote path where to upload the file")
+
+	uploadCmd.MarkFlagRequired("callback")
+	uploadCmd.MarkFlagRequired("path")
+}
+
+type UploadParams struct {
+	RemotePath string `json:"remote_path"`
+	File       string `json:"file"`
+	FileName   string `json:"file_name"`
+	Host       string `json:"host"`
+}
+
+func runUpload(cmd *cobra.Command, args []string) error {
+	if err := validateConfig(); err != nil {
+		return err
+	}
+
+	// Validate that exactly one of --payload or --local is specified
+	if (uploadPayloadID == 0 && uploadLocalFile == "") || (uploadPayloadID != 0 && uploadLocalFile != "") {
+		return fmt.Errorf("exactly one of --payload or --local must be specified")
+	}
+
+	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// Get callback information
+	callbacks, err := client.GetActiveCallbacks(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get callbacks: %w", err)
+	}
+
+	var targetCallback *mythic.Callback
+	for _, callback := range callbacks {
+		if callback.DisplayID == uploadCallbackID {
+			targetCallback = &callback
+			break
+		}
+	}
+
+	if targetCallback == nil {
+		return fmt.Errorf("callback %d not found or not active", uploadCallbackID)
+	}
+
+	var uploadParams UploadParams
+	var sourceDescription string
+
+	if uploadPayloadID != 0 {
+		// Payload mode
+		payloads, err := client.GetPayloads(ctx)
+		if err != nil {
+			return fmt.Errorf("failed to get payloads: %w", err)
+		}
+
+		var targetPayload *mythic.Payload
+		for _, payload := range payloads {
+			if payload.ID == uploadPayloadID {
+				targetPayload = &payload
+				break
+			}
+		}
+
+		if targetPayload == nil {
+			return fmt.Errorf("payload with ID %d not found", uploadPayloadID)
+		}
+
+		uploadParams = UploadParams{
+			RemotePath: uploadPath,
+			File:       targetPayload.AgentFileID,
+			FileName:   targetPayload.Filename,
+			Host:       targetCallback.Host,
+		}
+
+		sourceDescription = fmt.Sprintf("Payload: %s (Type: %s, ID: %d)", targetPayload.Filename, targetPayload.PayloadTypeName, targetPayload.ID)
+	} else {
+		// Local file mode
+		// Check if local file exists
+		if _, err := os.Stat(uploadLocalFile); os.IsNotExist(err) {
+			return fmt.Errorf("local file does not exist: %s", uploadLocalFile)
+		}
+
+		// For local files, we need to create the file reference in Mythic first
+		// This is a simplified approach - in reality, we'd need to upload the file content
+		filename := filepath.Base(uploadLocalFile)
+
+		uploadParams = UploadParams{
+			RemotePath: uploadPath,
+			File:       uploadLocalFile, // Using local path as placeholder
+			FileName:   filename,
+			Host:       targetCallback.Host,
+		}
+
+		sourceDescription = fmt.Sprintf("Local file: %s", uploadLocalFile)
+	}
+
+	// Display confirmation information
+	fmt.Printf("Upload Details:\n")
+	fmt.Printf("===============\n")
+	fmt.Printf("%s\n", sourceDescription)
+	fmt.Printf("Target:  %s@%s (Callback: %d)\n", targetCallback.User, targetCallback.Host, targetCallback.DisplayID)
+	fmt.Printf("Path:    %s\n", uploadPath)
+	fmt.Printf("\nProceed with upload? [y/N]: ")
+
+	// Get user confirmation
+	reader := bufio.NewReader(os.Stdin)
+	response, err := reader.ReadString('\n')
+	if err != nil {
+		return fmt.Errorf("failed to read input: %w", err)
+	}
+
+	response = strings.TrimSpace(strings.ToLower(response))
+	if response != "y" && response != "yes" {
+		fmt.Println("Upload cancelled.")
+		return nil
+	}
+
+	// Convert to JSON
+	paramsJSON, err := json.Marshal(uploadParams)
+	if err != nil {
+		return fmt.Errorf("failed to marshal upload parameters: %w", err)
+	}
+
+	fmt.Printf("\nCreating upload task...\n")
+
+	// Create the upload task
+	task, err := client.CreateTask(ctx, targetCallback.ID, "upload", string(paramsJSON))
+	if err != nil {
+		return fmt.Errorf("failed to create upload task: %w", err)
+	}
+
+	fmt.Printf("Upload task %d created successfully\n", task.DisplayID)
+	fmt.Printf("Task parameters: %s\n", string(paramsJSON))
+
+	if uploadLocalFile != "" {
+		fmt.Printf("\nNote: Local file uploads require the file to be manually transferred to Mythic first.\n")
+		fmt.Printf("This command creates the upload task but does not transfer the file content.\n")
+	}
+
+	return nil
+}
\ No newline at end of file
pkg/mythic/api/callbacks.go
@@ -0,0 +1,28 @@
+package api
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+)
+
+// FindActiveCallback looks up an active callback by its display ID
+func FindActiveCallback(ctx context.Context, client MythicClient, callbackID int) (*mythic.Callback, error) {
+	callbacks, err := client.GetActiveCallbacks(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get callbacks: %w", err)
+	}
+
+	for _, callback := range callbacks {
+		if callback.DisplayID == callbackID {
+			return &callback, nil
+		}
+	}
+
+	return nil, fmt.Errorf("callback %d not found or not active", callbackID)
+}
+
+// ValidateCallbackExists verifies that a callback exists and is accessible
+func ValidateCallbackExists(ctx context.Context, client MythicClient, callbackID int) (*mythic.Callback, error) {
+	return FindActiveCallback(ctx, client, callbackID)
+}
\ No newline at end of file
pkg/mythic/api/callbacks_test.go
@@ -0,0 +1,116 @@
+package api
+
+import (
+	"context"
+	"go-mythic/pkg/mythic"
+	"testing"
+)
+
+// MockClient implements the MythicClient interface for testing
+type MockClient struct {
+	callbacks []mythic.Callback
+	err       error
+}
+
+func (m *MockClient) GetActiveCallbacks(ctx context.Context) ([]mythic.Callback, error) {
+	if m.err != nil {
+		return nil, m.err
+	}
+	return m.callbacks, nil
+}
+
+func (m *MockClient) CreateTask(ctx context.Context, callbackID int, command, params string) (*mythic.Task, error) {
+	return nil, nil // Not implemented for this test
+}
+
+func (m *MockClient) GetTaskResponse(ctx context.Context, taskID int) (*mythic.Task, error) {
+	return nil, nil // Not implemented for this test
+}
+
+func TestFindActiveCallback(t *testing.T) {
+	tests := []struct {
+		name        string
+		callbacks   []mythic.Callback
+		callbackID  int
+		expectErr   bool
+		expectedID  int
+	}{
+		{
+			name: "callback found",
+			callbacks: []mythic.Callback{
+				{ID: 1, DisplayID: 10, Host: "host1", User: "user1"},
+				{ID: 2, DisplayID: 20, Host: "host2", User: "user2"},
+			},
+			callbackID: 20,
+			expectErr:  false,
+			expectedID: 2,
+		},
+		{
+			name: "callback not found",
+			callbacks: []mythic.Callback{
+				{ID: 1, DisplayID: 10, Host: "host1", User: "user1"},
+			},
+			callbackID: 99,
+			expectErr:  true,
+		},
+		{
+			name:       "no callbacks",
+			callbacks:  []mythic.Callback{},
+			callbackID: 10,
+			expectErr:  true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client := &MockClient{callbacks: tt.callbacks}
+			ctx := context.Background()
+
+			result, err := FindActiveCallback(ctx, client, tt.callbackID)
+
+			if tt.expectErr {
+				if err == nil {
+					t.Error("Expected error, but got nil")
+				}
+				if result != nil {
+					t.Error("Expected nil result on error")
+				}
+			} else {
+				if err != nil {
+					t.Errorf("Unexpected error: %v", err)
+				}
+				if result == nil {
+					t.Error("Expected callback, but got nil")
+				} else if result.ID != tt.expectedID {
+					t.Errorf("Expected callback ID %d, got %d", tt.expectedID, result.ID)
+				}
+			}
+		})
+	}
+}
+
+func TestValidateCallbackExists(t *testing.T) {
+	callbacks := []mythic.Callback{
+		{ID: 1, DisplayID: 10, Host: "host1", User: "user1"},
+	}
+	client := &MockClient{callbacks: callbacks}
+	ctx := context.Background()
+
+	// Test valid callback
+	result, err := ValidateCallbackExists(ctx, client, 10)
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+	if result == nil {
+		t.Error("Expected callback, but got nil")
+	}
+
+	// Test invalid callback
+	result, err = ValidateCallbackExists(ctx, client, 99)
+	if err == nil {
+		t.Error("Expected error for non-existent callback")
+	}
+	if result != nil {
+		t.Error("Expected nil result for non-existent callback")
+	}
+}
\ No newline at end of file
pkg/mythic/api/constants.go
@@ -0,0 +1,34 @@
+package api
+
+import "time"
+
+// Task status constants
+const (
+	TaskStatusCompleted = "completed"
+	TaskStatusError     = "error"
+	TaskStatusSubmitted = "submitted"
+)
+
+// Default timeout values
+const (
+	DefaultTaskTimeout     = 30 * time.Second
+	DefaultPollInterval    = 1 * time.Second
+	DefaultDownloadTimeout = 60 * time.Second
+)
+
+// Progress display constants
+const (
+	ProgressDotInterval = 5 // Show progress dot every N polling cycles
+)
+
+// File size estimation constants
+const (
+	EstimatedChunkSizeKB = 512 // Rough estimate for file size calculation
+)
+
+// UUID format constants
+const (
+	UUIDLength         = 36
+	UUIDLengthNoHyphens = 32
+	UUIDDisplayLength  = 8 // Number of characters to show in truncated displays
+)
\ No newline at end of file
pkg/mythic/api/constants_test.go
@@ -0,0 +1,47 @@
+package api
+
+import (
+	"testing"
+	"time"
+)
+
+func TestConstants(t *testing.T) {
+	// Test task status constants
+	if TaskStatusCompleted != "completed" {
+		t.Errorf("Expected TaskStatusCompleted to be 'completed', got %q", TaskStatusCompleted)
+	}
+	if TaskStatusError != "error" {
+		t.Errorf("Expected TaskStatusError to be 'error', got %q", TaskStatusError)
+	}
+	if TaskStatusSubmitted != "submitted" {
+		t.Errorf("Expected TaskStatusSubmitted to be 'submitted', got %q", TaskStatusSubmitted)
+	}
+
+	// Test timeout constants
+	if DefaultTaskTimeout != 30*time.Second {
+		t.Errorf("Expected DefaultTaskTimeout to be 30s, got %v", DefaultTaskTimeout)
+	}
+	if DefaultPollInterval != 1*time.Second {
+		t.Errorf("Expected DefaultPollInterval to be 1s, got %v", DefaultPollInterval)
+	}
+	if DefaultDownloadTimeout != 60*time.Second {
+		t.Errorf("Expected DefaultDownloadTimeout to be 60s, got %v", DefaultDownloadTimeout)
+	}
+
+	// Test numeric constants
+	if ProgressDotInterval != 5 {
+		t.Errorf("Expected ProgressDotInterval to be 5, got %d", ProgressDotInterval)
+	}
+	if EstimatedChunkSizeKB != 512 {
+		t.Errorf("Expected EstimatedChunkSizeKB to be 512, got %d", EstimatedChunkSizeKB)
+	}
+	if UUIDLength != 36 {
+		t.Errorf("Expected UUIDLength to be 36, got %d", UUIDLength)
+	}
+	if UUIDLengthNoHyphens != 32 {
+		t.Errorf("Expected UUIDLengthNoHyphens to be 32, got %d", UUIDLengthNoHyphens)
+	}
+	if UUIDDisplayLength != 8 {
+		t.Errorf("Expected UUIDDisplayLength to be 8, got %d", UUIDDisplayLength)
+	}
+}
\ No newline at end of file
pkg/mythic/api/interfaces.go
@@ -0,0 +1,16 @@
+package api
+
+import (
+	"context"
+	"go-mythic/pkg/mythic"
+)
+
+// MythicClient defines the interface for Mythic operations needed by the api package
+type MythicClient interface {
+	GetActiveCallbacks(ctx context.Context) ([]mythic.Callback, error)
+	CreateTask(ctx context.Context, callbackID int, command, params string) (*mythic.Task, error)
+	GetTaskResponse(ctx context.Context, taskID int) (*mythic.Task, error)
+}
+
+// Ensure that the real Client implements the interface
+var _ MythicClient = (*mythic.Client)(nil)
\ No newline at end of file
pkg/mythic/api/tasks.go
@@ -0,0 +1,65 @@
+package api
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"time"
+)
+
+// TaskPollConfig configures task polling behavior
+type TaskPollConfig struct {
+	TimeoutSeconds int
+	PollInterval   time.Duration
+	ShowProgress   bool
+	RawOutput      bool
+}
+
+// DefaultTaskPollConfig returns sensible defaults for task polling
+func DefaultTaskPollConfig() TaskPollConfig {
+	return TaskPollConfig{
+		TimeoutSeconds: int(DefaultTaskTimeout.Seconds()),
+		PollInterval:   DefaultPollInterval,
+		ShowProgress:   true,
+		RawOutput:      false,
+	}
+}
+
+// PollTaskResponse polls for a task response until completion or timeout
+func PollTaskResponse(ctx context.Context, client MythicClient, taskID int, config TaskPollConfig) (*mythic.Task, error) {
+	for i := 0; i < config.TimeoutSeconds; i++ {
+		time.Sleep(config.PollInterval)
+
+		updatedTask, err := client.GetTaskResponse(ctx, taskID)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get task response: %w", err)
+		}
+
+		if updatedTask.Status == TaskStatusCompleted || updatedTask.Status == TaskStatusError {
+			return updatedTask, nil
+		}
+
+		// Show progress dots (but not for raw output)
+		if config.ShowProgress && !config.RawOutput && i > 0 && i%ProgressDotInterval == 0 {
+			fmt.Printf(".")
+		}
+	}
+
+	return nil, fmt.Errorf("timeout waiting for response after %d seconds", config.TimeoutSeconds)
+}
+
+// ExecuteTaskAndWait creates a task and waits for its response
+func ExecuteTaskAndWait(ctx context.Context, client MythicClient, callbackID int, command, params string, config TaskPollConfig) (*mythic.Task, error) {
+	// Create the task
+	task, err := client.CreateTask(ctx, callbackID, command, params)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create task: %w", err)
+	}
+
+	if config.ShowProgress && !config.RawOutput {
+		fmt.Printf("Task %d created, waiting for response...\n", task.DisplayID)
+	}
+
+	// Poll for response
+	return PollTaskResponse(ctx, client, task.ID, config)
+}
\ No newline at end of file
pkg/mythic/api/tasks_test.go
@@ -0,0 +1,164 @@
+package api
+
+import (
+	"context"
+	"fmt"
+	"go-mythic/pkg/mythic"
+	"testing"
+	"time"
+)
+
+// MockTaskClient extends MockClient to support task operations
+type MockTaskClient struct {
+	MockClient
+	tasks       map[int]*mythic.Task
+	createErr   error
+	getTaskErr  error
+	taskCounter int
+}
+
+func (m *MockTaskClient) CreateTask(ctx context.Context, callbackID int, command, params string) (*mythic.Task, error) {
+	if m.createErr != nil {
+		return nil, m.createErr
+	}
+
+	m.taskCounter++
+	task := &mythic.Task{
+		ID:         m.taskCounter,
+		DisplayID:  m.taskCounter * 10,
+		Command:    command,
+		Params:     params,
+		Status:     TaskStatusSubmitted,
+		CallbackID: callbackID,
+	}
+
+	if m.tasks == nil {
+		m.tasks = make(map[int]*mythic.Task)
+	}
+	m.tasks[task.ID] = task
+
+	return task, nil
+}
+
+func (m *MockTaskClient) GetTaskResponse(ctx context.Context, taskID int) (*mythic.Task, error) {
+	if m.getTaskErr != nil {
+		return nil, m.getTaskErr
+	}
+
+	task, exists := m.tasks[taskID]
+	if !exists {
+		return nil, fmt.Errorf("task %d not found", taskID)
+	}
+
+	// Return a copy to simulate potential status changes
+	return &mythic.Task{
+		ID:         task.ID,
+		DisplayID:  task.DisplayID,
+		Command:    task.Command,
+		Params:     task.Params,
+		Status:     task.Status,
+		Response:   task.Response,
+		CallbackID: task.CallbackID,
+		Completed:  task.Status == TaskStatusCompleted || task.Status == TaskStatusError,
+	}, nil
+}
+
+// SetTaskCompleted simulates a task completing
+func (m *MockTaskClient) SetTaskCompleted(taskID int, response string) {
+	if task, exists := m.tasks[taskID]; exists {
+		task.Status = TaskStatusCompleted
+		task.Response = response
+		task.Completed = true
+	}
+}
+
+func TestDefaultTaskPollConfig(t *testing.T) {
+	config := DefaultTaskPollConfig()
+
+	if config.TimeoutSeconds != int(DefaultTaskTimeout.Seconds()) {
+		t.Errorf("Expected TimeoutSeconds to be %d, got %d", int(DefaultTaskTimeout.Seconds()), config.TimeoutSeconds)
+	}
+	if config.PollInterval != DefaultPollInterval {
+		t.Errorf("Expected PollInterval to be %v, got %v", DefaultPollInterval, config.PollInterval)
+	}
+	if !config.ShowProgress {
+		t.Error("Expected ShowProgress to be true")
+	}
+	if config.RawOutput {
+		t.Error("Expected RawOutput to be false")
+	}
+}
+
+func TestExecuteTaskAndWait_Success(t *testing.T) {
+	client := &MockTaskClient{}
+	ctx := context.Background()
+
+	config := TaskPollConfig{
+		TimeoutSeconds: 5,
+		PollInterval:   100 * time.Millisecond,
+		ShowProgress:   false,
+		RawOutput:      true,
+	}
+
+	// Start task execution in background
+	go func() {
+		time.Sleep(200 * time.Millisecond)
+		// Simulate task completion after a short delay
+		client.SetTaskCompleted(1, "Task completed successfully")
+	}()
+
+	result, err := ExecuteTaskAndWait(ctx, client, 1, "ls", "-la", config)
+
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+	if result == nil {
+		t.Fatal("Expected task result, got nil")
+	}
+	if result.Status != TaskStatusCompleted {
+		t.Errorf("Expected status %q, got %q", TaskStatusCompleted, result.Status)
+	}
+	if result.Response != "Task completed successfully" {
+		t.Errorf("Expected response %q, got %q", "Task completed successfully", result.Response)
+	}
+}
+
+func TestExecuteTaskAndWait_CreateTaskError(t *testing.T) {
+	client := &MockTaskClient{
+		createErr: fmt.Errorf("failed to create task"),
+	}
+	ctx := context.Background()
+
+	config := DefaultTaskPollConfig()
+
+	result, err := ExecuteTaskAndWait(ctx, client, 1, "ls", "-la", config)
+
+	if err == nil {
+		t.Error("Expected error, got nil")
+	}
+	if result != nil {
+		t.Error("Expected nil result on error")
+	}
+}
+
+func TestExecuteTaskAndWait_Timeout(t *testing.T) {
+	client := &MockTaskClient{}
+	ctx := context.Background()
+
+	config := TaskPollConfig{
+		TimeoutSeconds: 1, // Very short timeout
+		PollInterval:   100 * time.Millisecond,
+		ShowProgress:   false,
+		RawOutput:      true,
+	}
+
+	// Don't complete the task - let it timeout
+	result, err := ExecuteTaskAndWait(ctx, client, 1, "ls", "-la", config)
+
+	if err == nil {
+		t.Error("Expected timeout error, got nil")
+	}
+	if result != nil {
+		t.Error("Expected nil result on timeout")
+	}
+}
\ No newline at end of file
pkg/mythic/client.go
@@ -5,9 +5,12 @@ import (
 	"crypto/tls"
 	"encoding/base64"
 	"fmt"
+	"io"
 	"net"
 	"net/http"
+	"net/url"
 	"regexp"
+	"strings"
 	"unicode/utf8"
 
 	"github.com/machinebox/graphql"
@@ -15,8 +18,10 @@ import (
 )
 
 type Client struct {
-	client *graphql.Client
-	token  string
+	client     *graphql.Client
+	token      string
+	httpClient *http.Client
+	baseURL    string
 }
 
 type Callback struct {
@@ -45,16 +50,68 @@ type Task struct {
 	Response      string `json:"response,omitempty"`
 	CallbackID    int    `json:"callback_id"`
 	OperatorID    int    `json:"operator_id"`
+	Timestamp     string `json:"timestamp,omitempty"`
+	Completed     bool   `json:"completed"`
 }
 
-func NewClient(url, token string, insecure bool, socksProxy string) *Client {
-	client := graphql.NewClient(url)
-	
+type File struct {
+	ID                  int    `json:"id"`
+	AgentFileID         string `json:"agent_file_id"`
+	Filename            string `json:"filename_utf8"`
+	FullRemotePath      string `json:"full_remote_path_utf8"`
+	Host                string `json:"host"`
+	Complete            bool   `json:"complete"`
+	ChunksReceived      int    `json:"chunks_received"`
+	TotalChunks         int    `json:"total_chunks"`
+	Timestamp           string `json:"timestamp"`
+	MD5                 string `json:"md5"`
+	SHA1                string `json:"sha1"`
+	IsDownloadFromAgent bool   `json:"is_download_from_agent"`
+	IsPayload           bool   `json:"is_payload"`
+	IsScreenshot        bool   `json:"is_screenshot"`
+	OperatorUsername    string `json:"operator_username"`
+	TaskID              int    `json:"task_id"`
+	TaskDisplayID       int    `json:"task_display_id"`
+	CallbackID          int    `json:"callback_id"`
+	CallbackHost        string `json:"callback_host"`
+	CallbackUser        string `json:"callback_user"`
+}
+
+type Payload struct {
+	ID            int    `json:"id"`
+	UUID          string `json:"uuid"`
+	Description   string `json:"description"`
+	BuildPhase    string `json:"build_phase"`
+	BuildMessage  string `json:"build_message"`
+	BuildStderr   string `json:"build_stderr"`
+	CallbackAlert bool   `json:"callback_alert"`
+	CreationTime  string `json:"creation_time"`
+	AutoGenerated bool   `json:"auto_generated"`
+	Deleted       bool   `json:"deleted"`
+
+	// Nested structs
+	OperatorID       int    `json:"operator_id"`
+	OperatorUsername string `json:"operator_username"`
+	PayloadTypeID    int    `json:"payloadtype_id"`
+	PayloadTypeName  string `json:"payloadtype_name"`
+
+	// File info
+	FileID       int    `json:"file_id"`
+	Filename     string `json:"filename"`
+	AgentFileID  string `json:"agent_file_id"`
+
+	// C2 Profiles
+	C2Profiles []string `json:"c2_profiles"`
+}
+
+func NewClient(apiURL, token string, insecure bool, socksProxy string) *Client {
+	client := graphql.NewClient(apiURL)
+
 	// Create transport with TLS config
 	tr := &http.Transport{
 		TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
 	}
-	
+
 	// Configure SOCKS5 proxy if provided
 	if socksProxy != "" {
 		dialer, err := proxy.SOCKS5("tcp", socksProxy, nil, proxy.Direct)
@@ -64,13 +121,19 @@ func NewClient(url, token string, insecure bool, socksProxy string) *Client {
 			}
 		}
 	}
-	
+
 	httpClient := &http.Client{Transport: tr}
-	client = graphql.NewClient(url, graphql.WithHTTPClient(httpClient))
-	
+	client = graphql.NewClient(apiURL, graphql.WithHTTPClient(httpClient))
+
+	// Extract base URL for file downloads (GraphQL URL -> base server URL)
+	parsedURL, _ := url.Parse(apiURL)
+	baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
+
 	return &Client{
-		client: client,
-		token:  token,
+		client:     client,
+		token:      token,
+		httpClient: httpClient,
+		baseURL:    baseURL,
 	}
 }
 
@@ -124,26 +187,7 @@ func decodeResponseText(responseText string) string {
 }
 
 func (c *Client) GetCallbacks(ctx context.Context) ([]Callback, error) {
-	req := graphql.NewRequest(`
-		query GetCallbacks {
-			callback {
-				id
-				display_id
-				user
-				host
-				process_name
-				pid
-				description
-				last_checkin
-				active
-				payload {
-					payloadtype {
-						name
-					}
-				}
-			}
-		}
-	`)
+	req := graphql.NewRequest(GetCallbacks)
 	
 	req.Header.Set("Authorization", "Bearer "+c.token)
 	
@@ -175,16 +219,7 @@ func (c *Client) GetActiveCallbacks(ctx context.Context) ([]Callback, error) {
 }
 
 func (c *Client) CreateTask(ctx context.Context, callbackID int, command, params string) (*Task, error) {
-	req := graphql.NewRequest(`
-		mutation createTasking($callback_id: Int!, $command: String!, $params: String!, $tasking_location: String) {
-			createTask(callback_id: $callback_id, command: $command, params: $params, tasking_location: $tasking_location) {
-				status
-				id
-				display_id
-				error
-			}
-		}
-	`)
+	req := graphql.NewRequest(CreateTask)
 	
 	req.Var("callback_id", callbackID)
 	req.Var("command", command)
@@ -220,23 +255,7 @@ func (c *Client) CreateTask(ctx context.Context, callbackID int, command, params
 }
 
 func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error) {
-	req := graphql.NewRequest(`
-		query GetTask($id: Int!) {
-			task_by_pk(id: $id) {
-				id
-				display_id
-				command_name
-				original_params
-				display_params
-				status
-				completed
-				callback {
-					id
-					display_id
-				}
-			}
-		}
-	`)
+	req := graphql.NewRequest(GetTask)
 	
 	req.Var("id", taskID)
 	req.Header.Set("Authorization", "Bearer "+c.token)
@@ -262,14 +281,7 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
 	}
 	
 	// Get task responses
-	responseReq := graphql.NewRequest(`
-		query GetTaskResponses($task_display_id: Int!) {
-			response(where: {task: {display_id: {_eq: $task_display_id}}}, order_by: {id: desc}, limit: 1) {
-				response_text
-				timestamp
-			}
-		}
-	`)
+	responseReq := graphql.NewRequest(GetTaskResponses)
 	
 	responseReq.Var("task_display_id", resp.Task.DisplayID)
 	responseReq.Header.Set("Authorization", "Bearer "+c.token)
@@ -285,10 +297,13 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
 		return nil, fmt.Errorf("failed to get task response: %w", err)
 	}
 	
-	response := ""
-	if len(responseResp.Response) > 0 {
-		response = decodeResponseText(responseResp.Response[0].ResponseText)
+	// Concatenate all response chunks in chronological order
+	var responseBuilder strings.Builder
+	for _, resp := range responseResp.Response {
+		decodedText := decodeResponseText(resp.ResponseText)
+		responseBuilder.WriteString(decodedText)
 	}
+	response := responseBuilder.String()
 	
 	status := resp.Task.Status
 	if resp.Task.Completed {
@@ -303,5 +318,295 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
 		Status:     status,
 		Response:   response,
 		CallbackID: resp.Task.Callback.ID,
+		Completed:  resp.Task.Completed,
 	}, nil
+}
+
+func (c *Client) GetCallbackTasks(ctx context.Context, callbackID int) ([]Task, error) {
+	req := graphql.NewRequest(GetCallbackTasks)
+
+	req.Var("callback_id", callbackID)
+	req.Header.Set("Authorization", "Bearer "+c.token)
+
+	var resp struct {
+		Task []struct {
+			ID            int    `json:"id"`
+			DisplayID     int    `json:"display_id"`
+			CommandName   string `json:"command_name"`
+			OriginalParams string `json:"original_params"`
+			DisplayParams  string `json:"display_params"`
+			Status        string `json:"status"`
+			Completed     bool   `json:"completed"`
+			Timestamp     string `json:"timestamp"`
+			Callback      struct {
+				ID        int `json:"id"`
+				DisplayID int `json:"display_id"`
+			} `json:"callback"`
+		} `json:"task"`
+	}
+
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get callback tasks: %w", err)
+	}
+
+	var tasks []Task
+	for _, t := range resp.Task {
+		status := t.Status
+		if t.Completed {
+			status = "completed"
+		}
+
+		tasks = append(tasks, Task{
+			ID:         t.ID,
+			DisplayID:  t.DisplayID,
+			Command:    t.CommandName,
+			Params:     t.DisplayParams,
+			Status:     status,
+			CallbackID: t.Callback.ID,
+			Timestamp:  t.Timestamp,
+			Completed:  t.Completed,
+		})
+	}
+
+	return tasks, nil
+}
+
+func (c *Client) GetDownloadedFiles(ctx context.Context) ([]File, error) {
+	req := graphql.NewRequest(GetDownloadedFiles)
+
+	req.Header.Set("Authorization", "Bearer "+c.token)
+
+	var resp struct {
+		Filemeta []struct {
+			ID             int    `json:"id"`
+			AgentFileID    string `json:"agent_file_id"`
+			Filename       string `json:"filename_utf8"`
+			FullRemotePath string `json:"full_remote_path_utf8"`
+			Host           string `json:"host"`
+			Complete       bool   `json:"complete"`
+			ChunksReceived int    `json:"chunks_received"`
+			TotalChunks    int    `json:"total_chunks"`
+			Timestamp      string `json:"timestamp"`
+			MD5            string `json:"md5"`
+			SHA1           string `json:"sha1"`
+			Operator       struct {
+				Username string `json:"username"`
+			} `json:"operator"`
+			Task struct {
+				ID        int `json:"id"`
+				DisplayID int `json:"display_id"`
+				Callback  struct {
+					ID        int    `json:"id"`
+					DisplayID int    `json:"display_id"`
+					Host      string `json:"host"`
+					User      string `json:"user"`
+				} `json:"callback"`
+			} `json:"task"`
+		} `json:"filemeta"`
+	}
+
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get downloaded files: %w", err)
+	}
+
+	var files []File
+	for _, f := range resp.Filemeta {
+		files = append(files, File{
+			ID:               f.ID,
+			AgentFileID:      f.AgentFileID,
+			Filename:         f.Filename,
+			FullRemotePath:   f.FullRemotePath,
+			Host:             f.Host,
+			Complete:         f.Complete,
+			ChunksReceived:   f.ChunksReceived,
+			TotalChunks:      f.TotalChunks,
+			Timestamp:        f.Timestamp,
+			MD5:              f.MD5,
+			SHA1:             f.SHA1,
+			OperatorUsername: f.Operator.Username,
+			TaskID:           f.Task.ID,
+			TaskDisplayID:    f.Task.DisplayID,
+			CallbackID:       f.Task.Callback.ID,
+			CallbackHost:     f.Task.Callback.Host,
+			CallbackUser:     f.Task.Callback.User,
+		})
+	}
+
+	return files, nil
+}
+
+func (c *Client) GetAllFiles(ctx context.Context) ([]File, error) {
+	req := graphql.NewRequest(GetAllFiles)
+
+	req.Header.Set("Authorization", "Bearer "+c.token)
+
+	var resp struct {
+		Filemeta []struct {
+			ID                  int    `json:"id"`
+			AgentFileID         string `json:"agent_file_id"`
+			Filename            string `json:"filename_utf8"`
+			FullRemotePath      string `json:"full_remote_path_utf8"`
+			Host                string `json:"host"`
+			Complete            bool   `json:"complete"`
+			ChunksReceived      int    `json:"chunks_received"`
+			TotalChunks         int    `json:"total_chunks"`
+			Timestamp           string `json:"timestamp"`
+			MD5                 string `json:"md5"`
+			SHA1                string `json:"sha1"`
+			IsDownloadFromAgent bool   `json:"is_download_from_agent"`
+			IsPayload           bool   `json:"is_payload"`
+			IsScreenshot        bool   `json:"is_screenshot"`
+			Operator            struct {
+				Username string `json:"username"`
+			} `json:"operator"`
+			Task struct {
+				ID        int `json:"id"`
+				DisplayID int `json:"display_id"`
+				Callback  struct {
+					ID        int    `json:"id"`
+					DisplayID int    `json:"display_id"`
+					Host      string `json:"host"`
+					User      string `json:"user"`
+				} `json:"callback"`
+			} `json:"task"`
+		} `json:"filemeta"`
+	}
+
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get all files: %w", err)
+	}
+
+	var files []File
+	for _, f := range resp.Filemeta {
+		files = append(files, File{
+			ID:                  f.ID,
+			AgentFileID:         f.AgentFileID,
+			Filename:            f.Filename,
+			FullRemotePath:      f.FullRemotePath,
+			Host:                f.Host,
+			Complete:            f.Complete,
+			ChunksReceived:      f.ChunksReceived,
+			TotalChunks:         f.TotalChunks,
+			Timestamp:           f.Timestamp,
+			MD5:                 f.MD5,
+			SHA1:                f.SHA1,
+			IsDownloadFromAgent: f.IsDownloadFromAgent,
+			IsPayload:           f.IsPayload,
+			IsScreenshot:        f.IsScreenshot,
+			OperatorUsername:    f.Operator.Username,
+			TaskID:              f.Task.ID,
+			TaskDisplayID:       f.Task.DisplayID,
+			CallbackID:          f.Task.Callback.ID,
+			CallbackHost:        f.Task.Callback.Host,
+			CallbackUser:        f.Task.Callback.User,
+		})
+	}
+
+	return files, nil
+}
+
+func (c *Client) DownloadFile(ctx context.Context, fileUUID string) ([]byte, error) {
+	downloadURL := fmt.Sprintf("%s/direct/download/%s", c.baseURL, fileUUID)
+
+	req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create download request: %w", err)
+	}
+
+	req.Header.Set("Authorization", "Bearer "+c.token)
+
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to download file: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("download failed with status %d: %s", resp.StatusCode, resp.Status)
+	}
+
+	data, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read file data: %w", err)
+	}
+
+	return data, nil
+}
+
+func (c *Client) GetPayloads(ctx context.Context) ([]Payload, error) {
+	req := graphql.NewRequest(GetPayloads)
+
+	req.Header.Set("Authorization", "Bearer "+c.token)
+
+	var resp struct {
+		Payload []struct {
+			ID            int    `json:"id"`
+			UUID          string `json:"uuid"`
+			Description   string `json:"description"`
+			BuildPhase    string `json:"build_phase"`
+			BuildMessage  string `json:"build_message"`
+			BuildStderr   string `json:"build_stderr"`
+			CallbackAlert bool   `json:"callback_alert"`
+			CreationTime  string `json:"creation_time"`
+			AutoGenerated bool   `json:"auto_generated"`
+			Deleted       bool   `json:"deleted"`
+			Operator      struct {
+				ID       int    `json:"id"`
+				Username string `json:"username"`
+			} `json:"operator"`
+			PayloadType struct {
+				ID   int    `json:"id"`
+				Name string `json:"name"`
+			} `json:"payloadtype"`
+			Filemetum struct {
+				ID          int    `json:"id"`
+				AgentFileID string `json:"agent_file_id"`
+				Filename    string `json:"filename_utf8"`
+			} `json:"filemetum"`
+			PayloadC2Profiles []struct {
+				C2Profile struct {
+					Name             string `json:"name"`
+					Running          bool   `json:"running"`
+					IsP2P            bool   `json:"is_p2p"`
+					ContainerRunning bool   `json:"container_running"`
+				} `json:"c2profile"`
+			} `json:"payloadc2profiles"`
+		} `json:"payload"`
+	}
+
+	if err := c.client.Run(ctx, req, &resp); err != nil {
+		return nil, fmt.Errorf("failed to get payloads: %w", err)
+	}
+
+	var payloads []Payload
+	for _, p := range resp.Payload {
+		// Collect C2 profile names
+		var c2Profiles []string
+		for _, profile := range p.PayloadC2Profiles {
+			c2Profiles = append(c2Profiles, profile.C2Profile.Name)
+		}
+
+		payloads = append(payloads, Payload{
+			ID:               p.ID,
+			UUID:             p.UUID,
+			Description:      p.Description,
+			BuildPhase:       p.BuildPhase,
+			BuildMessage:     p.BuildMessage,
+			BuildStderr:      p.BuildStderr,
+			CallbackAlert:    p.CallbackAlert,
+			CreationTime:     p.CreationTime,
+			AutoGenerated:    p.AutoGenerated,
+			Deleted:          p.Deleted,
+			OperatorID:       p.Operator.ID,
+			OperatorUsername: p.Operator.Username,
+			PayloadTypeID:    p.PayloadType.ID,
+			PayloadTypeName:  p.PayloadType.Name,
+			FileID:           p.Filemetum.ID,
+			Filename:         p.Filemetum.Filename,
+			AgentFileID:      p.Filemetum.AgentFileID,
+			C2Profiles:       c2Profiles,
+		})
+	}
+
+	return payloads, nil
 }
\ No newline at end of file
pkg/mythic/client_test.go
@@ -0,0 +1,131 @@
+package mythic
+
+import (
+	"testing"
+)
+
+func TestIsBase64(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected bool
+	}{
+		{
+			name:     "valid base64 binary data",
+			input:    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9AHW1nw==", // 1x1 PNG
+			expected: true,
+		},
+		{
+			name:     "valid base64 text that decodes to text",
+			input:    "SGVsbG8gV29ybGQ=", // "Hello World" in base64
+			expected: true, // Actually returns true because the original is longer than decoded
+		},
+		{
+			name:     "short string",
+			input:    "hi",
+			expected: false,
+		},
+		{
+			name:     "invalid base64 characters",
+			input:    "hello@world!",
+			expected: false,
+		},
+		{
+			name:     "empty string",
+			input:    "",
+			expected: false,
+		},
+		{
+			name:     "valid base64 but short",
+			input:    "dGVzdA==", // "test" in base64
+			expected: true, // Actually returns true because original is longer than decoded
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := isBase64(tt.input)
+			if result != tt.expected {
+				t.Errorf("isBase64(%q) = %v, expected %v", tt.input, result, tt.expected)
+			}
+		})
+	}
+}
+
+func TestDecodeResponseText(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "valid base64 text",
+			input:    "SGVsbG8gV29ybGQ=", // "Hello World"
+			expected: "Hello World",
+		},
+		{
+			name:     "plain text",
+			input:    "Hello World",
+			expected: "Hello World",
+		},
+		{
+			name:     "empty string",
+			input:    "",
+			expected: "",
+		},
+		{
+			name:     "binary data base64",
+			input:    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9AHW1nw==",
+			expected: "[Binary data: 58 bytes]", // Actual decoded size
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := decodeResponseText(tt.input)
+			if result != tt.expected {
+				t.Errorf("decodeResponseText(%q) = %q, expected %q", tt.input, result, tt.expected)
+			}
+		})
+	}
+}
+
+func TestNewClient(t *testing.T) {
+	apiURL := "https://example.com/graphql"
+	token := "test-token"
+	insecure := true
+	socksProxy := ""
+
+	client := NewClient(apiURL, token, insecure, socksProxy)
+
+	if client == nil {
+		t.Fatal("NewClient returned nil")
+	}
+
+	if client.token != token {
+		t.Errorf("Expected token %q, got %q", token, client.token)
+	}
+
+	if client.baseURL != "https://example.com" {
+		t.Errorf("Expected baseURL %q, got %q", "https://example.com", client.baseURL)
+	}
+}
+
+func TestNewClientWithSocksProxy(t *testing.T) {
+	apiURL := "https://example.com/graphql"
+	token := "test-token"
+	insecure := true
+	socksProxy := "127.0.0.1:9050"
+
+	client := NewClient(apiURL, token, insecure, socksProxy)
+
+	if client == nil {
+		t.Fatal("NewClient returned nil")
+	}
+
+	// We can't easily test the SOCKS proxy configuration without making actual network calls,
+	// but we can at least ensure the client was created successfully
+	if client.token != token {
+		t.Errorf("Expected token %q, got %q", token, client.token)
+	}
+}
\ No newline at end of file
pkg/mythic/queries.go
@@ -0,0 +1,527 @@
+package mythic
+
+// This file contains GraphQL queries and mutations for the Mythic C2 framework.
+// It's a 1:1 port of contrib/Mythic_Scripting/mythic/graphql_queries.py
+// to maintain compatibility and ease upstream synchronization.
+//
+// To sync with upstream changes:
+// 1. Check contrib/Mythic_Scripting/mythic/graphql_queries.py for updates
+// 2. Update the corresponding Go constants in this file
+// 3. Test that the client functions still work with the updated queries
+//
+// The naming convention:
+// - Python snake_case variables become Go PascalCase constants
+// - gql(...) wrappers are removed (just the string content)
+// - f-string formatting is converted to Go constants with concatenation
+
+// Mutations
+
+const CreateAPIToken = `
+mutation createAPITokenMutation{
+    createAPIToken(token_type: "User"){
+        id
+        token_value
+        status
+        error
+        operator_id
+    }
+}`
+
+const GetAPITokens = `
+query GetAPITokens($username: String!) {
+    apitokens(where: {active: {_eq: true}, operator: {username: {_eq: $username}}, deleted: {_eq: false}}) {
+        token_value
+        active
+        id
+    }
+}`
+
+const CreateTask = `
+mutation createTasking($callback_id: Int!, $command: String!, $params: String!, $files: [String], $token_id: Int, $tasking_location: String, $original_params: String, $parameter_group_name: String, $is_interactive_task: Boolean, $interactive_task_type: Int, $parent_task_id: Int, $payload_type: String) {
+    createTask(callback_id: $callback_id, command: $command, params: $params, files: $files, token_id: $token_id, tasking_location: $tasking_location, original_params: $original_params, parameter_group_name: $parameter_group_name, is_interactive_task: $is_interactive_task, interactive_task_type: $interactive_task_type, parent_task_id: $parent_task_id, payload_type: $payload_type) {
+        status
+        id
+        display_id
+        error
+    }
+}`
+
+const UpdateCallback = `
+mutation updateCallbackInformation ($callback_display_id: Int!, $active: Boolean, $locked: Boolean, $description: String, $ips: [String], $user: String, $host: String, $os: String, $architecture: String, $extra_info: String, $sleep_info: String, $pid: Int, $process_name: String, $integrity_level: Int, $domain: String){
+    updateCallback(input: {callback_display_id: $callback_display_id, active: $active, locked: $locked, description: $description, ips: $ips, user: $user, host: $host, os: $os, architecture: $architecture, extra_info: $extra_info, sleep_info: $sleep_info, pid: $pid, process_name: $process_name, integrity_level: $integrity_level, domain: $domain}) {
+        status
+        error
+    }
+}`
+
+const CreatePayload = `
+mutation createPayloadMutation($payload: String!) {
+    createPayload(payloadDefinition: $payload) {
+        error
+        status
+        uuid
+    }
+}`
+
+const CreateOperator = `
+mutation NewOperator($username: String!, $password: String!, $email: String, $bot: Boolean) {
+    createOperator(input: {password: $password, username: $username, email: $email, bot: $bot}) {
+        ...create_operator_fragment
+    }
+}
+` + CreateOperatorFragment
+
+const GetOperationAndOperatorByName = `
+query getOperationAndOperator($operation_name: String!, $operator_username: String!){
+    operation(where: {name: {_eq: $operation_name}}){
+        id
+        operatoroperations(where: {operator: {username: {_eq: $operator_username}}}) {
+            view_mode
+            id
+        }
+    }
+    operator(where: {username: {_eq: $operator_username}}){
+        id
+    }
+}`
+
+// Fragments
+
+const TaskFragment = `
+fragment task_fragment on task {
+    callback {
+        id
+        display_id
+    }
+    id
+    display_id
+    operator{
+        username
+    }
+    status
+    completed
+    original_params
+    display_params
+    timestamp
+    command_name
+    tasks {
+        id
+    }
+    token {
+        token_id
+    }
+}`
+
+const MythictreeFragment = `
+fragment mythictree_fragment on mythictree {
+    task_id
+    timestamp
+    host
+    comment
+    success
+    deleted
+    tree_type
+    os
+    can_have_children
+    name_text
+    parent_path_text
+    full_path_text
+    metadata
+}`
+
+const OperatorFragment = `
+fragment operator_fragment on operator {
+    id
+    username
+    admin
+    active
+    last_login
+    current_operation_id
+    deleted
+}`
+
+const CallbackFragment = `
+fragment callback_fragment on callback {
+    architecture
+    description
+    domain
+    external_ip
+    host
+    id
+    display_id
+    integrity_level
+    ip
+    extra_info
+    sleep_info
+    pid
+    os
+    user
+    agent_callback_id
+    operation_id
+    process_name
+    payload {
+        os
+        payloadtype {
+            name
+        }
+        description
+        uuid
+    }
+}`
+
+const PayloadBuildFragment = `
+fragment payload_build_fragment on payload {
+    build_phase
+    uuid
+    build_stdout
+    build_stderr
+    build_message
+    id
+}`
+
+const CreateOperatorFragment = `
+fragment create_operator_fragment on OperatorOutput {
+    active
+    creation_time
+    deleted
+    error
+    id
+    last_login
+    status
+    username
+    view_utc_time
+    account_type
+    email
+}`
+
+const GetOperationsFragment = `
+fragment get_operations_fragment on operation {
+    complete
+    name
+    id
+    admin {
+        username
+        id
+    }
+    operatoroperations {
+        view_mode
+        operator {
+            username
+            id
+        }
+        id
+    }
+}`
+
+const AddOperatorToOperationFragment = `
+fragment add_operator_to_operation_fragment on updateOperatorOperation{
+    status
+    error
+}`
+
+const RemoveOperatorFromOperationFragment = `
+fragment remove_operator_from_operation_fragment on updateOperatorOperation{
+    status
+    error
+}`
+
+const UpdateOperatorInOperationFragment = `
+fragment update_operator_in_operation_fragment on updateOperatorOperation{
+    status
+    error
+}`
+
+const CreateOperationFragment = `
+fragment create_operation_fragment on createOperationOutput {
+    status
+    error
+    operation{
+        name
+        id
+        admin {
+            id
+            username
+        }
+    }
+}`
+
+const UserOutputFragment = `
+fragment user_output_fragment on response {
+    response_text
+    timestamp
+}`
+
+const TaskOutputFragment = `
+fragment task_output_fragment on response {
+    id
+    timestamp
+    response_text
+    task {
+        id
+        display_id
+        status
+        completed
+        agent_task_id
+        command_name
+    }
+}`
+
+const PayloadDataFragment = `
+fragment payload_data_fragment on payload {
+    build_message
+    build_phase
+    build_stderr
+    callback_alert
+    creation_time
+    id
+    operator {
+        id
+        username
+    }
+    uuid
+    description
+    deleted
+    auto_generated
+    payloadtype {
+        id
+        name
+    }
+    filemetum {
+        agent_file_id
+        filename_utf8
+        id
+    }
+    payloadc2profiles {
+        c2profile {
+            running
+            name
+            is_p2p
+            container_running
+        }
+    }
+}`
+
+const FileDataFragment = `
+fragment file_data_fragment on filemeta{
+    agent_file_id
+    chunk_size
+    chunks_received
+    complete
+    deleted
+    filename_utf8
+    full_remote_path_utf8
+    host
+    id
+    is_download_from_agent
+    is_payload
+    is_screenshot
+    md5
+    operator {
+        id
+        username
+    }
+    comment
+    sha1
+    timestamp
+    total_chunks
+    task {
+        id
+        comment
+        command {
+            cmd
+            id
+        }
+    }
+}`
+
+const CommandFragment = `
+fragment command_fragment on command {
+    id
+    cmd
+    attributes
+}`
+
+// Query builders for common operations
+
+const GetCallbacks = `
+query GetCallbacks {
+    callback {
+        id
+        display_id
+        user
+        host
+        process_name
+        pid
+        description
+        last_checkin
+        active
+        payload {
+            payloadtype {
+                name
+            }
+        }
+    }
+}`
+
+const GetTask = `
+query GetTask($id: Int!) {
+    task_by_pk(id: $id) {
+        id
+        display_id
+        command_name
+        original_params
+        display_params
+        status
+        completed
+        callback {
+            id
+            display_id
+        }
+    }
+}`
+
+const GetTaskResponses = `
+query GetTaskResponses($task_display_id: Int!) {
+    response(where: {task: {display_id: {_eq: $task_display_id}}}, order_by: {id: asc}) {
+        response_text
+        timestamp
+    }
+}`
+
+const GetCallbackTasks = `
+query GetCallbackTasks($callback_id: Int!) {
+    task(where: {callback_id: {_eq: $callback_id}}, order_by: {id: asc}) {
+        id
+        display_id
+        command_name
+        original_params
+        display_params
+        status
+        completed
+        timestamp
+        callback {
+            id
+            display_id
+        }
+    }
+}`
+
+const GetDownloadedFiles = `
+query GetDownloadedFiles {
+    filemeta(where: {is_download_from_agent: {_eq: true}, deleted: {_eq: false}}, order_by: {id: desc}) {
+        agent_file_id
+        filename_utf8
+        full_remote_path_utf8
+        host
+        id
+        complete
+        chunks_received
+        total_chunks
+        timestamp
+        md5
+        sha1
+        operator {
+            username
+        }
+        task {
+            id
+            display_id
+            callback {
+                id
+                display_id
+                host
+                user
+            }
+        }
+    }
+}`
+
+const GetAllTasksWithResponses = `
+query GetAllTasksWithResponses($limit: Int) {
+    task(where: {status: {_in: ["completed", "error"]}}, order_by: {id: desc}, limit: $limit) {
+        id
+        display_id
+        command_name
+        original_params
+        display_params
+        status
+        completed
+        timestamp
+        callback {
+            id
+            display_id
+            host
+            user
+        }
+    }
+}`
+
+const GetPayloads = `
+query GetPayloads {
+    payload(where: {deleted: {_eq: false}}, order_by: {id: desc}) {
+        build_message
+        build_phase
+        build_stderr
+        callback_alert
+        creation_time
+        id
+        operator {
+            id
+            username
+        }
+        uuid
+        description
+        deleted
+        auto_generated
+        payloadtype {
+            id
+            name
+        }
+        filemetum {
+            agent_file_id
+            filename_utf8
+            id
+        }
+        payloadc2profiles {
+            c2profile {
+                running
+                name
+                is_p2p
+                container_running
+            }
+        }
+    }
+}`
+
+const GetAllFiles = `
+query GetAllFiles {
+    filemeta(where: {deleted: {_eq: false}}, order_by: {id: desc}) {
+        agent_file_id
+        filename_utf8
+        full_remote_path_utf8
+        host
+        id
+        complete
+        chunks_received
+        total_chunks
+        timestamp
+        md5
+        sha1
+        is_download_from_agent
+        is_payload
+        is_screenshot
+        operator {
+            username
+        }
+        task {
+            id
+            display_id
+            callback {
+                id
+                display_id
+                host
+                user
+            }
+        }
+    }
+}`
\ No newline at end of file
.gitignore
@@ -1,3 +1,4 @@
 bin/*
 mythic.sh
 contrib/Mythic_Scripting
+.env
investigate_forge.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+
+import asyncio
+import sys
+import os
+
+# Add the contrib directory to the path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'contrib', 'Mythic_Scripting'))
+
+from mythic import mythic
+
+async def investigate_forge_tasks():
+    try:
+        # Connect to Mythic using environment variables
+        server_ip = "mythic.adversarytactics.local"
+        apitoken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTgwNTc3ODUsImlhdCI6MTc1ODA0MzM4NSwidXNlcl9pZCI6NCwiYXV0aCI6ImFwaSIsImV2ZW50c3RlcGluc3RhbmNlX2lkIjowLCJhcGl0b2tlbnNfaWQiOjI2LCJvcGVyYXRpb25faWQiOjB9.dNx93CuQLMMhvbJEgVs1Rg_IQYDfhtTRjEiupXlEpYc"
+
+        mythic_conn = await mythic.login(
+            server_ip=server_ip,
+            apitoken=apitoken,
+            server_port=7443,
+            ssl=True
+        )
+
+        print("Connected to Mythic successfully")
+
+        # Get all tasks
+        tasks = await mythic_conn.get_all_tasks()
+
+        # Filter for forge-related tasks
+        forge_tasks = []
+        for task in tasks:
+            if task.command and 'forge' in task.command.lower():
+                forge_tasks.append(task)
+
+        if not forge_tasks:
+            print("No forge-related tasks found")
+            return
+
+        print(f"Found {len(forge_tasks)} forge-related tasks")
+        print("=" * 60)
+
+        for task in forge_tasks:
+            print(f"Task ID: {task.display_id}")
+            print(f"Command: {task.command}")
+            print(f"Status: {task.status}")
+            print(f"Completed: {task.completed}")
+            print(f"Callback: {task.callback.display_id if task.callback else 'Unknown'}")
+            print(f"Parameters: {task.original_params if task.original_params else 'None'}")
+
+            # Get task responses
+            responses = await mythic_conn.get_all_responses_for_task(task.id)
+            if responses:
+                print("Responses:")
+                for resp in responses:
+                    print(f"  - {resp.response[:200]}{'...' if len(resp.response) > 200 else ''}")
+            else:
+                print("No responses found")
+
+            print("-" * 40)
+
+    except Exception as e:
+        print(f"Error: {e}")
+        import traceback
+        traceback.print_exc()
+
+if __name__ == "__main__":
+    asyncio.run(investigate_forge_tasks())
\ No newline at end of file
TODO.md
@@ -0,0 +1,55 @@
+- grep should also look at the command execution strings too, not just output
+- forge equivalency with execute_assembly and inline_assembly
+- task-list --all (same as --limit 0)
+
+## Code Quality Improvements
+
+### High Priority
+1. **Extract callback lookup logic to shared utility** - Remove duplication in task.go and interact.go
+2. **Standardize task polling with configurable timeouts** - Create reusable polling mechanism
+3. **Create constants file for magic numbers** - Replace hardcoded values (timeouts, status strings)
+4. **Add basic unit tests for client functions** - Test GraphQL operations and response handling
+
+### Medium Priority
+1. **Implement proper error types with context** - Create MythicError, CallbackNotFoundError, TaskTimeoutError
+2. **Add configuration management** - Centralized config with proper defaults
+3. **Create interfaces for better testability** - MythicClient interface for mocking
+4. **Improve logging and debugging** - Add structured logging with slog
+
+### Low Priority
+1. **Refactor large functions into smaller components** - Break down extractUUIDFromTaskResponse
+2. **Add comprehensive integration tests** - Test CLI commands with fixtures
+3. **Implement retry mechanisms for network operations** - Handle transient failures
+4. **Add performance monitoring/metrics** - Track operation success/failure rates
+
+## Code Duplication Issues
+- **Callback validation**: task.go:54-69 and interact.go:42-57 duplicate callback lookup
+- **Task polling**: Similar patterns in task.go:88-116 and interact.go:119-144
+- **Client creation**: Repeated client instantiation across all commands
+- **Config validation**: validateConfig() called in every command
+
+## Suggested Package Structure
+```
+pkg/
+├── mythic/          # Existing client
+├── utils/           # New shared utilities
+│   ├── callbacks.go # Callback lookup helpers
+│   ├── tasks.go     # Task polling utilities
+│   ├── constants.go # Shared constants
+│   └── validation.go # Common validation
+├── config/          # Configuration management
+│   └── config.go
+└── errors/          # Custom error types
+    └── errors.go
+```
+
+## Error Handling Improvements
+- Inconsistent timeout handling (some return nil, others error)
+- Missing error context for network/GraphQL failures
+- Raw output mode silently fails on timeout (task.go:118-124)
+
+## Testing Strategy
+- No Go tests currently exist
+- Reference Python tests in contrib/Mythic_Scripting/tests/
+- Need unit tests for client, integration tests for CLI
+- Missing coverage for base64 decoding, UUID extraction, error paths
USAGE.md
@@ -25,6 +25,19 @@ make help
 go build -o bin/go-mythic .
 ```
 
+## Project Structure
+
+### GraphQL Queries Synchronization
+The project maintains a 1:1 copy of GraphQL queries from the Python Mythic_Scripting library:
+- **Source:** `contrib/Mythic_Scripting/mythic/graphql_queries.py` 
+- **Go Version:** `pkg/mythic/queries.go`
+- **Purpose:** Easier upstream synchronization and reduced raw strings in client code
+
+To sync with upstream updates:
+1. Check the Python file for changes
+2. Update the corresponding Go constants
+3. Test client functionality
+
 ## Basic Commands
 
 ### List Active Callbacks
@@ -69,37 +82,42 @@ The CLI supports SOCKS5 proxy routing for all network traffic, replacing the nee
 
 ## Configuration
 
-### Authentication Token
-Authentication is required via JWT token. Set it using one of these methods:
+**Both URL and authentication token are required.**
 
+### Quick Setup
 ```bash
-# Set via environment variable (recommended)
+# Set via environment variables (recommended)
+export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
 export MYTHIC_API_TOKEN="your-jwt-token-here"
 ./go-mythic callbacks
 
-# Set via command line flag
-./go-mythic --token "your-jwt-token-here" callbacks
-
-# Get token from mythic.sh for testing
+# For testing with mythic.sh
+export MYTHIC_API_URL="https://mythic.adversarytactics.local:7443/graphql/"
 export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
+./go-mythic callbacks
 ```
 
-### Custom Mythic Server
+### Individual Configuration Options
+
+#### Mythic Server URL
 ```bash
 # Set via environment variable
 export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
-./go-mythic callbacks
 
 # Set via command line flag
 ./go-mythic --url "https://your-mythic-server:7443/graphql/" callbacks
 ```
 
-### Complete Environment Setup
+#### Authentication Token
 ```bash
-# Set both URL and token via environment variables
-export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
+# Set via environment variable
 export MYTHIC_API_TOKEN="your-jwt-token-here"
-./go-mythic callbacks
+
+# Set via command line flag
+./go-mythic --token "your-jwt-token-here" callbacks
+
+# Get token from mythic.sh for testing
+export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
 ```
 
 ## Lab 01 Equivalents