kbot cpp port 1/3

This commit is contained in:
lovebird 2026-03-29 21:19:22 +02:00
parent f0aee9ce27
commit 04e056d395
13 changed files with 828 additions and 1 deletions

View File

@ -29,3 +29,4 @@ Thumbs.db
cache/
config/postgres.toml
dist
src/cmd_grid*.cpp

View File

@ -79,6 +79,7 @@ add_subdirectory(packages/gadm_reader)
add_subdirectory(packages/grid)
add_subdirectory(packages/search)
add_subdirectory(packages/enrichers)
add_subdirectory(packages/kbot)
# Sources
add_executable(${PROJECT_NAME}
@ -87,11 +88,12 @@ add_executable(${PROJECT_NAME}
src/cmd_gridsearch-filters.cpp
src/cmd_gridsearch-uds.cpp
src/cmd_gridsearch-postgres.cpp
src/cmd_kbot.cpp
src/gridsearch_serialize.cpp
src/sys_metrics.cpp
)
target_link_libraries(${PROJECT_NAME} PRIVATE CLI11::CLI11 tomlplusplus::tomlplusplus logger html postgres http json polymech ipc geo gadm_reader grid search enrichers)
target_link_libraries(${PROJECT_NAME} PRIVATE CLI11::CLI11 tomlplusplus::tomlplusplus logger html postgres http json polymech ipc geo gadm_reader grid search enrichers kbot)
target_include_directories(${PROJECT_NAME} PRIVATE
${asio_SOURCE_DIR}/asio/include

View File

@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.20)
project(kbot CXX)
add_library(kbot STATIC
kbot.cpp
)
target_include_directories(kbot PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${taskflow_SOURCE_DIR}
)
target_link_libraries(kbot PUBLIC
logger
json
)

View File

@ -0,0 +1,55 @@
#include "kbot.h"
#include <taskflow/taskflow.hpp>
#include <iostream>
#include "logger/logger.h"
namespace polymech {
namespace kbot {
int run_kbot_ai_pipeline(const KBotOptions& opts, const KBotCallbacks& cb) {
logger::info("Starting kbot ai pipeline (stub)");
if (opts.dry_run) {
logger::info("Dry run triggered for kbot ai");
}
// Scaffolding multithreaded AI tasks
tf::Executor executor(4);
tf::Taskflow taskflow;
taskflow.emplace([opts, cb](){
logger::info("Executing kbot ai tasks via Taskflow -> emit events...");
if (cb.onEvent) {
cb.onEvent("ai_progress", "{\"message\":\"Task stub completed\"}");
}
});
executor.run(taskflow).wait();
if (cb.onEvent) {
cb.onEvent("job_result", "{\"status\":\"success\",\"mode\":\"ai\"}");
}
return 0;
}
int run_kbot_run_pipeline(const KBotRunOptions& opts, const KBotCallbacks& cb) {
logger::info("Starting kbot run pipeline (stub) for config: " + opts.config);
if (opts.dry) {
logger::info("Dry run triggered for kbot run");
}
if (opts.list) {
logger::info("List configs mode enabled");
}
// Stub std::system call execution (simulating child_process.execFileSync from TypeScript)
if (!opts.dry && !opts.list) {
logger::info("Simulating launching: .vscode/launch.json targeting " + opts.config);
}
if (cb.onEvent) {
cb.onEvent("job_result", "{\"status\":\"success\",\"mode\":\"run\"}");
}
return 0;
}
} // namespace kbot
} // namespace polymech

View File

@ -0,0 +1,70 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <atomic>
#include <functional>
namespace polymech {
namespace kbot {
struct KBotOptions {
std::string path = ".";
std::string prompt;
std::string output;
std::string dst;
std::string append = "concat";
std::string wrap = "none";
std::string each;
std::vector<std::string> disable;
std::vector<std::string> disable_tools;
std::vector<std::string> tools;
std::vector<std::string> include_globs;
std::vector<std::string> exclude_globs;
std::string glob_extension;
std::string api_key;
std::string model;
std::string router = "openrouter";
std::string mode = "tools";
int log_level = 4;
std::string profile;
std::string base_url;
std::string config_path;
std::string dump;
std::string preferences;
std::string logs;
bool stream = false;
bool alt = false;
std::string env = "default";
std::string filters;
std::string query;
bool dry_run = false;
std::string format;
// Internal
std::string job_id;
std::shared_ptr<std::atomic<bool>> cancel_token;
};
struct KBotRunOptions {
std::string config = "default";
bool dry = false;
bool list = false;
std::string project_path;
std::string log_file_path;
// Internal
std::string job_id;
std::shared_ptr<std::atomic<bool>> cancel_token;
};
struct KBotCallbacks {
std::function<void(const std::string& type, const std::string& json)> onEvent;
};
int run_kbot_ai_pipeline(const KBotOptions& opts, const KBotCallbacks& cb);
int run_kbot_run_pipeline(const KBotRunOptions& opts, const KBotCallbacks& cb);
} // namespace kbot
} // namespace polymech

View File

@ -0,0 +1,93 @@
#include "cmd_kbot.h"
#include "logger/logger.h"
#include <CLI/CLI.hpp>
#include <rapidjson/document.h>
#include <iostream>
namespace polymech {
// Global states for CLI mode
static kbot::KBotOptions g_kbot_opts;
static kbot::KBotRunOptions g_run_opts;
static CLI::App* ai_cmd = nullptr;
static CLI::App* run_cmd = nullptr;
CLI::App* setup_cmd_kbot(CLI::App& app) {
auto* kbot_cmd = app.add_subcommand("kbot", "KBot AI & Task runner");
// ── AI Subcommand ──
ai_cmd = kbot_cmd->add_subcommand("ai", "Run KBot AI tasks");
ai_cmd->add_option("-p,--path", g_kbot_opts.path, "Target directory")->default_val(".");
ai_cmd->add_option("--prompt", g_kbot_opts.prompt, "The prompt. Supports file paths and vars.");
ai_cmd->add_option("--output", g_kbot_opts.output, "Optional output path for modified files");
ai_cmd->add_option("--dst", g_kbot_opts.dst, "Optional destination path for the result");
ai_cmd->add_option("--append", g_kbot_opts.append, "How to handle output if --dst exists: concat|merge|replace")->default_val("concat");
ai_cmd->add_option("--wrap", g_kbot_opts.wrap, "Specify how to wrap output: meta|none")->default_val("none");
ai_cmd->add_option("--each", g_kbot_opts.each, "Iterate over items (GLOB, JSON, array)");
ai_cmd->add_option("--disable", g_kbot_opts.disable, "Disable tools categories");
ai_cmd->add_option("--disableTools", g_kbot_opts.disable_tools, "Specific tools to disable");
ai_cmd->add_option("--tools", g_kbot_opts.tools, "Tools to use");
ai_cmd->add_option("--include", g_kbot_opts.include_globs, "Glob patterns or paths to include");
ai_cmd->add_option("--exclude", g_kbot_opts.exclude_globs, "Glob patterns or paths to exclude");
ai_cmd->add_option("--globExtension", g_kbot_opts.glob_extension, "Glob extension behavior (e.g. match-cpp)");
ai_cmd->add_option("--api_key", g_kbot_opts.api_key, "Explicit API key to use");
ai_cmd->add_option("--model", g_kbot_opts.model, "AI model to use");
ai_cmd->add_option("--router", g_kbot_opts.router, "Router to use (openai, openrouter, deepseek)")->default_val("openrouter");
ai_cmd->add_option("--mode", g_kbot_opts.mode, "Chat completion mode")->default_val("tools");
ai_cmd->add_flag("--dry", g_kbot_opts.dry_run, "Dry run");
// ── Run Subcommand ──
run_cmd = kbot_cmd->add_subcommand("run", "Run a .vscode/launch.json configuration");
run_cmd->add_option("-c,--config", g_run_opts.config, "Config name")->default_val("default");
run_cmd->add_flag("--dry", g_run_opts.dry, "Dry run");
run_cmd->add_flag("--list", g_run_opts.list, "List available configs");
run_cmd->add_option("--projectPath", g_run_opts.project_path, "Project path")->default_val(".");
run_cmd->add_option("--logFilePath", g_run_opts.log_file_path, "Log file path");
return kbot_cmd;
}
bool is_kbot_ai_parsed() { return ai_cmd != nullptr && ai_cmd->parsed(); }
bool is_kbot_run_parsed() { return run_cmd != nullptr && run_cmd->parsed(); }
int run_cmd_kbot_ai() {
return kbot::run_kbot_ai_pipeline(g_kbot_opts, kbot::KBotCallbacks{});
}
int run_cmd_kbot_run() {
return kbot::run_kbot_run_pipeline(g_run_opts, kbot::KBotCallbacks{});
}
int run_kbot_ai_ipc(const std::string& payload, const std::string& jobId, const kbot::KBotCallbacks& cb) {
kbot::KBotOptions opts;
opts.job_id = jobId;
// Optional: Parse JSON from payload to overwrite opts variables using rapidjson
rapidjson::Document doc;
doc.Parse(payload.c_str());
if (!doc.HasParseError() && doc.IsObject()) {
if (doc.HasMember("prompt") && doc["prompt"].IsString()) opts.prompt = doc["prompt"].GetString();
if (doc.HasMember("dry_run") && doc["dry_run"].IsBool()) opts.dry_run = doc["dry_run"].GetBool();
}
logger::info("Receiving AI task over IPC... job: " + jobId);
return kbot::run_kbot_ai_pipeline(opts, cb);
}
int run_kbot_run_ipc(const std::string& payload, const std::string& jobId, const kbot::KBotCallbacks& cb) {
kbot::KBotRunOptions opts;
opts.job_id = jobId;
rapidjson::Document doc;
doc.Parse(payload.c_str());
if (!doc.HasParseError() && doc.IsObject()) {
if (doc.HasMember("config") && doc["config"].IsString()) opts.config = doc["config"].GetString();
if (doc.HasMember("dry") && doc["dry"].IsBool()) opts.dry = doc["dry"].GetBool();
}
logger::info("Receiving run task over IPC... job: " + jobId);
return kbot::run_kbot_run_pipeline(opts, cb);
}
} // namespace polymech

View File

@ -0,0 +1,25 @@
#pragma once
#include <CLI/CLI.hpp>
#include <string>
#include <functional>
#include "kbot.h"
namespace polymech {
/// Attach kbot subcommands to the main app
CLI::App* setup_cmd_kbot(CLI::App& app);
/// CLI Entry points
int run_cmd_kbot_ai();
int run_cmd_kbot_run();
/// IPC / UDS Entry points
int run_kbot_ai_ipc(const std::string& payload, const std::string& jobId, const kbot::KBotCallbacks& cb);
int run_kbot_run_ipc(const std::string& payload, const std::string& jobId, const kbot::KBotCallbacks& cb);
/// Helper to check parsed state
bool is_kbot_ai_parsed();
bool is_kbot_run_parsed();
} // namespace polymech

View File

@ -0,0 +1,351 @@
#include "gridsearch_serialize.h"
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include "cmd_gridsearch.h"
namespace polymech::serialize {
// ── grid-ready ──────────────────────────────────────────────────────────────
std::string grid_ready(const std::vector<grid::Waypoint>& waypoints) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("areas"); w.StartArray();
for (size_t i = 0; i < waypoints.size(); ++i) {
const auto& wp = waypoints[i];
w.StartObject();
w.Key("name"); w.String(("Waypoint " + std::to_string(wp.step)).c_str());
w.Key("gid"); w.String(("wp-" + std::to_string(wp.step)).c_str());
w.Key("lat"); w.Double(wp.lat);
w.Key("lon"); w.Double(wp.lng);
w.Key("radius_km"); w.Double(wp.radius_km);
w.Key("area_gid"); w.String(wp.area_gid.c_str());
w.Key("area_name"); w.String(wp.area_name.c_str());
w.Key("index"); w.Int(static_cast<int>(i));
w.EndObject();
}
w.EndArray();
w.Key("total"); w.Int(static_cast<int>(waypoints.size()));
w.EndObject();
return sb.GetString();
}
// ── waypoint-start ──────────────────────────────────────────────────────────
std::string waypoint_start(const grid::Waypoint& wp, int index, int total) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("name"); w.String(("Waypoint " + std::to_string(wp.step)).c_str());
w.Key("gid"); w.String(("wp-" + std::to_string(wp.step)).c_str());
w.Key("lat"); w.Double(wp.lat);
w.Key("lon"); w.Double(wp.lng);
w.Key("radius_km"); w.Double(wp.radius_km);
w.Key("area_gid"); w.String(wp.area_gid.c_str());
w.Key("area_name"); w.String(wp.area_name.c_str());
w.Key("index"); w.Int(index);
w.Key("total"); w.Int(total);
w.EndObject();
return sb.GetString();
}
// ── location (per search result) ────────────────────────────────────────────
std::string location(const search::MapResult& r, int step) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("location"); w.StartObject();
w.Key("title"); w.String(r.title.c_str());
w.Key("place_id"); w.String(r.place_id.c_str());
w.Key("address"); w.String(r.address.c_str());
w.Key("website"); w.String(r.website.c_str());
w.Key("type"); w.String(r.type.c_str());
w.Key("phone"); w.String(r.phone.c_str());
w.Key("rating"); w.Double(r.rating);
w.Key("reviews"); w.Int(r.reviews);
w.Key("lat"); w.Double(r.gps.lat);
w.Key("lng"); w.Double(r.gps.lng);
w.Key("types"); w.StartArray();
for (const auto& t : r.types) w.String(t.c_str());
w.EndArray();
w.EndObject();
w.Key("areaName"); w.String(("Waypoint " + std::to_string(step)).c_str());
w.EndObject();
return sb.GetString();
}
// ── area_start ──────────────────────────────────────────────────────────────
std::string area_start(const std::string& area_gid, const std::string& area_name) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("gid"); w.String(area_gid.c_str());
w.Key("name"); w.String(area_name.c_str());
w.EndObject();
return sb.GetString();
}
// ── area_finish ─────────────────────────────────────────────────────────────
std::string area_finish(const std::string& area_gid) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("gid"); w.String(area_gid.c_str());
w.EndObject();
return sb.GetString();
}
// ── waypoint_finish ─────────────────────────────────────────────────────────
std::string waypoint_finish(const grid::Waypoint& wp, int results, int apiCalls) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("name"); w.String(("Waypoint " + std::to_string(wp.step)).c_str());
w.Key("gid"); w.String(("wp-" + std::to_string(wp.step)).c_str());
w.Key("results"); w.Int(results);
w.Key("apiCalls"); w.Int(apiCalls);
w.EndObject();
return sb.GetString();
}
// ── enrich-start ────────────────────────────────────────────────────────────
std::string enrich_start(int locationCount) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("locationCount"); w.Int(locationCount);
w.EndObject();
return sb.GetString();
}
// ── nodePage (per page error) ───────────────────────────────────────────────
std::string node_page(const enrichers::PageError& pe, const std::string& placeId) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("location"); w.String(placeId.c_str());
w.Key("url"); w.String(pe.url.c_str());
w.Key("status"); w.String(pe.status.c_str());
w.Key("error"); w.String(pe.error.c_str());
w.EndObject();
return sb.GetString();
}
// ── node-error ──────────────────────────────────────────────────────────────
std::string node_error(const enrichers::EnrichedNode& node) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("node"); w.StartObject();
w.Key("title"); w.String(node.title.c_str());
w.Key("placeId"); w.String(node.place_id.c_str());
w.EndObject();
w.Key("error"); w.String(node.error.c_str());
w.EndObject();
return sb.GetString();
}
// ── node (enriched location) ────────────────────────────────────────────────
std::string node(const enrichers::EnrichedNode& n) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("idx"); w.Int(n.idx);
w.Key("title"); w.String(n.title.c_str());
w.Key("placeId"); w.String(n.place_id.c_str());
w.Key("website"); w.String(n.website.c_str());
w.Key("address"); w.String(n.address.c_str());
w.Key("type"); w.String(n.type.c_str());
w.Key("status"); w.String(enrichers::status_string(n.status));
w.Key("emails"); w.StartArray();
for (const auto& e : n.emails) w.String(e.c_str());
w.EndArray();
w.Key("social"); w.StartArray();
for (const auto& s : n.socials) {
w.StartObject();
w.Key("url"); w.String(s.url.c_str());
w.Key("platform"); w.String(s.platform.c_str());
w.EndObject();
}
w.EndArray();
w.Key("sites"); w.StartArray();
for (const auto& s : n.sites) {
w.StartObject();
w.Key("url"); w.String(s.first.c_str());
w.Key("name"); w.String("home");
w.Key("content"); w.String(s.second.c_str());
w.EndObject();
}
w.EndArray();
w.Key("pagesFound"); w.Int(n.pages_found);
w.Key("pagesScraped"); w.Int(n.pages_scraped);
w.Key("metaMs"); w.Int(n.meta_ms);
w.Key("emailMs"); w.Int(n.email_ms);
w.Key("totalMs"); w.Int(n.total_ms);
w.Key("gridArea"); w.String(n.grid_area.c_str());
w.Key("gridGid"); w.String(n.grid_gid.c_str());
w.EndObject();
return sb.GetString();
}
// ── write_options helper ────────────────────────────────────────────────────
static void write_options(rapidjson::Writer<rapidjson::StringBuffer>& w, const polymech::PipelineOptions& opts) {
w.Key("options");
w.StartObject();
w.Key("jobId"); w.String(opts.job_id.c_str());
w.Key("searchQuery"); w.String(opts.search_query.c_str());
w.Key("searchDomain"); w.String(opts.search_domain.c_str());
w.Key("searchLanguage"); w.String(opts.search_language.c_str());
w.Key("searchCountry"); w.String(opts.search_country.c_str());
w.Key("searchLimit"); w.Int(opts.search_limit);
w.Key("searchZoom"); w.Int(opts.search_zoom);
w.Key("dryRun"); w.Bool(opts.dry_run);
w.Key("enrich"); w.Bool(opts.enrich);
w.Key("grid");
w.StartObject();
w.Key("gridMode"); w.String(opts.grid_opts.gridMode.c_str());
w.Key("cellSize"); w.Double(opts.grid_opts.cellSize);
w.Key("cellOverlap"); w.Double(opts.grid_opts.cellOverlap);
w.Key("centroidOverlap"); w.Double(opts.grid_opts.centroidOverlap);
w.Key("maxCellsLimit"); w.Int(opts.grid_opts.maxCellsLimit);
w.Key("maxElevation"); w.Double(opts.grid_opts.maxElevation);
w.Key("minDensity"); w.Double(opts.grid_opts.minDensity);
w.Key("minGhsPop"); w.Double(opts.grid_opts.minGhsPop);
w.Key("minGhsBuilt"); w.Double(opts.grid_opts.minGhsBuilt);
w.Key("ghsFilterMode"); w.String(opts.grid_opts.ghsFilterMode.c_str());
w.Key("allowMissingGhs"); w.Bool(opts.grid_opts.allowMissingGhs);
w.Key("bypassFilters"); w.Bool(opts.grid_opts.bypassFilters);
w.Key("pathOrder"); w.String(opts.grid_opts.pathOrder.c_str());
w.Key("groupByRegion"); w.Bool(opts.grid_opts.groupByRegion);
w.EndObject();
w.Key("areas");
w.StartArray();
for (const auto& a : opts.areas) {
w.StartObject();
w.Key("gid"); w.String(a.gid.c_str());
w.Key("name"); w.String(a.name.c_str());
w.Key("level"); w.Int(a.level);
w.EndObject();
}
w.EndArray();
w.EndObject();
}
// ── job_result (with enrichment) ────────────────────────────────────────────
std::string job_result(const polymech::PipelineOptions& opts, int64_t enumMs, int64_t searchMs, int64_t enrichMs, int64_t totalMs,
int totalEmails, int totalPagesScraped, int freshApiCalls,
int waypointCount, int validCells, int skippedCells,
int totalResults, const std::vector<std::string>& enrichResults,
double totalScannedSqKm, double totalPopulation) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
write_options(w, opts);
w.Key("enumMs"); w.Int64(enumMs);
w.Key("searchMs"); w.Int64(searchMs);
w.Key("enrichMs"); w.Int64(enrichMs);
w.Key("totalMs"); w.Int64(totalMs);
w.Key("gridStats");
w.StartObject();
w.Key("validCells"); w.Int(validCells);
w.Key("skippedCells"); w.Int(skippedCells);
w.Key("totalWaypoints"); w.Int(waypointCount);
w.EndObject();
w.Key("searchStats");
w.StartObject();
w.Key("apiCalls"); w.Int(freshApiCalls);
w.Key("filtered"); w.Int(0); // placeholder if needed
w.Key("areaCount"); w.Int(waypointCount);
w.Key("totalResults"); w.Int(totalResults);
w.Key("totalScannedSqKm"); w.Double(totalScannedSqKm);
w.Key("totalPopulation"); w.Double(totalPopulation);
w.EndObject();
w.Key("totalEmails"); w.Int(totalEmails);
w.Key("enrichResults");
w.StartArray();
for (const auto& id : enrichResults) {
w.String(id.c_str());
}
w.EndArray();
w.Key("freshApiCalls"); w.Int(freshApiCalls);
w.Key("waypointCount"); w.Int(waypointCount);
w.Key("totalPagesScraped"); w.Int(totalPagesScraped);
w.EndObject();
return sb.GetString();
}
// ── job_result (search only) ────────────────────────────────────────────────
std::string job_result_search_only(const polymech::PipelineOptions& opts, int64_t enumMs, int64_t searchMs, int64_t totalMs,
int freshApiCalls, int waypointCount, int validCells,
int skippedCells, int totalResults, const std::vector<std::string>& enrichResults,
double totalScannedSqKm, double totalPopulation) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
write_options(w, opts);
w.Key("enumMs"); w.Int64(enumMs);
w.Key("searchMs"); w.Int64(searchMs);
w.Key("enrichMs"); w.Int64(0);
w.Key("totalMs"); w.Int64(totalMs);
w.Key("gridStats");
w.StartObject();
w.Key("validCells"); w.Int(validCells);
w.Key("skippedCells"); w.Int(skippedCells);
w.Key("totalWaypoints"); w.Int(waypointCount);
w.EndObject();
w.Key("searchStats");
w.StartObject();
w.Key("apiCalls"); w.Int(freshApiCalls);
w.Key("filtered"); w.Int(0);
w.Key("areaCount"); w.Int(waypointCount);
w.Key("totalResults"); w.Int(totalResults);
w.Key("totalScannedSqKm"); w.Double(totalScannedSqKm);
w.Key("totalPopulation"); w.Double(totalPopulation);
w.EndObject();
w.Key("totalEmails"); w.Int(0);
w.Key("enrichResults");
w.StartArray();
for (const auto& id : enrichResults) {
w.String(id.c_str());
}
w.EndArray();
w.Key("freshApiCalls"); w.Int(freshApiCalls);
w.Key("waypointCount"); w.Int(waypointCount);
w.Key("totalPagesScraped"); w.Int(0);
w.EndObject();
return sb.GetString();
}
} // namespace polymech::serialize

View File

@ -22,6 +22,7 @@
#include "search/search.h"
#include "enrichers/enrichers.h"
#include "cmd_gridsearch.h"
#include "cmd_kbot.h"
#ifndef PROJECT_VERSION
#define PROJECT_VERSION "0.1.0"
@ -91,6 +92,9 @@ int main(int argc, char *argv[]) {
// Subcommand: gridsearch — Run a full gridsearch pipeline
auto* gs_cmd = polymech::setup_cmd_gridsearch(app);
// Subcommand: kbot — AI workflows & task configurations
auto* kbot_cmd = polymech::setup_cmd_kbot(app);
CLI11_PARSE(app, argc, argv);
// Worker mode uses stderr for logs to keep stdout clean for IPC frames
@ -163,6 +167,38 @@ int main(int argc, char *argv[]) {
// 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");
@ -263,6 +299,14 @@ int main(int argc, char *argv[]) {
return polymech::run_cmd_gridsearch();
}
// ── 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;

View File

@ -0,0 +1,36 @@
#include "sys_metrics.h"
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#include <psapi.h>
#pragma comment(lib, "psapi.lib")
namespace polymech {
size_t get_current_rss_mb() {
PROCESS_MEMORY_COUNTERS info;
if (GetProcessMemoryInfo(GetCurrentProcess(), &info, sizeof(info))) {
return (size_t)(info.WorkingSetSize) / (1024 * 1024);
}
return 0;
}
uint64_t get_cpu_time_ms() {
FILETIME creationTime, exitTime, kernelTime, userTime;
if (GetProcessTimes(GetCurrentProcess(), &creationTime, &exitTime, &kernelTime, &userTime)) {
ULARGE_INTEGER kernel, user;
kernel.LowPart = kernelTime.dwLowDateTime;
kernel.HighPart = kernelTime.dwHighDateTime;
user.LowPart = userTime.dwLowDateTime;
user.HighPart = userTime.dwHighDateTime;
return (kernel.QuadPart + user.QuadPart) / 10000;
}
return 0;
}
}
#else
namespace polymech {
size_t get_current_rss_mb() { return 0; }
uint64_t get_cpu_time_ms() { return 0; }
}
#endif

View File

@ -44,6 +44,10 @@ add_executable(test_polymech unit/test_polymech.cpp)
target_link_libraries(test_polymech PRIVATE Catch2::Catch2WithMain polymech postgres Threads::Threads)
catch_discover_tests(test_polymech)
add_executable(test_cmd_kbot unit/test_cmd_kbot.cpp ../src/cmd_kbot.cpp)
target_link_libraries(test_cmd_kbot PRIVATE Catch2::Catch2WithMain CLI11::CLI11 logger tomlplusplus::tomlplusplus kbot Threads::Threads)
catch_discover_tests(test_cmd_kbot)
# E2E test polymech fetch_pages from live Supabase
add_executable(test_polymech_e2e e2e/test_polymech_e2e.cpp)
target_link_libraries(test_polymech_e2e PRIVATE Catch2::Catch2WithMain tomlplusplus::tomlplusplus logger postgres polymech json Threads::Threads)

View File

@ -0,0 +1,60 @@
#include <catch2/catch_test_macros.hpp>
#include <CLI/CLI.hpp>
#include "../../src/cmd_kbot.h"
using namespace polymech;
TEST_CASE("KBot CLI AI Command Parsing", "[kbot]") {
CLI::App app{"polymech-cli"};
auto* kbot_cmd = setup_cmd_kbot(app);
REQUIRE(kbot_cmd != nullptr);
SECTION("Default values for kbot ai") {
int argc = 3;
const char* argv[] = {"polymech-cli", "kbot", "ai"};
REQUIRE_NOTHROW(app.parse(argc, argv));
REQUIRE(is_kbot_ai_parsed() == true);
REQUIRE(is_kbot_run_parsed() == false);
// We can't access g_kbot_opts easily since it's static in cmd_kbot.cpp,
// but testing that it doesn't throw is a good start.
// In a real app we might pass options structs around instead of globals.
}
SECTION("Arguments for kbot ai") {
int argc = 7;
const char* argv[] = {
"polymech-cli", "kbot", "ai",
"--prompt", "hello world",
"--mode", "chat"
};
REQUIRE_NOTHROW(app.parse(argc, argv));
REQUIRE(is_kbot_ai_parsed() == true);
}
}
TEST_CASE("KBot CLI Run Command Parsing", "[kbot]") {
CLI::App app{"polymech-cli"};
auto* kbot_cmd = setup_cmd_kbot(app);
REQUIRE(kbot_cmd != nullptr);
SECTION("Default values for kbot run") {
int argc = 3;
const char* argv[] = {"polymech-cli", "kbot", "run"};
REQUIRE_NOTHROW(app.parse(argc, argv));
REQUIRE(is_kbot_run_parsed() == true);
REQUIRE(is_kbot_ai_parsed() == false);
}
SECTION("Arguments for kbot run") {
int argc = 6;
const char* argv[] = {
"polymech-cli", "kbot", "run",
"-c", "frontend-dev",
"--dry"
};
REQUIRE_NOTHROW(app.parse(argc, argv));
REQUIRE(is_kbot_run_parsed() == true);
}
}

View File

@ -0,0 +1,69 @@
# KBot C++ Port Plan (WIP Scaffolding)
This document outlines the scaffolding steps to port the TypeScript `kbot` implementation (both AI tools and the project runner) over to the C++ `polymech-cli` application.
## 1. CLI Scaffolding (`main.cpp` & `cmd_kbot`)
The C++ port will introduce a new `kbot` subcommand tree loosely mimicking the existing TypeScript entry points (`zod_schema.ts`).
- **Target Files**:
- `src/main.cpp` (Register the command)
- `src/cmd_kbot.h` (Declarations & Options Structs)
- `src/cmd_kbot.cpp` (Implementation)
### Subcommand `ai`
This command replaces the standard `OptionsSchema` from `zod_schema.ts`.
- Using `CLI::App* ai_cmd = kbot_cmd->add_subcommand("ai", "Run KBot AI workflows");`
- **Arguments to Map** via `CLI11`:
- `--path` (default `.`)
- `--prompt` (string)
- `--output` (string)
- `--dst` (string)
- `--append` (enum: `concat`, `merge`, `replace`)
- `--wrap` (enum: `meta`, `none`)
- `--each` (Glob pattern / list / JSON)
- `--disable`, `--disableTools`, `--tools`
- `--include`, `--exclude`, `--globExtension`
- `--model`, `--router`, `--mode` (enum: `completion`, `tools`, `assistant`, `responses`, `custom`)
- Flags: `--stream`, `--dry`, `--alt`
- Advanced: `--baseURL`, `--config`, `--dump`, `--preferences`, `--logs`, `--env`
### Subcommand `run`
This command replaces `commons/src/lib/run.ts` which spawns debug configurations.
- Using `CLI::App* run_cmd = kbot_cmd->add_subcommand("run", "Run a launch.json configuration");`
- **Arguments to Map**:
- `--config` (default `default`)
- `--dry` (flag)
- `--list` (flag)
- `--projectPath` (default `process.cwd()`)
- `--logFilePath` (default `log-configuration.json`)
## 2. Multithreading & Execution Pattern
Referencing `cmd_gridsearch.h`, the port will leverage `tf::Taskflow` and `tf::Executor` along with `moodycamel::ConcurrentQueue` for processing parallel tasks (like running a prompt against multiple items via `--each`).
- **Architecture Details**:
1. **Config Loading**: Read preferences/configs (using `tomlplusplus` or `rapidjson`).
2. **Globbing / Resolution**: Resolve paths using `--include`/`--exclude`/`--each`.
3. **Task Queueing**: For every item resolved by `--each`, queue a task.
4. **Task Execution (Stubbed)**: The concurrent thread handles creating the LLM request.
5. **Streaming / Output**: Results stream back (or are written to `--dst`), potentially emitting events over an IPC channel or to `stdout` depending on daemon mode setups.
## 3. Testing Setup
We'll replicate the testing approach found in `tests/` utilizing `Catch2` for BDD/TDD styled tests.
- **Target Files**:
- `tests/test_cmd_kbot.cpp`
- `tests/test_kbot_run.cpp`
- **Cases to cover**:
- Validation of CLI argument defaults against `zod_schema.ts`.
- Behavior of `kbot run --list` correctly interpreting a mock `.vscode/launch.json`.
- Dry run of the `--each` pipeline ensuring tasks get initialized properly.
## Next Steps (Scaffolding Phase)
1. Add `cmd_kbot.h/cpp` with the CLI schema variables.
2. Hook up the subcommands in `main.cpp`.
3. Stub the execution functions (`run_cmd_kbot_ai` and `run_cmd_kbot_run`) just to print out the parsed JSON representing the state.
4. Add the targets to `CMakeLists.txt` and verify the build passes.
5. Create initial Catch2 tests just to ensure the flags parse correctly without crashing.