diff --git a/taicpp/Makefile b/taicpp/Makefile index 777b582..02fa868 100644 --- a/taicpp/Makefile +++ b/taicpp/Makefile @@ -1,7 +1,7 @@ CXX ?= g++ CXXSTD = -std=c++23 WARN = -Wall -Wextra -CXXFLAGS ?= $(CXXSTD) $(WARN) -O2 +CXXFLAGS ?= $(CXXSTD) $(WARN) -Werror -O2 BUILD = build VERSION = $(shell cat VERSION) BIN = $(BUILD)/detz @@ -31,11 +31,13 @@ $(BIN): $(OBJS) test: $(BIN) $(BUILD)/detz_test ./$(BUILD)/detz_test + ./$(BUILD)/cli_test ./$(BIN) --version -$(BUILD)/detz_test: tests/test_main.cpp src/cli/cli.cpp $(VERSION_HPP) +$(BUILD)/detz_test: tests/test_main.cpp tests/cli_test.cpp src/cli/cli.cpp $(VERSION_HPP) @mkdir -p $(BUILD) - $(CXX) $(CXXFLAGS) -I$(GEN_INC) -Isrc/cli tests/test_main.cpp src/cli/cli.cpp -o $@ + $(CXX) $(CXXFLAGS) -Werror -I$(GEN_INC) -Isrc/cli tests/test_main.cpp src/cli/cli.cpp -o $@ + $(CXX) $(CXXFLAGS) -Werror -I$(GEN_INC) -Isrc/cli tests/cli_test.cpp src/cli/cli.cpp -o $(BUILD)/cli_test lint: $(CXX) $(CXXSTD) $(WARN) -Werror -fsyntax-only main.cpp -I$(GEN_INC) -Isrc/cli || true diff --git a/taicpp/src/agent/agent.cpp b/taicpp/src/agent/agent.cpp new file mode 100644 index 0000000..0c0f966 --- /dev/null +++ b/taicpp/src/agent/agent.cpp @@ -0,0 +1,10 @@ +#include "agent.h" +namespace detz::agent { +void ToolRegistry::add(ToolDef d){tools_[d.name]=d;} bool ToolRegistry::has(const std::string& n) const {return tools_.count(n)>0;} +std::vector ToolRegistry::names() const {std::vector o; for(auto&kv:tools_)o.push_back(kv.first); return o;} +Approval::Decision Approval::check(const std::string&) const {return Decision::Allow;} +GoalVerdict GoalVerifier::verify(const std::string& g,const std::string& o) const {return {!g.empty()&&!o.empty(),o.empty()?"empty outcome":""};} +std::vector RetrievalEval::score(const std::string&,const std::vector& docs) const {std::vector o; for(auto&d:docs)o.push_back({0.0,d}); return o;} +void RagStats::record(bool h){d_.queries++; if(h)d_.hits++;} RagStatsData RagStats::data() const {return d_;} +std::string Agent::run(const std::string& g){return "agent:"+g;} ToolRegistry& Agent::registry(){return reg_;} +} // namespace detz::agent diff --git a/taicpp/src/agent/agent.h b/taicpp/src/agent/agent.h new file mode 100644 index 0000000..d180136 --- /dev/null +++ b/taicpp/src/agent/agent.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include +#include +#include +#include +namespace detz::agent { +struct ToolDef { std::string name, desc; }; +class ToolRegistry { public: void add(ToolDef d); bool has(const std::string& n) const; std::vector names() const; private: std::map tools_; }; +class Approval { public: enum class Decision { Allow, Deny, Ask }; Decision check(const std::string& tool) const; }; +struct GoalVerdict { bool ok=false; std::string reason; }; +class GoalVerifier { public: GoalVerdict verify(const std::string& goal, const std::string& outcome) const; }; +struct RetrievalScore { double score=0; std::string doc; }; +class RetrievalEval { public: std::vector score(const std::string& q, const std::vector& docs) const; }; +struct RagStatsData { std::size_t queries=0, hits=0; }; +class RagStats { public: void record(bool hit); RagStatsData data() const; private: RagStatsData d_; }; +class Agent { public: explicit Agent(ToolRegistry r): reg_(r) {} std::string run(const std::string& goal); ToolRegistry& registry(); private: ToolRegistry reg_; }; +} // namespace detz::agent diff --git a/taicpp/src/agent/agent.mk b/taicpp/src/agent/agent.mk new file mode 100644 index 0000000..0fa9b2a --- /dev/null +++ b/taicpp/src/agent/agent.mk @@ -0,0 +1 @@ +SRCS += src/agent/agent.cpp diff --git a/taicpp/src/cap/cap.cpp b/taicpp/src/cap/cap.cpp new file mode 100644 index 0000000..5f66536 --- /dev/null +++ b/taicpp/src/cap/cap.cpp @@ -0,0 +1,5 @@ +#include "cap.h" +namespace detz::cap { +void CapOrchestrator::add(Cap c){caps_[c.name]=c;} bool CapOrchestrator::has(const std::string& n) const {return caps_.count(n)>0;} +std::string CapOrchestrator::run(const std::string& n,const std::string& a) const {return n+":"+a;} +} // namespace detz::cap diff --git a/taicpp/src/cap/cap.h b/taicpp/src/cap/cap.h new file mode 100644 index 0000000..44efc62 --- /dev/null +++ b/taicpp/src/cap/cap.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include +#include +#include +#include +namespace detz::cap { +struct Cap { std::string name; std::string desc; }; +class CapOrchestrator { public: void add(Cap c); bool has(const std::string& n) const; std::string run(const std::string& n, const std::string& arg) const; private: std::map caps_; }; +} // namespace detz::cap diff --git a/taicpp/src/cap/cap.mk b/taicpp/src/cap/cap.mk new file mode 100644 index 0000000..be07c86 --- /dev/null +++ b/taicpp/src/cap/cap.mk @@ -0,0 +1 @@ +SRCS += src/cap/cap.cpp diff --git a/taicpp/src/cli/cli.cpp b/taicpp/src/cli/cli.cpp index 0b54d7e..86ddb36 100644 --- a/taicpp/src/cli/cli.cpp +++ b/taicpp/src/cli/cli.cpp @@ -1,17 +1,77 @@ #include "cli.h" #include "version.hpp" #include +#include namespace detz::cli { +std::string Parser::helpText() { + return std::string("usage: tai [options] [prompt...]\n\noptions:\n" + " --profile P --yes --yolo --auto --continue --debug [SINK] --debug-format FORMAT\n" + " --env E --no-opencode --update-models --eval-retrieval --stats-rag --version\n" + " --cap-manifest --cap-speak --cap-cwd DIR --cap-model MODEL\n" + " --cap-permission-mode MODE --cap-max-turns N --cap COMMAND...\n" + " --serve-mcp KEY --mcp-port PORT --mcp-bind HOST\n" + " --telegram --scheduler --install --install-telegram --uninstall-telegram\n" + " --plexer-wrap --mplex COMMAND... --demo --demo-shot --tui --tui-local --theme NAME\n" + " --help (-h)\n version ") + std::string(kVersion.data()) + "\n"; +} +Options Parser::parseOpts(int argc, char** argv) { + Options o; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + auto nxt = [&](std::optional& f) { if (i+1 < argc) f = argv[++i]; }; + if (a=="--profile") nxt(o.profile); + else if (a=="--yes") o.yes = true; + else if (a=="--yolo") o.yolo = true; + else if (a=="--auto") o.auto_ = true; + else if (a=="--continue") o.resume = true; + else if (a=="--debug") { o.debug = (i+1{}; while (++ipush_back(argv[i]); break; } + else if (a=="--serve-mcp") nxt(o.serve_mcp); + else if (a=="--mcp-port") { if (i+1{}; while (++ipush_back(argv[i]); break; } + else if (a=="--demo") o.demo = true; + else if (a=="--demo-shot") o.demo_shot = true; + else if (a=="--tui") o.tui = true; + else if (a=="--tui-local") o.tui_local = true; + else if (a=="--theme") nxt(o.theme); + else if (a=="--help"||a=="-h") o.help = true; + else if (!a.empty() && a[0]=='-') { /* unknown flag: ignore */ } + else o.prompt.push_back(a); + } + return o; +} Command Parser::parse(int argc, char** argv) { - Command c; - if (argc > 1) { c.name = argv[1]; for (int i = 2; i < argc; ++i) c.args.emplace_back(argv[i]); } + Command c; c.opts = parseOpts(argc, argv); + if (c.opts.help || argc<=1) c.name = "--help"; + else if (c.opts.version) c.name = "--version"; + else c.name = "run"; + for (int i=1;i #include #include namespace detz::cli { -struct Command { std::string name{"--help"}; std::vector args; int run(); }; -class Parser { public: Command parse(int argc, char** argv); }; +struct Options { + std::optional profile; + bool yes = false, yolo = false, auto_ = false, resume = false; + std::optional debug; // file|stdout|stderr + std::optional debug_format; + std::optional env; + bool no_opencode = false, update_models = false, eval_retrieval = false, stats_rag = false; + bool version = false, cap_manifest = false, cap_speak = false; + std::optional cap_cwd, cap_model, cap_permission_mode; + std::optional cap_max_turns; + std::optional> cap; // REMAINDER + std::optional serve_mcp; + std::optional mcp_port; + std::optional mcp_bind; + bool telegram = false, scheduler = false, install = false; + bool install_telegram = false, uninstall_telegram = false, plexer_wrap = false; + std::optional> mplex; // REMAINDER + bool demo = false, demo_shot = false, tui = false, tui_local = false; + std::optional theme; + bool help = false; + std::vector prompt; +}; +struct Command { std::string name{"--help"}; std::vector args; Options opts; int run(); }; +class Parser { public: Options parseOpts(int argc, char** argv); Command parse(int argc, char** argv); static std::string helpText(); }; class DetzCli { public: int run(int argc, char** argv); }; -// molo-subcommand adapter stub (M1 wires `molo detz` registration here). class MoloSubcommand { public: const char* name() const { return "detz"; } }; -} // namespace detz::cli +} // namespace detz::cli diff --git a/taicpp/src/comms/comms.cpp b/taicpp/src/comms/comms.cpp new file mode 100644 index 0000000..d1c0cee --- /dev/null +++ b/taicpp/src/comms/comms.cpp @@ -0,0 +1,6 @@ +#include "comms.h" +namespace detz::comms { +bool Telegram::send(const std::string&,const std::string&) const {return !tok_.empty();} +bool Installer::install(const std::string&) const {return true;} +SysInfoData SysInfo::collect() const {return {"linux","x86_64"};} +} // namespace detz::comms diff --git a/taicpp/src/comms/comms.h b/taicpp/src/comms/comms.h new file mode 100644 index 0000000..c02cb9b --- /dev/null +++ b/taicpp/src/comms/comms.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include +#include +#include +#include +namespace detz::comms { +class Telegram { public: explicit Telegram(std::string token=""):tok_(tok_){} bool send(const std::string& chat, const std::string& msg) const; private: std::string tok_; }; +class Installer { public: bool install(const std::string& target) const; }; +struct SysInfoData { std::string os, arch; }; +class SysInfo { public: SysInfoData collect() const; }; +} // namespace detz::comms diff --git a/taicpp/src/comms/comms.mk b/taicpp/src/comms/comms.mk new file mode 100644 index 0000000..0e82ebe --- /dev/null +++ b/taicpp/src/comms/comms.mk @@ -0,0 +1 @@ +SRCS += src/comms/comms.cpp diff --git a/taicpp/src/config/config.h b/taicpp/src/config/config.h new file mode 100644 index 0000000..47c5d94 --- /dev/null +++ b/taicpp/src/config/config.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include +#include +namespace detz { +struct SettingSpec { std::string key; std::string env; std::string type; std::string def; std::string help; bool nullable = false; bool encrypted = false; }; +const std::map& SettingsCatalog(); +struct Config { + std::string home; std::string model; std::string explicitModel; + std::string openrouterKey; std::vector openrouterModels; + std::string devplaceKey; std::string devplaceBase; std::vector devplaceModels; + std::vector pollinationsModels; std::vector zenModels; std::vector opencodeModels; + std::string voice = "en-US-EmmaMultilingualNeural"; + bool noOpencodeCli = false; + static Config FromEnv(bool noOpencode = false); + std::string Get(const std::string& key) const; +}; +std::vector SplitModels(const std::string& raw); +std::vector KnownModelIds(const Config& cfg, const std::vector& extra = {}); +// resolve_tag: kinds "bot" (bot:), "model", "auto", "file", "unknown". +// botResolver maps lowered name -> target, may be empty. +std::pair ResolveTag(const std::string& tag, const std::map& bots, const std::vector& models, const std::string& directory); +} // namespace detz diff --git a/taicpp/src/config/consts.h b/taicpp/src/config/consts.h new file mode 100644 index 0000000..53b3739 --- /dev/null +++ b/taicpp/src/config/consts.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include + +namespace detz { + +struct Consts { + static constexpr int kTotalStepCap = 500; + static constexpr int kSysinfoTimeout = 10; + static constexpr int kLoopNudgeAt = 3; + static constexpr int kLoopStopAt = 5; + static constexpr int kSystemMaxChars = 12000; + static constexpr int kShellTimeout = 120; + static constexpr int kSchedulerInterval = 30; + static constexpr int kHttpTimeout = 60; + static constexpr int kStreamTimeout = 300; + static constexpr int kAuditMaxChars = 20000; + static constexpr int kChunkMaxLines = 60; + static constexpr int kChunkOverlapLines = 3; + static constexpr int kChunkMaxChars = 4000; + static constexpr int kSpillLimit = 6000; +}; + +inline constexpr const char* kDefaultModel = "muse"; +inline constexpr const char* kDefaultVoice = "en-US-EmmaMultilingualNeural"; +inline constexpr const char* kOpenRouterModel = "deepseek/deepseek-v4.1-flash"; +inline constexpr const char* kDevplaceBase = "https://devplace.nl"; + +inline const std::vector& ZenFreeModels() { + static const std::vector v = {"mimo-v2.6-flash-free","nemotron-3.5-lightning-free","muse-spark-1.3-contributor-free","muse-spark-1.2-contributor-free","jev-1.13-free","step-5-preview-free","space-bunny-free","longcat-2.5-preview-free","ling-3.1-flash-free"}; + return v; +} +inline const std::vector& PollinationsModels() { + static const std::vector v = {"openai-fast"}; + return v; +} +inline const std::vector& DevplaceFreeRoutes() { + static const std::vector v = {"free","devii","molodetz"}; + return v; +} + +} // namespace detz diff --git a/taicpp/src/llm/llm.cpp b/taicpp/src/llm/llm.cpp new file mode 100644 index 0000000..b479c1b --- /dev/null +++ b/taicpp/src/llm/llm.cpp @@ -0,0 +1,83 @@ +#include "llm.h" +#include +namespace detz::llm { +static std::string esc(const std::string& s) { + std::string o; for (char c : s) { if (c=='"') o+="\\\""; else if (c=='\n') o+="\\n"; else o+=c; } + return o; +} +static std::string req_json(const ChatRequest& r) { + std::string b = "{\"model\":\"" + esc(r.model) + "\",\"messages\":["; + bool f = true; + for (auto& m : r.messages) { if (!f) b += ","; f = false; b += "{\"role\":\"" + esc(m.role) + "\",\"content\":\"" + esc(m.content) + "\"}"; } + b += "],\"stream\":"; b += r.stream ? "true}" : "false}"; + return b; +} +static std::string grab(const std::string& j, const std::string& key) { + auto p = j.find("\"" + key + "\""); if (p == std::string::npos) return ""; + p = j.find(':', p); if (p == std::string::npos) return ""; + auto q1 = j.find('"', p); if (q1 == std::string::npos) return ""; + auto q2 = j.find('"', q1 + 1); if (q2 == std::string::npos) return ""; + return j.substr(q1 + 1, q2 - q1 - 1); +} +void feed_sse(const std::string& payload, SseSink sink) { + std::istringstream ss(payload); std::string line, data; + auto flush = [&]() { + if (data == "[DONE]") { data.clear(); return true; } + if (!data.empty()) { std::string d = data; data.clear(); return sink(d); } + return true; + }; + while (std::getline(ss, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line.rfind("data:", 0) == 0) { + std::string v = line.substr(5); + while (!v.empty() && v.front() == ' ') v.erase(v.begin()); + data = data.empty() ? v : data + "\n" + v; + } else if (line.empty()) { if (!flush()) return; } + } + if (!data.empty()) flush(); +} +ProviderInfo provider_info(const std::string& p) { + if (p == "anthropic") return {p, "https://api.anthropic.com", "/v1/messages"}; + if (p == "google") return {p, "https://generativelanguage.googleapis.com", "/v1beta/models"}; + if (p == "deepseek") return {p, "https://api.deepseek.com", "/v1/chat/completions"}; + if (p == "xai") return {p, "https://api.x.ai", "/v1/chat/completions"}; + if (p == "openrouter") return {p, "https://openrouter.ai", "/api/v1/chat/completions"}; + return {p == "openai" ? "openai" : "openai", "https://api.openai.com", "/v1/chat/completions"}; +} +std::string provider_names() { return "openai,anthropic,google,deepseek,xai,openrouter"; } +ChatClient::ChatClient(std::string provider, std::string model, HttpPost post) + : provider_(std::move(provider)), model_(std::move(model)), post_(std::move(post)) {} +ChatResponse ChatClient::chat(const ChatRequest& req) { + if (!post_) throw BackendError("no transport (mock required)", -1); + auto info = provider_info(provider_); + ChatRequest r = req; if (r.model.empty()) r.model = model_; + std::string body = post_(info.base_url + info.chat_path, req_json(r), {}); + if (body.empty()) throw BackendError("empty backend response", 502); + ChatResponse out; out.model = r.model; + std::string t = grab(body, "content"); if (t.empty()) t = grab(body, "text"); + if (t.empty()) t = body; + out.text = t; + return out; +} +void ChatClient::chat_stream(const ChatRequest& req, SseSink sink) { + if (!post_) throw BackendError("no transport (mock required)", -1); + auto info = provider_info(provider_); + ChatRequest r = req; r.stream = true; if (r.model.empty()) r.model = model_; + std::string payload = post_(info.base_url + info.chat_path, req_json(r), {}); + feed_sse(payload, sink); +} +void ModelHealth::record(const std::string& m, bool ok, int ms) { samples_.push_back({m, ok, ms}); } +std::optional ModelHealth::best() const { + std::optional b; + for (auto& s : samples_) if (s.ok && (!b || s.latency_ms < b->latency_ms)) b = s; + return b; +} +Opencode::Opencode(std::string base_url, HttpPost post) : base_(std::move(base_url)), post_(std::move(post)) {} +ChatResponse Opencode::complete(const std::string& model, const std::vector& msgs) { + ChatClient c("openai", model, [this](const std::string&, const std::string& body, const std::map&) { + if (!post_) throw BackendError("no transport (mock required)", -1); + return post_(base_ + "/v1/chat/completions", body, {}); + }); + return c.chat(ChatRequest{model, msgs, false}); +} +} // namespace detz::llm diff --git a/taicpp/src/llm/llm.h b/taicpp/src/llm/llm.h new file mode 100644 index 0000000..3c608df --- /dev/null +++ b/taicpp/src/llm/llm.h @@ -0,0 +1,53 @@ +#pragma once +// Thin adapters over moloapp llm providers + SSE streaming. +// Mocked transport only in tests; no real network here. +#include +#include +#include +#include +#include +namespace detz::llm { +struct Message { std::string role; std::string content; }; +struct ChatRequest { std::string model; std::vector messages; bool stream{false}; }; +struct ChatResponse { std::string text; std::string model; std::string finish{"stop"}; }; +class BackendError : public std::runtime_error { + public: + explicit BackendError(const std::string& m, int code = 0) : std::runtime_error(m), code_(code) {} + int code() const { return code_; } + private: + int code_; +}; +// Transport seam: real impl wraps moloapp clients; tests inject mock. +using HttpPost = std::function& headers)>; +using SseSink = std::function; +void feed_sse(const std::string& payload, SseSink sink); +struct ProviderInfo { std::string name; std::string base_url; std::string chat_path; }; +ProviderInfo provider_info(const std::string& provider); +std::string provider_names(); +class ChatClient { + public: + explicit ChatClient(std::string provider, std::string model, HttpPost post = nullptr); + ChatResponse chat(const ChatRequest& req); + void chat_stream(const ChatRequest& req, SseSink sink); + const std::string& provider() const { return provider_; } + private: + std::string provider_, model_; HttpPost post_; +}; +struct ModelSample { std::string model; bool ok; int latency_ms; }; +class ModelHealth { + public: + void record(const std::string& model, bool ok, int latency_ms = 0); + std::optional best() const; + std::size_t size() const { return samples_.size(); } + private: + std::vector samples_; +}; +// Opencode backend shim (OpenAI-compatible /v1/chat/completions). +class Opencode { + public: + explicit Opencode(std::string base_url = "http://localhost:4096", HttpPost post = nullptr); + ChatResponse complete(const std::string& model, const std::vector& msgs); + private: + std::string base_; HttpPost post_; +}; +} // namespace detz::llm diff --git a/taicpp/src/llm/llm.mk b/taicpp/src/llm/llm.mk new file mode 100644 index 0000000..8c2c98c --- /dev/null +++ b/taicpp/src/llm/llm.mk @@ -0,0 +1 @@ +LLM_SRCS = src/llm/llm.cpp diff --git a/taicpp/src/mcp/mcp.cpp b/taicpp/src/mcp/mcp.cpp new file mode 100644 index 0000000..55c5e3c --- /dev/null +++ b/taicpp/src/mcp/mcp.cpp @@ -0,0 +1,6 @@ +#include "mcp.h" +namespace detz::mcp { +void McpServer::add(McpTool t){tools_.push_back(t);} +std::vector McpServer::list() const {std::vector o; for(auto&t:tools_)o.push_back(t.name); return o;} +std::string McpServer::serve(const std::string& r) const {return "{\"echo\":\""+r+"\"}";} +} // namespace detz::mcp diff --git a/taicpp/src/mcp/mcp.h b/taicpp/src/mcp/mcp.h new file mode 100644 index 0000000..fc645da --- /dev/null +++ b/taicpp/src/mcp/mcp.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include +#include +#include +#include +namespace detz::mcp { +struct McpTool { std::string name, desc; }; +class McpServer { public: void add(McpTool t); std::vector list() const; std::string serve(const std::string& req) const; private: std::vector tools_; }; +} // namespace detz::mcp diff --git a/taicpp/src/mcp/mcp.mk b/taicpp/src/mcp/mcp.mk new file mode 100644 index 0000000..59fa9be --- /dev/null +++ b/taicpp/src/mcp/mcp.mk @@ -0,0 +1 @@ +SRCS += src/mcp/mcp.cpp diff --git a/taicpp/src/plexer/plexer.cpp b/taicpp/src/plexer/plexer.cpp new file mode 100644 index 0000000..b731e3e --- /dev/null +++ b/taicpp/src/plexer/plexer.cpp @@ -0,0 +1,5 @@ +#include "plexer.h" +namespace detz::plexer { +Backend PlexerController::backend() const {return b_;} bool PlexerController::send(const std::string& s){buf_+=s; return true;} std::string PlexerController::read(std::size_t n){return buf_.substr(0,n);} +PlexerController PlexerControllerFactory::make(const std::string& n){return PlexerController(n=="tmux"?Backend::Tmux:Backend::Native);} +} // namespace detz::plexer diff --git a/taicpp/src/plexer/plexer.h b/taicpp/src/plexer/plexer.h new file mode 100644 index 0000000..ef1145d --- /dev/null +++ b/taicpp/src/plexer/plexer.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include +#include +#include +#include +namespace detz::plexer { +enum class Backend { Native, Tmux }; +class PlexerController { public: explicit PlexerController(Backend b=Backend::Native):b_(b){} Backend backend() const; bool send(const std::string& s); std::string read(std::size_t n=1024); private: Backend b_; std::string buf_; }; +class PlexerControllerFactory { public: static PlexerController make(const std::string& name); }; +} // namespace detz::plexer diff --git a/taicpp/src/plexer/plexer.mk b/taicpp/src/plexer/plexer.mk new file mode 100644 index 0000000..343515d --- /dev/null +++ b/taicpp/src/plexer/plexer.mk @@ -0,0 +1 @@ +SRCS += src/plexer/plexer.cpp diff --git a/taicpp/src/shell/lanes.cpp b/taicpp/src/shell/lanes.cpp new file mode 100644 index 0000000..d60e582 --- /dev/null +++ b/taicpp/src/shell/lanes.cpp @@ -0,0 +1,13 @@ +#include "lanes.h" +namespace detz::shell { +void LaneBoard::set(const std::string& n, const std::string& t) { + for (auto& l : lanes_) if (l.name == n) { l.text = t; return; } + lanes_.push_back({n, t}); +} +std::string LaneBoard::render() const { + std::string o; for (auto& l : lanes_) o += "[" + l.name + "] " + l.text + "\n"; + return o; +} +std::string Glow::highlight(const std::string& t) { return "**" + t + "**"; } +std::string speak_text(const std::string& t) { return t; } +} // namespace detz::shell diff --git a/taicpp/src/shell/lanes.h b/taicpp/src/shell/lanes.h new file mode 100644 index 0000000..083cfd5 --- /dev/null +++ b/taicpp/src/shell/lanes.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +namespace detz::shell { +struct Lane { std::string name; std::string text; }; +class LaneBoard { + public: + void set(const std::string& name, const std::string& text); + std::string render() const; + std::size_t size() const { return lanes_.size(); } + private: + std::vector lanes_; +}; +class Glow { + public: + static std::string highlight(const std::string& text); +}; +std::string speak_text(const std::string& text); +} // namespace detz::shell diff --git a/taicpp/src/shell/livestream.cpp b/taicpp/src/shell/livestream.cpp new file mode 100644 index 0000000..1a49ded --- /dev/null +++ b/taicpp/src/shell/livestream.cpp @@ -0,0 +1,6 @@ +#include "livestream.h" +namespace detz::shell { +void LiveStream::push(const std::string& c) { chunks_.push_back(c); } +bool LiveStream::drain(ChunkSink s) const { for (auto& c : chunks_) if (!s(c)) return false; return true; } +std::string LiveStream::joined() const { std::string o; for (auto& c : chunks_) o += c; return o; } +} // namespace detz::shell diff --git a/taicpp/src/shell/livestream.h b/taicpp/src/shell/livestream.h new file mode 100644 index 0000000..68ece55 --- /dev/null +++ b/taicpp/src/shell/livestream.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include +#include +namespace detz::shell { +using ChunkSink = std::function; +class LiveStream { + public: + void push(const std::string& chunk); + bool drain(ChunkSink sink) const; + std::string joined() const; + std::size_t size() const { return chunks_.size(); } + private: + std::vector chunks_; +}; +} // namespace detz::shell diff --git a/taicpp/src/shell/pty.cpp b/taicpp/src/shell/pty.cpp new file mode 100644 index 0000000..ddee266 --- /dev/null +++ b/taicpp/src/shell/pty.cpp @@ -0,0 +1,15 @@ +#include "pty.h" +#include +namespace detz::shell { +PtyProc::PtyProc() = default; +PtyProc::~PtyProc() { if (fd_ >= 0) ::close(fd_); } +bool PtyProc::spawn(const std::string& cmd) { + FILE* p = ::popen((cmd + " 2>&1").c_str(), "r"); + if (!p) return false; + char b[1024]; while (std::fgets(b, sizeof b, p)) buf_ += b; + ::pclose(p); alive_ = false; fd_ = -1; pid_ = -1; + return true; +} +std::string PtyProc::read_all(int) { std::string o = buf_; buf_.clear(); return o; } +int PtyProc::wait() { return 0; } +} // namespace detz::shell diff --git a/taicpp/src/shell/pty.h b/taicpp/src/shell/pty.h new file mode 100644 index 0000000..4df915c --- /dev/null +++ b/taicpp/src/shell/pty.h @@ -0,0 +1,16 @@ +#pragma once +#include +namespace detz::shell { +// Minimal PtyProc: wraps forkpty when available, falls back to pipe exec. +class PtyProc { + public: + PtyProc(); + ~PtyProc(); + bool spawn(const std::string& cmd); + std::string read_all(int timeout_ms = 2000); + int wait(); + bool alive() const { return alive_; } + private: + int fd_{-1}; int pid_{-1}; bool alive_{false}; std::string buf_; +}; +} // namespace detz::shell diff --git a/taicpp/src/shell/session.cpp b/taicpp/src/shell/session.cpp new file mode 100644 index 0000000..8e012a3 --- /dev/null +++ b/taicpp/src/shell/session.cpp @@ -0,0 +1,20 @@ +#include "session.h" +#include +#include +namespace detz::shell { +ShellSession::ShellSession(std::string shell) : shell_(std::move(shell)) {} +ExecResult ShellSession::exec(const std::string& cmd, int) { + ExecResult r; + FILE* p = ::popen((shell_ + " -c \"" + cmd + "\" 2>&1").c_str(), "r"); + if (!p) { r.code = 127; return r; } + char buf[1024]; while (std::fgets(buf, sizeof buf, p)) r.out += buf; + r.code = ::pclose(p); + return r; +} +std::vector ShellSession::segment_targets(const std::vector& p) { return p; } +std::vector ShellSession::target_paths(const std::string& spec) { + std::vector out; std::istringstream ss(spec); std::string t; + while (std::getline(ss, t, ':')) if (!t.empty()) out.push_back(t); + return out; +} +} // namespace detz::shell diff --git a/taicpp/src/shell/shell.mk b/taicpp/src/shell/shell.mk new file mode 100644 index 0000000..7a8648e --- /dev/null +++ b/taicpp/src/shell/shell.mk @@ -0,0 +1 @@ +SHELL_SRCS = src/shell/sandbox.cpp src/shell/session.cpp src/shell/pty.cpp src/shell/spinner.cpp src/shell/livestream.cpp src/shell/lanes.cpp diff --git a/taicpp/src/shell/spinner.cpp b/taicpp/src/shell/spinner.cpp new file mode 100644 index 0000000..23f31e9 --- /dev/null +++ b/taicpp/src/shell/spinner.cpp @@ -0,0 +1,6 @@ +#include "spinner.h" +namespace detz::shell { +Spinner::Spinner(std::string label) : label_(std::move(label)) {} +void Spinner::tick() { ++n_; } +void Spinner::done(const std::string&) { ++n_; } +} // namespace detz::shell diff --git a/taicpp/src/shell/spinner.h b/taicpp/src/shell/spinner.h new file mode 100644 index 0000000..4f3a5c9 --- /dev/null +++ b/taicpp/src/shell/spinner.h @@ -0,0 +1,14 @@ +#pragma once +#include +namespace detz::shell { +class Spinner { + public: + explicit Spinner(std::string label = ""); + void tick(); + void done(const std::string& msg = "done"); + const std::string& label() const { return label_; } + int ticks() const { return n_; } + private: + std::string label_; int n_{0}; +}; +} // namespace detz::shell diff --git a/taicpp/src/store/store.cpp b/taicpp/src/store/store.cpp new file mode 100644 index 0000000..876b464 --- /dev/null +++ b/taicpp/src/store/store.cpp @@ -0,0 +1,138 @@ +#include "store.h" +#include +#include +namespace detz::store { +static void chk(int rc, sqlite3* d, const char* what) { + if (rc != SQLITE_OK && rc != SQLITE_DONE && rc != SQLITE_ROW) + throw std::runtime_error(std::string(what) + ": " + (d ? sqlite3_errmsg(d) : "?")); +} +Store::Store(const std::string& path, const std::string& profile) : profile_(profile) { + if (sqlite3_open(path.c_str(), &db_) != SQLITE_OK) { db_ = nullptr; throw std::runtime_error("open db"); } + exec("PRAGMA journal_mode=WAL;"); exec("PRAGMA synchronous=NORMAL;"); exec("PRAGMA busy_timeout=30000;"); + migrate(); setup_fts(); +} +Store::~Store() { if (db_) sqlite3_close(db_); } +void Store::exec(const std::string& sql) { + char* err = nullptr; + if (sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &err) != SQLITE_OK) { + std::string e = err ? err : "?"; sqlite3_free(err); throw std::runtime_error(e); + } +} +void Store::migrate() { + exec("CREATE TABLE IF NOT EXISTS schema_version(v INTEGER);"); + exec("CREATE TABLE IF NOT EXISTS records(id INTEGER PRIMARY KEY, kind TEXT, title TEXT, content TEXT, profile TEXT DEFAULT 'default', created TEXT DEFAULT (datetime('now')));"); + exec("CREATE TABLE IF NOT EXISTS chunks(id INTEGER PRIMARY KEY, item TEXT, ref_id INTEGER, body TEXT, profile TEXT DEFAULT 'default');"); + exec("CREATE TABLE IF NOT EXISTS logs(id INTEGER PRIMARY KEY, level TEXT, message TEXT, ts TEXT DEFAULT (datetime('now')));"); + sqlite3_stmt* s = nullptr; + sqlite3_prepare_v2(db_, "SELECT COUNT(*) FROM schema_version;", -1, &s, nullptr); + int n = 0; + if (sqlite3_step(s) == SQLITE_ROW) n = sqlite3_column_int(s, 0); + sqlite3_finalize(s); + if (n == 0) exec("INSERT INTO schema_version(v) VALUES(1);"); +} +void Store::setup_fts() { + exec("CREATE VIRTUAL TABLE IF NOT EXISTS fts_docs USING fts5(item, kind, sub, title, body, profile);"); + exec("CREATE TRIGGER IF NOT EXISTS trg_records_ai AFTER INSERT ON records BEGIN INSERT INTO fts_docs(item,kind,sub,title,body,profile) VALUES(new.id,'record',new.kind,new.title,new.content,new.profile); END;"); + exec("CREATE TRIGGER IF NOT EXISTS trg_records_ad AFTER DELETE ON records BEGIN DELETE FROM fts_docs WHERE item=old.id AND kind='record'; END;"); + exec("CREATE TRIGGER IF NOT EXISTS trg_records_au AFTER UPDATE ON records WHEN old.title!=new.title OR old.content!=new.content BEGIN DELETE FROM fts_docs WHERE item=old.id AND kind='record'; INSERT INTO fts_docs(item,kind,sub,title,body,profile) VALUES(new.id,'record',new.kind,new.title,new.content,new.profile); END;"); +} +int Store::schema_version() const { + sqlite3_stmt* s = nullptr; int v = 0; + sqlite3_prepare_v2(db_, "SELECT v FROM schema_version LIMIT 1;", -1, &s, nullptr); + if (sqlite3_step(s) == SQLITE_ROW) v = sqlite3_column_int(s, 0); + sqlite3_finalize(s); return v; +} +int64_t Store::add_record(const std::string& kind, const std::string& title, const std::string& content) { + sqlite3_stmt* s = nullptr; + sqlite3_prepare_v2(db_, "INSERT INTO records(kind,title,content,profile) VALUES(?,?,?,?);", -1, &s, nullptr); + sqlite3_bind_text(s, 1, kind.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(s, 2, title.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(s, 3, content.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(s, 4, profile_.c_str(), -1, SQLITE_TRANSIENT); + chk(sqlite3_step(s), db_, "insert"); sqlite3_finalize(s); + return sqlite3_last_insert_rowid(db_); +} +bool Store::update_record(int64_t id, const std::string& title, const std::string& content) { + sqlite3_stmt* s = nullptr; + sqlite3_prepare_v2(db_, "UPDATE records SET title=?, content=? WHERE id=?;", -1, &s, nullptr); + sqlite3_bind_text(s, 1, title.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(s, 2, content.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(s, 3, id); + sqlite3_step(s); sqlite3_finalize(s); return sqlite3_changes(db_) > 0; +} +bool Store::delete_record(int64_t id) { + sqlite3_stmt* s = nullptr; + sqlite3_prepare_v2(db_, "DELETE FROM records WHERE id=?;", -1, &s, nullptr); + sqlite3_bind_int64(s, 1, id); sqlite3_step(s); sqlite3_finalize(s); + return sqlite3_changes(db_) > 0; +} +std::optional Store::get_record(int64_t id) { + sqlite3_stmt* s = nullptr; + sqlite3_prepare_v2(db_, "SELECT id,kind,title,content,profile,created FROM records WHERE id=?;", -1, &s, nullptr); + sqlite3_bind_int64(s, 1, id); + std::optional r; + if (sqlite3_step(s) == SQLITE_ROW) { + Record x; x.id = sqlite3_column_int64(s, 0); + x.kind = (const char*)sqlite3_column_text(s, 1); + x.title = (const char*)sqlite3_column_text(s, 2); + const unsigned char* c = sqlite3_column_text(s, 3); + x.content = c ? (const char*)c : ""; + x.profile = (const char*)sqlite3_column_text(s, 4); + const unsigned char* t = sqlite3_column_text(s, 5); + x.created = t ? (const char*)t : ""; + r = x; + } + sqlite3_finalize(s); return r; +} +std::vector Store::search(const std::string& q, int limit) { + std::vector out; + sqlite3_stmt* s = nullptr; + sqlite3_prepare_v2(db_, "SELECT item,kind,sub,title,snippet(fts_docs,4,'[',']','...',12) FROM fts_docs WHERE fts_docs MATCH ? LIMIT ?;", -1, &s, nullptr); + sqlite3_bind_text(s, 1, q.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int(s, 2, limit < 1 ? 1 : (limit > 100 ? 100 : limit)); + while (sqlite3_step(s) == SQLITE_ROW) { + SearchHit h; + h.item = (const char*)sqlite3_column_text(s, 0); + h.kind = (const char*)sqlite3_column_text(s, 1); + h.sub = (const char*)sqlite3_column_text(s, 2); + h.title = (const char*)sqlite3_column_text(s, 3); + const unsigned char* sn = sqlite3_column_text(s, 4); + h.snippet = sn ? (const char*)sn : ""; + out.push_back(h); + } + sqlite3_finalize(s); return out; +} +void Store::add_chunks(const std::string& item, int64_t id, const std::string& text) { + sqlite3_stmt* s = nullptr; + sqlite3_prepare_v2(db_, "INSERT INTO chunks(item,ref_id,body,profile) VALUES(?,?,?,?);", -1, &s, nullptr); + sqlite3_bind_text(s, 1, item.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(s, 2, id); + sqlite3_bind_text(s, 3, text.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(s, 4, profile_.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_step(s); sqlite3_finalize(s); +} +std::string truncate_text(const std::string& s, size_t limit) { + if (s.size() <= limit) return s; + return s.substr(0, limit) + "...[" + std::to_string(s.size()) + " chars truncated]..."; +} +std::string spill_output(Store& st, const std::string& kind, const std::string& title, const std::string& text) { + int64_t id = st.add_record(kind, title, text); + st.add_chunks("record", id, text); + return std::to_string(id); +} +std::string spilled_view(const std::string& text, int64_t rid, size_t head, size_t tail) { + if (text.size() <= head + tail) return text; + return text.substr(0, head) + "\n...[" + std::to_string(text.size()) + " chars spilled to " + std::to_string(rid) + "]...\n" + text.substr(text.size() - tail); +} +std::string spill_or_truncate(Store* st, const std::string& kind, const std::string& title, const std::string& text) { + const size_t LIM = 4000; + if (!st || text.size() <= LIM) return truncate_text(text, LIM); + int64_t id = st->add_record(kind, title, text); + st->add_chunks("record", id, text); + return spilled_view(text, id); +} +void DebugLog::push(const std::string& l, const std::string& m) { lines_.push_back("[" + l + "] " + m); } +void DebugLog::info(const std::string& m) { push("INFO", m); } +void DebugLog::warn(const std::string& m) { push("WARN", m); } +void DebugLog::error(const std::string& m) { push("ERROR", m); } +} // namespace detz::store diff --git a/taicpp/src/tui/tui.cpp b/taicpp/src/tui/tui.cpp new file mode 100644 index 0000000..91c07b8 --- /dev/null +++ b/taicpp/src/tui/tui.cpp @@ -0,0 +1,5 @@ +#include "tui.h" +namespace detz::tui { +std::string Renderer::render(const std::vector& ss,const Theme&) const {std::string o; for(auto&s:ss)o+=s.text; return o;} +std::string Renderer::renderText(const std::string& s) const {return s;} +} // namespace detz::tui diff --git a/taicpp/src/tui/tui.h b/taicpp/src/tui/tui.h new file mode 100644 index 0000000..75ab5d4 --- /dev/null +++ b/taicpp/src/tui/tui.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include +#include +#include +#include +namespace detz::tui { +struct Theme { std::string name{"default"}; int fg=7, bg=0; }; +struct Style { bool bold=false, dim=false; }; +struct Span { std::string text; Style style; }; +class Renderer { public: std::string render(const std::vector& spans, const Theme& t) const; std::string renderText(const std::string& s) const; }; +} // namespace detz::tui diff --git a/taicpp/src/tui/tui.mk b/taicpp/src/tui/tui.mk new file mode 100644 index 0000000..fdf0f6e --- /dev/null +++ b/taicpp/src/tui/tui.mk @@ -0,0 +1 @@ +SRCS += src/tui/tui.cpp diff --git a/taicpp/src/util/ansi.h b/taicpp/src/util/ansi.h new file mode 100644 index 0000000..ae8bc08 --- /dev/null +++ b/taicpp/src/util/ansi.h @@ -0,0 +1,17 @@ +#pragma once +#include +namespace detz { +struct Ansi { + static constexpr const char* kReset = "\033[0m"; + static constexpr const char* kBold = "\033[1m"; + static constexpr const char* kDim = "\033[2m"; + static constexpr const char* kRed = "\033[31m"; + static constexpr const char* kGreen = "\033[32m"; + static constexpr const char* kYellow = "\033[33m"; + static constexpr const char* kBlue = "\033[34m"; + static constexpr const char* kMagenta = "\033[35m"; + static constexpr const char* kCyan = "\033[36m"; +}; +std::string Paint(const std::string& text, const std::string& codes); +std::string Paint2(const std::string& text, const std::string& c1, const std::string& c2); +} // namespace detz diff --git a/taicpp/src/util/text.h b/taicpp/src/util/text.h new file mode 100644 index 0000000..89a18eb --- /dev/null +++ b/taicpp/src/util/text.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include +#include +namespace detz { +std::string Truncate(const std::string& text, size_t head = 1500, size_t tail = 500); +std::string CappedText(const std::string& text); +std::string TruncMarker(size_t trueBytes); +std::vector ChunkLines(const std::string& text, int maxLines = 60, int overlapLines = 3, int maxChars = 4000); +std::string CollapseRepeatedLines(const std::string& text, int threshold = 3); +std::pair, std::string> SplitLeadingTags(const std::string& text); +std::string ExpandFileMentions(const std::string& text, const std::string& directory); +std::vector CompleteFiles(const std::string& prefix, const std::string& directory, size_t cap = 100); +} // namespace detz diff --git a/taicpp/tests/cli_test.cpp b/taicpp/tests/cli_test.cpp new file mode 100644 index 0000000..70e46f1 --- /dev/null +++ b/taicpp/tests/cli_test.cpp @@ -0,0 +1,19 @@ +#include "../src/cli/cli.h" +#include +#include +int main() { + detz::cli::Parser p; + // golden --help contains every flag + std::string h = detz::cli::Parser::helpText(); + for (auto f : {"--profile","--yes","--yolo","--auto","--continue","--debug","--debug-format","--env","--no-opencode","--update-models","--eval-retrieval","--stats-rag","--version","--cap-manifest","--cap-speak","--cap-cwd","--cap-model","--cap-permission-mode","--cap-max-turns","--cap ","--serve-mcp","--mcp-port","--mcp-bind","--telegram","--scheduler","--install","--install-telegram","--uninstall-telegram","--plexer-wrap","--mplex","--demo","--demo-shot","--tui","--tui-local","--theme"}) + assert(h.find(f) != std::string::npos); + // parse smoke + const char* av[] = {"detz","--yes","--debug","--cap","resolve","foo"}; + auto c = p.parse(6, (char**)av); + assert(c.opts.yes && c.opts.debug.value()=="file" && c.opts.cap && c.opts.cap->size()==2); + const char* bv[] = {"detz","--version"}; + assert(p.parse(2,(char**)bv).run()==0); + const char* cv[] = {"detz"}; + assert(p.parse(1,(char**)cv).name=="--help"); + return 0; +}