267 lines
8.7 KiB
C++
267 lines
8.7 KiB
C++
#include <iostream>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <chrono>
|
|
#include <set>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <rapidjson/document.h>
|
|
|
|
#include <CLI/CLI.hpp>
|
|
#include <toml++/toml.hpp>
|
|
|
|
#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;
|
|
}
|