#include #include #include #include #include #include #include #include #include #include #include #include "html/html.h" #include "http/http.h" #include "ipc/ipc.h" #include "logger/logger.h" #include "postgres/postgres.h" #include "json/json.h" #include "cmd_kbot.h" #ifndef PROJECT_VERSION #define PROJECT_VERSION "0.1.0" #endif int main(int argc, char *argv[]) { CLI::App app{"kbot — KBot C++ CLI", "kbot"}; app.set_version_flag("-v,--version", PROJECT_VERSION); std::string log_level = "info"; app.add_option("--log-level", log_level, "Set log level (debug/info/warn/error)")->default_val("info"); // Subcommand: parse HTML std::string html_input; auto *parse_cmd = app.add_subcommand("parse", "Parse HTML and list elements"); parse_cmd->add_option("html", html_input, "HTML string to parse")->required(); // Subcommand: select from HTML std::string select_input; std::string selector; auto *select_cmd = app.add_subcommand("select", "CSS-select elements from HTML"); select_cmd->add_option("html", select_input, "HTML string")->required(); select_cmd->add_option("selector", selector, "CSS selector")->required(); // Subcommand: config — read a TOML file std::string config_path; auto *config_cmd = app.add_subcommand("config", "Read and display a TOML config file"); config_cmd->add_option("file", config_path, "Path to TOML file")->required(); // Subcommand: fetch — HTTP GET a URL std::string fetch_url; auto *fetch_cmd = app.add_subcommand("fetch", "HTTP GET a URL and print the response"); fetch_cmd->add_option("url", fetch_url, "URL to fetch")->required(); // Subcommand: json — prettify JSON std::string json_input; auto *json_cmd = app.add_subcommand("json", "Prettify a JSON string"); json_cmd->add_option("input", json_input, "JSON string")->required(); // Subcommand: db — connect to Supabase and query std::string db_config_path = "config/postgres.toml"; std::string db_table; int db_limit = 10; auto *db_cmd = app.add_subcommand("db", "Connect to Supabase and query a table"); db_cmd->add_option("-c,--config", db_config_path, "TOML config path") ->default_val("config/postgres.toml"); db_cmd->add_option("table", db_table, "Table to query (optional)"); db_cmd->add_option("-l,--limit", db_limit, "Row limit")->default_val(10); // Subcommand: worker — IPC mode (spawned by Node.js orchestrator) std::string uds_path; auto *worker_cmd = app.add_subcommand( "worker", "Run as IPC worker (stdin/stdout length-prefixed JSON)"); worker_cmd->add_option("--uds", uds_path, "Listen on TCP port (Windows) or Unix socket path"); // Subcommand: kbot — AI workflows & task configurations auto* kbot_cmd = polymech::setup_cmd_kbot(app); (void)kbot_cmd; CLI11_PARSE(app, argc, argv); // Worker mode uses stderr for logs to keep stdout clean for IPC frames if (worker_cmd->parsed()) { if (!uds_path.empty()) { logger::init_uds("polymech-uds", log_level, "../logs/uds.json"); } else { logger::init_stderr("polymech-worker", log_level); } } else { logger::init("polymech-cli", log_level); } // ── worker mode ───────────────────────────────────────────────────────── if (worker_cmd->parsed()) { logger::info("Worker mode: listening on stdin"); if (!uds_path.empty()) { logger::info("Worker mode: UDS Server active on " + uds_path); int rc = polymech::run_cmd_kbot_uds(uds_path); return rc; } // Send a "ready" message so the orchestrator knows we're alive ipc::write_message({"0", "ready", "{}"}); while (true) { ipc::Message req; if (!ipc::read_message(req)) { logger::info("Worker: stdin closed, exiting"); break; } logger::debug("Worker recv: type=" + req.type + " id=" + req.id); if (req.type == "ping") { ipc::write_message({req.id, "pong", "{}"}); } else if (req.type == "job") { // Stub: echo the payload back as job_result ipc::write_message({req.id, "job_result", req.payload}); } else if (req.type == "kbot-ai") { logger::info("Worker: kbot-ai job received"); std::string req_id = req.id; polymech::kbot::KBotCallbacks cb; cb.onEvent = [&req_id](const std::string& type, const std::string& json) { if (type == "job_result") { ipc::write_message({req_id, "job_result", json}); } else { ipc::write_message({"0", type, json}); } }; int rc = polymech::run_kbot_ai_ipc(req.payload, req.id, cb); if (rc != 0) { ipc::write_message({req.id, "error", "{\"message\":\"kbot ai pipeline failed\"}"}); } } else if (req.type == "kbot-run") { logger::info("Worker: kbot-run job received"); std::string req_id = req.id; polymech::kbot::KBotCallbacks cb; cb.onEvent = [&req_id](const std::string& type, const std::string& json) { if (type == "job_result") { ipc::write_message({req_id, "job_result", json}); } else { ipc::write_message({"0", type, json}); } }; int rc = polymech::run_kbot_run_ipc(req.payload, req.id, cb); if (rc != 0) { ipc::write_message({req.id, "error", "{\"message\":\"kbot run pipeline failed\"}"}); } } else if (req.type == "shutdown") { ipc::write_message({req.id, "shutdown_ack", "{}"}); logger::info("Worker: shutdown requested, exiting"); break; } else { // Unknown type — respond with error ipc::write_message( {req.id, "error", "{\"message\":\"unknown type: " + req.type + "\"}"}); } } return 0; } // ── existing subcommands ──────────────────────────────────────────────── if (parse_cmd->parsed()) { auto elements = html::parse(html_input); logger::info("Parsed " + std::to_string(elements.size()) + " elements"); for (const auto &el : elements) { std::cout << "<" << el.tag << "> " << el.text << "\n"; } return 0; } if (select_cmd->parsed()) { auto matches = html::select(select_input, selector); logger::info("Matched " + std::to_string(matches.size()) + " elements"); for (const auto &m : matches) { std::cout << m << "\n"; } return 0; } if (config_cmd->parsed()) { try { auto tbl = toml::parse_file(config_path); logger::info("Loaded config: " + config_path); std::cout << tbl << "\n"; } catch (const toml::parse_error &err) { logger::error("TOML parse error: " + std::string(err.what())); return 1; } return 0; } if (fetch_cmd->parsed()) { auto resp = http::get(fetch_url); logger::info("HTTP " + std::to_string(resp.status_code) + " from " + fetch_url); if (json::is_valid(resp.body)) { std::cout << json::prettify(resp.body) << "\n"; } else { std::cout << resp.body << "\n"; } return 0; } if (json_cmd->parsed()) { if (!json::is_valid(json_input)) { logger::error("Invalid JSON input"); return 1; } std::cout << json::prettify(json_input) << "\n"; return 0; } if (db_cmd->parsed()) { try { auto cfg = toml::parse_file(db_config_path); postgres::Config pg_cfg; pg_cfg.supabase_url = cfg["supabase"]["url"].value_or(std::string("")); pg_cfg.supabase_key = cfg["supabase"]["publishable_key"].value_or(std::string("")); postgres::init(pg_cfg); auto status = postgres::ping(); logger::info("Supabase: " + status); if (!db_table.empty()) { auto result = postgres::query(db_table, "*", "", db_limit); if (json::is_valid(result)) { std::cout << json::prettify(result) << "\n"; } else { std::cout << result << "\n"; } } } catch (const std::exception &e) { logger::error(std::string("db error: ") + e.what()); return 1; } return 0; } // ── kbot subcommand ────────────────────────────────────────────────── if (polymech::is_kbot_ai_parsed()) { return polymech::run_cmd_kbot_ai(); } if (polymech::is_kbot_run_parsed()) { return polymech::run_cmd_kbot_run(); } // No subcommand — show help std::cout << app.help() << "\n"; return 0; }