#include "cli/handlers/agent/commands.h" #include #include #include #include #include #include #include #include #include "absl/flags/declare.h" #include "absl/flags/flag.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_replace.h" #include "absl/strings/string_view.h" #include "app/core/project.h" #include "app/zelda3/dungeon/room.h" #include "cli/handlers/agent/common.h" #include "cli/modern_cli.h" #include "cli/service/ai/ai_service.h" #include "cli/service/ai/gemini_ai_service.h" #include "cli/service/ai/ollama_ai_service.h" #include "cli/service/ai/service_factory.h" #include "cli/service/agent/proposal_executor.h" #include "cli/service/agent/simple_chat_session.h" #include "cli/service/planning/proposal_registry.h" #include "cli/service/planning/tile16_proposal_generator.h" #include "cli/service/resources/resource_catalog.h" #include "cli/service/resources/resource_context_builder.h" #include "cli/service/rom/rom_sandbox_manager.h" #include "cli/tui/chat_tui.h" #include "cli/z3ed.h" #include "util/macro.h" ABSL_DECLARE_FLAG(std::string, rom); ABSL_DECLARE_FLAG(std::string, ai_provider); namespace yaze { namespace cli { namespace agent { namespace { struct DescribeOptions { std::optional resource; std::string format = "json"; std::optional output_path; std::string version = "0.1.0"; std::optional last_updated; }; // Helper to load project and labels if available absl::Status TryLoadProjectAndLabels(Rom& rom) { // Try to find and load a project file in current directory core::YazeProject project; auto project_status = project.Open("."); if (project_status.ok()) { std::cout << "📂 Loaded project: " << project.name << "\n"; // Initialize embedded labels (all default Zelda3 resource names) auto labels_status = project.InitializeEmbeddedLabels(); if (labels_status.ok()) { std::cout << "✅ Embedded labels initialized (all Zelda3 resources available)\n"; } // Load labels from project (either embedded or external) if (!project.labels_filename.empty()) { auto* label_mgr = rom.resource_label(); if (label_mgr && label_mgr->LoadLabels(project.labels_filename)) { std::cout << "đŸˇī¸ Loaded custom labels from: " << project.labels_filename << "\n"; } } else if (!project.resource_labels.empty() || project.use_embedded_labels) { // Use labels embedded in project or default Zelda3 labels auto* label_mgr = rom.resource_label(); if (label_mgr) { label_mgr->labels_ = project.resource_labels; label_mgr->labels_loaded_ = true; std::cout << "đŸˇī¸ Using embedded Zelda3 labels (rooms, sprites, entrances, items, etc.)\n"; } } } else { // No project found - use embedded defaults anyway std::cout << "â„šī¸ No project file found. Using embedded default Zelda3 labels.\n"; project.InitializeEmbeddedLabels(); } return absl::OkStatus(); } absl::Status EnsureRomLoaded(Rom& rom, const std::string& command) { if (rom.is_loaded()) { return absl::OkStatus(); } std::string rom_path = absl::GetFlag(FLAGS_rom); if (rom_path.empty()) { return absl::FailedPreconditionError( absl::StrFormat( "No ROM loaded. Pass --rom= when running %s.\n" "Example: z3ed %s --rom=zelda3.sfc", command, command)); } // Load the ROM auto status = rom.LoadFromFile(rom_path); if (!status.ok()) { return absl::FailedPreconditionError(absl::StrFormat( "Failed to load ROM from '%s': %s", rom_path, status.message())); } return absl::OkStatus(); } absl::StatusOr ParseDescribeArgs( const std::vector& args) { DescribeOptions options; for (size_t i = 0; i < args.size(); ++i) { const std::string& token = args[i]; std::string flag = token; std::optional inline_value; if (absl::StartsWith(token, "--")) { auto eq_pos = token.find('='); if (eq_pos != std::string::npos) { flag = token.substr(0, eq_pos); inline_value = token.substr(eq_pos + 1); } } auto require_value = [&](absl::string_view flag_name) -> absl::StatusOr { if (inline_value.has_value()) { return *inline_value; } if (i + 1 >= args.size()) { return absl::InvalidArgumentError( absl::StrFormat("Flag %s requires a value", flag_name)); } return args[++i]; }; if (flag == "--resource") { ASSIGN_OR_RETURN(auto value, require_value("--resource")); options.resource = std::move(value); } else if (flag == "--format") { ASSIGN_OR_RETURN(auto value, require_value("--format")); options.format = std::move(value); } else if (flag == "--output") { ASSIGN_OR_RETURN(auto value, require_value("--output")); options.output_path = std::move(value); } else if (flag == "--version") { ASSIGN_OR_RETURN(auto value, require_value("--version")); options.version = std::move(value); } else if (flag == "--last-updated") { ASSIGN_OR_RETURN(auto value, require_value("--last-updated")); options.last_updated = std::move(value); } else { return absl::InvalidArgumentError( absl::StrFormat("Unknown flag for agent describe: %s", token)); } } options.format = absl::AsciiStrToLower(options.format); if (options.format != "json" && options.format != "yaml") { return absl::InvalidArgumentError("--format must be either json or yaml"); } return options; } } // namespace absl::Status HandleRunCommand(const std::vector& arg_vec, Rom& rom) { if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") { return absl::InvalidArgumentError("Usage: agent run --prompt "); } std::string prompt = arg_vec[1]; RETURN_IF_ERROR(EnsureRomLoaded(rom, "agent run --prompt \"\"")); // Get commands from the AI service auto ai_service = CreateAIService(); // Use service factory auto response_or = ai_service->GenerateResponse(prompt); if (!response_or.ok()) { return response_or.status(); } AgentResponse response = std::move(response_or.value()); if (response.commands.empty()) { return absl::FailedPreconditionError( "Agent response did not include any executable commands."); } std::string provider = absl::GetFlag(FLAGS_ai_provider); ProposalCreationRequest request; request.prompt = prompt; request.response = &response; request.rom = &rom; request.sandbox_label = "agent-run"; request.ai_provider = std::move(provider); ASSIGN_OR_RETURN(auto proposal_result, CreateProposalFromAgentResponse(request)); const auto& metadata = proposal_result.metadata; std::filesystem::path proposal_dir = metadata.log_path.parent_path(); std::cout << "✅ Agent successfully planned and executed changes in a sandbox." << std::endl; std::cout << " Proposal ID: " << metadata.id << std::endl; std::cout << " Sandbox ROM: " << metadata.sandbox_rom_path << std::endl; std::cout << " Proposal dir: " << proposal_dir << std::endl; std::cout << " Diff file: " << metadata.diff_path << std::endl; std::cout << " Log file: " << metadata.log_path << std::endl; std::cout << " Proposal JSON: " << proposal_result.proposal_json_path << std::endl; std::cout << " Commands executed: " << proposal_result.executed_commands << std::endl; std::cout << " Tile16 changes: " << proposal_result.change_count << std::endl; std::cout << "\nTo review the changes, run:\n"; std::cout << " z3ed agent diff --proposal-id " << metadata.id << std::endl; std::cout << "\nTo accept the changes, run:\n"; std::cout << " z3ed agent accept --proposal-id " << metadata.id << std::endl; return absl::OkStatus(); } absl::Status HandlePlanCommand(const std::vector& arg_vec) { if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") { return absl::InvalidArgumentError("Usage: agent plan --prompt "); } std::string prompt = arg_vec[1]; auto ai_service = CreateAIService(); // Use service factory auto response_or = ai_service->GenerateResponse(prompt); if (!response_or.ok()) { return response_or.status(); } std::vector commands = response_or.value().commands; // Create a proposal from the commands Tile16ProposalGenerator generator; auto proposal_or = generator.GenerateFromCommands(prompt, commands, "ollama", nullptr); if (!proposal_or.ok()) { return proposal_or.status(); } auto proposal = proposal_or.value(); // TODO: Save the proposal to disk using ProposalRegistry // For now, just print it. std::cout << "AI Agent Plan (Proposal ID: " << proposal.id << "):\n"; std::cout << proposal.ToJson() << std::endl; return absl::OkStatus(); } absl::Status HandleDiffCommand(Rom& rom, const std::vector& args) { std::optional proposal_id; for (size_t i = 0; i < args.size(); ++i) { const std::string& token = args[i]; if (absl::StartsWith(token, "--proposal-id=")) { proposal_id = token.substr(14); } else if (token == "--proposal-id" && i + 1 < args.size()) { proposal_id = args[i + 1]; ++i; } } auto& registry = ProposalRegistry::Instance(); absl::StatusOr proposal_or; if (proposal_id.has_value()) { proposal_or = registry.GetProposal(proposal_id.value()); } else { proposal_or = registry.GetLatestPendingProposal(); } if (proposal_or.ok()) { const auto& proposal = proposal_or.value(); std::cout << "\n=== Proposal Diff ===\n"; std::cout << "Proposal ID: " << proposal.id << "\n"; std::cout << "Sandbox ID: " << proposal.sandbox_id << "\n"; std::cout << "Prompt: " << proposal.prompt << "\n"; std::cout << "Description: " << proposal.description << "\n"; std::cout << "Status: "; switch (proposal.status) { case ProposalRegistry::ProposalStatus::kPending: std::cout << "Pending"; break; case ProposalRegistry::ProposalStatus::kAccepted: std::cout << "Accepted"; break; case ProposalRegistry::ProposalStatus::kRejected: std::cout << "Rejected"; break; } std::cout << "\n"; std::cout << "Created: " << absl::FormatTime(proposal.created_at) << "\n"; std::cout << "Commands Executed: " << proposal.commands_executed << "\n"; std::cout << "Bytes Changed: " << proposal.bytes_changed << "\n\n"; if (!proposal.sandbox_rom_path.empty()) { std::cout << "Sandbox ROM: " << proposal.sandbox_rom_path << "\n"; } std::cout << "Proposal directory: " << proposal.log_path.parent_path() << "\n"; std::cout << "Diff file: " << proposal.diff_path << "\n"; std::cout << "Log file: " << proposal.log_path << "\n\n"; if (std::filesystem::exists(proposal.diff_path)) { std::cout << "--- Diff Content ---\n"; std::ifstream diff_file(proposal.diff_path); if (diff_file.is_open()) { std::string line; while (std::getline(diff_file, line)) { std::cout << line << "\n"; } } else { std::cout << "(Unable to read diff file)\n"; } } else { std::cout << "(No diff file found)\n"; } std::cout << "\n--- Execution Log ---\n"; if (std::filesystem::exists(proposal.log_path)) { std::ifstream log_file(proposal.log_path); if (log_file.is_open()) { std::string line; int line_count = 0; while (std::getline(log_file, line)) { std::cout << line << "\n"; line_count++; if (line_count > 50) { std::cout << "... (log truncated, see " << proposal.log_path << " for full output)\n"; break; } } } else { std::cout << "(Unable to read log file)\n"; } } else { std::cout << "(No log file found)\n"; } std::cout << "\n=== Next Steps ===\n"; std::cout << "To accept changes: z3ed agent commit\n"; std::cout << "To reject changes: z3ed agent revert\n"; std::cout << "To review in GUI: yaze --proposal=" << proposal.id << "\n"; return absl::OkStatus(); } if (rom.is_loaded()) { auto sandbox_or = RomSandboxManager::Instance().ActiveSandbox(); if (!sandbox_or.ok()) { return absl::NotFoundError( "No pending proposals found and no active sandbox. Run 'z3ed agent " "run' first."); } RomDiff diff_handler; auto status = diff_handler.Run({rom.filename(), sandbox_or->rom_path.string()}); if (!status.ok()) { return status; } } else { return absl::AbortedError("No ROM loaded."); } return absl::OkStatus(); } absl::Status HandleLearnCommand() { std::cout << "Agent learn not yet implemented." << std::endl; return absl::OkStatus(); } absl::Status HandleListCommand() { auto& registry = ProposalRegistry::Instance(); auto proposals = registry.ListProposals(); if (proposals.empty()) { std::cout << "No proposals found.\n"; std::cout << "Run 'z3ed agent run --prompt \"...\"' to create a proposal.\n"; return absl::OkStatus(); } std::cout << "\n=== Agent Proposals ===\n\n"; for (const auto& proposal : proposals) { std::cout << "ID: " << proposal.id << "\n"; std::cout << " Status: "; switch (proposal.status) { case ProposalRegistry::ProposalStatus::kPending: std::cout << "Pending"; break; case ProposalRegistry::ProposalStatus::kAccepted: std::cout << "Accepted"; break; case ProposalRegistry::ProposalStatus::kRejected: std::cout << "Rejected"; break; } std::cout << "\n"; std::cout << " Created: " << absl::FormatTime(proposal.created_at) << "\n"; std::cout << " Prompt: " << proposal.prompt << "\n"; std::cout << " Commands: " << proposal.commands_executed << "\n"; std::cout << " Bytes Changed: " << proposal.bytes_changed << "\n"; std::cout << "\n"; } std::cout << "Total: " << proposals.size() << " proposal(s)\n"; std::cout << "\nUse 'z3ed agent diff --proposal-id=' to view details.\n"; return absl::OkStatus(); } absl::Status HandleCommitCommand(Rom& rom) { if (rom.is_loaded()) { auto status = rom.SaveToFile({.save_new = false}); if (!status.ok()) { return status; } std::cout << "✅ Changes committed successfully." << std::endl; } else { return absl::AbortedError("No ROM loaded."); } return absl::OkStatus(); } absl::Status HandleRevertCommand(Rom& rom) { if (rom.is_loaded()) { auto status = rom.LoadFromFile(rom.filename()); if (!status.ok()) { return status; } std::cout << "✅ Changes reverted successfully." << std::endl; } else { return absl::AbortedError("No ROM loaded."); } return absl::OkStatus(); } absl::Status HandleDescribeCommand(const std::vector& arg_vec) { ASSIGN_OR_RETURN(auto options, ParseDescribeArgs(arg_vec)); const auto& catalog = ResourceCatalog::Instance(); std::optional resource_schema; if (options.resource.has_value()) { auto resource_or = catalog.GetResource(*options.resource); if (!resource_or.ok()) { return resource_or.status(); } resource_schema = resource_or.value(); } std::string payload; if (options.format == "json") { if (resource_schema.has_value()) { payload = catalog.SerializeResource(*resource_schema); } else { payload = catalog.SerializeResources(catalog.AllResources()); } } else { std::string last_updated = options.last_updated.has_value() ? *options.last_updated : absl::FormatTime("%Y-%m-%d", absl::Now(), absl::LocalTimeZone()); if (resource_schema.has_value()) { std::vector schemas{*resource_schema}; payload = catalog.SerializeResourcesAsYaml(schemas, options.version, last_updated); } else { payload = catalog.SerializeResourcesAsYaml(catalog.AllResources(), options.version, last_updated); } } if (options.output_path.has_value()) { std::ofstream out(*options.output_path, std::ios::binary | std::ios::trunc); if (!out.is_open()) { return absl::InternalError(absl::StrFormat( "Failed to open %s for writing", *options.output_path)); } out << payload; out.close(); if (!out) { return absl::InternalError(absl::StrFormat("Failed to write schema to %s", *options.output_path)); } std::cout << absl::StrFormat("Wrote %s schema to %s", options.format, *options.output_path) << std::endl; return absl::OkStatus(); } std::cout << payload << std::endl; return absl::OkStatus(); } absl::Status HandleChatCommand(Rom& rom) { RETURN_IF_ERROR(EnsureRomLoaded(rom, "agent chat")); // Try to load project and labels automatically auto _ = TryLoadProjectAndLabels(rom); // Ignore errors - we'll use defaults tui::ChatTUI chat_tui(&rom); chat_tui.Run(); return absl::OkStatus(); } absl::Status HandleSimpleChatCommand(const std::vector& arg_vec, Rom* rom, bool quiet) { RETURN_IF_ERROR(EnsureRomLoaded(*rom, "agent simple-chat")); auto _ = TryLoadProjectAndLabels(*rom); std::optional batch_file; std::optional single_message; bool verbose = false; for (size_t i = 0; i < arg_vec.size(); ++i) { const std::string& arg = arg_vec[i]; if (absl::StartsWith(arg, "--file=")) { batch_file = arg.substr(7); } else if (arg == "--file" && i + 1 < arg_vec.size()) { batch_file = arg_vec[++i]; } else if (arg == "--verbose" || arg == "-v") { verbose = true; } else if (!absl::StartsWith(arg, "--") && !single_message.has_value()) { single_message = arg; } } agent::AgentConfig config; config.verbose = verbose; SimpleChatSession session; session.SetConfig(config); session.SetRomContext(rom); if (batch_file.has_value()) { std::ifstream file(*batch_file); if (!file.is_open()) { return absl::NotFoundError(absl::StrCat("Failed to open file: ", *batch_file)); } if (!quiet) { std::cout << "Running batch session from: " << *batch_file << std::endl; std::cout << "----------------------------------------\n\n"; } std::string line; int line_num = 0; while (std::getline(file, line)) { line_num++; std::string trimmed_line = std::string(absl::StripAsciiWhitespace(line)); if (trimmed_line.empty() || absl::StartsWith(trimmed_line, "#")) { continue; } if (!quiet) { std::cout << "Input [" << line_num << "]: " << trimmed_line << std::endl; } std::string response; auto status = session.SendAndWaitForResponse(trimmed_line, &response); if (!status.ok()) { std::cerr << "Error processing line " << line_num << ": " << status.message() << std::endl; continue; } std::cout << response << "\n"; if (!quiet) { std::cout << "\n"; } } return absl::OkStatus(); } else if (single_message.has_value()) { std::string response; auto status = session.SendAndWaitForResponse(*single_message, &response); if (!status.ok()) { return status; } std::cout << response << "\n"; return absl::OkStatus(); } else { return session.RunInteractive(); } } absl::Status HandleAcceptCommand(const std::vector& arg_vec, Rom& rom) { std::optional proposal_id; for (size_t i = 0; i < arg_vec.size(); ++i) { const std::string& token = arg_vec[i]; if (absl::StartsWith(token, "--proposal-id=")) { proposal_id = token.substr(14); break; } if (token == "--proposal-id" && i + 1 < arg_vec.size()) { proposal_id = arg_vec[i + 1]; break; } } if (!proposal_id.has_value() || proposal_id->empty()) { return absl::InvalidArgumentError( "Usage: agent accept --proposal-id "); } auto& registry = ProposalRegistry::Instance(); ASSIGN_OR_RETURN(auto metadata, registry.GetProposal(*proposal_id)); if (metadata.status == ProposalRegistry::ProposalStatus::kAccepted) { std::cout << "Proposal '" << *proposal_id << "' is already accepted." << std::endl; return absl::OkStatus(); } if (metadata.sandbox_rom_path.empty()) { return absl::FailedPreconditionError(absl::StrCat( "Proposal '", *proposal_id, "' is missing sandbox ROM metadata. Cannot accept.")); } if (!std::filesystem::exists(metadata.sandbox_rom_path)) { return absl::NotFoundError(absl::StrCat( "Sandbox ROM not found at ", metadata.sandbox_rom_path.string())); } RETURN_IF_ERROR( EnsureRomLoaded(rom, "agent accept --proposal-id ")); Rom sandbox_rom; auto sandbox_load_status = sandbox_rom.LoadFromFile( metadata.sandbox_rom_path.string(), RomLoadOptions::CliDefaults()); if (!sandbox_load_status.ok()) { return absl::InternalError(absl::StrCat( "Failed to load sandbox ROM: ", sandbox_load_status.message())); } if (rom.size() != sandbox_rom.size()) { rom.Expand(static_cast(sandbox_rom.size())); } auto copy_status = rom.WriteVector(0, sandbox_rom.vector()); if (!copy_status.ok()) { return absl::InternalError(absl::StrCat( "Failed to copy sandbox ROM data: ", copy_status.message())); } auto save_status = rom.SaveToFile({.save_new = false}); if (!save_status.ok()) { return absl::InternalError(absl::StrCat( "Failed to save changes to main ROM: ", save_status.message())); } RETURN_IF_ERROR(registry.UpdateStatus( *proposal_id, ProposalRegistry::ProposalStatus::kAccepted)); RETURN_IF_ERROR(registry.AppendLog( *proposal_id, absl::StrCat("Proposal accepted and applied to ", rom.filename()))); if (!metadata.sandbox_id.empty()) { auto remove_status = RomSandboxManager::Instance().RemoveSandbox(metadata.sandbox_id); if (!remove_status.ok()) { std::cerr << "Warning: Failed to remove sandbox '" << metadata.sandbox_id << "': " << remove_status.message() << "\n"; } } std::cout << "✅ Proposal '" << *proposal_id << "' accepted and applied to '" << rom.filename() << "'." << std::endl; std::cout << " Source sandbox ROM: " << metadata.sandbox_rom_path << std::endl; return absl::OkStatus(); } } // namespace agent } // namespace cli } // namespace yaze