detz: M1 CLI parity
This commit is contained in:
+5
-3
@@ -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
|
||||
|
||||
@@ -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<std::string> ToolRegistry::names() const {std::vector<std::string> 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<RetrievalScore> RetrievalEval::score(const std::string&,const std::vector<std::string>& docs) const {std::vector<RetrievalScore> 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
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
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<std::string> names() const; private: std::map<std::string,ToolDef> 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<RetrievalScore> score(const std::string& q, const std::vector<std::string>& 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
|
||||
@@ -0,0 +1 @@
|
||||
SRCS += src/agent/agent.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
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
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<std::string,Cap> caps_; };
|
||||
} // namespace detz::cap
|
||||
@@ -0,0 +1 @@
|
||||
SRCS += src/cap/cap.cpp
|
||||
+66
-6
@@ -1,17 +1,77 @@
|
||||
#include "cli.h"
|
||||
#include "version.hpp"
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
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<std::string>& 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<argc && argv[i+1][0]!='-') ? argv[++i] : "file"; }
|
||||
else if (a=="--debug-format") nxt(o.debug_format);
|
||||
else if (a=="--env") nxt(o.env);
|
||||
else if (a=="--no-opencode") o.no_opencode = true;
|
||||
else if (a=="--update-models") o.update_models = true;
|
||||
else if (a=="--eval-retrieval") o.eval_retrieval = true;
|
||||
else if (a=="--stats-rag") o.stats_rag = true;
|
||||
else if (a=="--version") o.version = true;
|
||||
else if (a=="--cap-manifest") o.cap_manifest = true;
|
||||
else if (a=="--cap-speak") o.cap_speak = true;
|
||||
else if (a=="--cap-cwd") nxt(o.cap_cwd);
|
||||
else if (a=="--cap-model") nxt(o.cap_model);
|
||||
else if (a=="--cap-permission-mode") nxt(o.cap_permission_mode);
|
||||
else if (a=="--cap-max-turns") { if (i+1<argc) o.cap_max_turns = std::stoi(argv[++i]); }
|
||||
else if (a=="--cap") { o.cap = std::vector<std::string>{}; while (++i<argc) o.cap->push_back(argv[i]); break; }
|
||||
else if (a=="--serve-mcp") nxt(o.serve_mcp);
|
||||
else if (a=="--mcp-port") { if (i+1<argc) o.mcp_port = std::stoi(argv[++i]); }
|
||||
else if (a=="--mcp-bind") nxt(o.mcp_bind);
|
||||
else if (a=="--telegram") o.telegram = true;
|
||||
else if (a=="--scheduler") o.scheduler = true;
|
||||
else if (a=="--install") o.install = true;
|
||||
else if (a=="--install-telegram") o.install_telegram = true;
|
||||
else if (a=="--uninstall-telegram") o.uninstall_telegram = true;
|
||||
else if (a=="--plexer-wrap") o.plexer_wrap = true;
|
||||
else if (a=="--mplex") { o.mplex = std::vector<std::string>{}; while (++i<argc) o.mplex->push_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<argc;++i) c.args.emplace_back(argv[i]);
|
||||
return c;
|
||||
}
|
||||
int Command::run() {
|
||||
if (name == "--version" || name == "version") { std::puts(kVersion.data()); return 0; }
|
||||
std::puts("detz — C++23 port of tai.py. See --help. Version:");
|
||||
std::puts(kVersion.data());
|
||||
if (opts.version || name=="--version") { std::puts(kVersion.data()); return 0; }
|
||||
std::fputs(Parser::helpText().c_str(), stdout);
|
||||
return 0;
|
||||
}
|
||||
int DetzCli::run(int argc, char** argv) { return Parser{}.parse(argc, argv).run(); }
|
||||
} // namespace detz::cli
|
||||
} // namespace detz::cli
|
||||
|
||||
+26
-4
@@ -1,10 +1,32 @@
|
||||
#pragma once
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace detz::cli {
|
||||
struct Command { std::string name{"--help"}; std::vector<std::string> args; int run(); };
|
||||
class Parser { public: Command parse(int argc, char** argv); };
|
||||
struct Options {
|
||||
std::optional<std::string> profile;
|
||||
bool yes = false, yolo = false, auto_ = false, resume = false;
|
||||
std::optional<std::string> debug; // file|stdout|stderr
|
||||
std::optional<std::string> debug_format;
|
||||
std::optional<std::string> 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<std::string> cap_cwd, cap_model, cap_permission_mode;
|
||||
std::optional<int> cap_max_turns;
|
||||
std::optional<std::vector<std::string>> cap; // REMAINDER
|
||||
std::optional<std::string> serve_mcp;
|
||||
std::optional<int> mcp_port;
|
||||
std::optional<std::string> mcp_bind;
|
||||
bool telegram = false, scheduler = false, install = false;
|
||||
bool install_telegram = false, uninstall_telegram = false, plexer_wrap = false;
|
||||
std::optional<std::vector<std::string>> mplex; // REMAINDER
|
||||
bool demo = false, demo_shot = false, tui = false, tui_local = false;
|
||||
std::optional<std::string> theme;
|
||||
bool help = false;
|
||||
std::vector<std::string> prompt;
|
||||
};
|
||||
struct Command { std::string name{"--help"}; std::vector<std::string> 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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
SRCS += src/comms/comms.cpp
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
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<std::string, SettingSpec>& SettingsCatalog();
|
||||
struct Config {
|
||||
std::string home; std::string model; std::string explicitModel;
|
||||
std::string openrouterKey; std::vector<std::string> openrouterModels;
|
||||
std::string devplaceKey; std::string devplaceBase; std::vector<std::string> devplaceModels;
|
||||
std::vector<std::string> pollinationsModels; std::vector<std::string> zenModels; std::vector<std::string> 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<std::string> SplitModels(const std::string& raw);
|
||||
std::vector<std::string> KnownModelIds(const Config& cfg, const std::vector<std::string>& extra = {});
|
||||
// resolve_tag: kinds "bot" (bot:<name>), "model", "auto", "file", "unknown".
|
||||
// botResolver maps lowered name -> target, may be empty.
|
||||
std::pair<std::string, std::string> ResolveTag(const std::string& tag, const std::map<std::string,std::string>& bots, const std::vector<std::string>& models, const std::string& directory);
|
||||
} // namespace detz
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::string>& ZenFreeModels() {
|
||||
static const std::vector<std::string> 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<std::string>& PollinationsModels() {
|
||||
static const std::vector<std::string> v = {"openai-fast"};
|
||||
return v;
|
||||
}
|
||||
inline const std::vector<std::string>& DevplaceFreeRoutes() {
|
||||
static const std::vector<std::string> v = {"free","devii","molodetz"};
|
||||
return v;
|
||||
}
|
||||
|
||||
} // namespace detz
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "llm.h"
|
||||
#include <sstream>
|
||||
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<ModelSample> ModelHealth::best() const {
|
||||
std::optional<ModelSample> 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<Message>& msgs) {
|
||||
ChatClient c("openai", model, [this](const std::string&, const std::string& body, const std::map<std::string,std::string>&) {
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
// Thin adapters over moloapp llm providers + SSE streaming.
|
||||
// Mocked transport only in tests; no real network here.
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace detz::llm {
|
||||
struct Message { std::string role; std::string content; };
|
||||
struct ChatRequest { std::string model; std::vector<Message> 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<std::string(const std::string& url, const std::string& body, const std::map<std::string,std::string>& headers)>;
|
||||
using SseSink = std::function<bool(const std::string&)>;
|
||||
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<ModelSample> best() const;
|
||||
std::size_t size() const { return samples_.size(); }
|
||||
private:
|
||||
std::vector<ModelSample> 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<Message>& msgs);
|
||||
private:
|
||||
std::string base_; HttpPost post_;
|
||||
};
|
||||
} // namespace detz::llm
|
||||
@@ -0,0 +1 @@
|
||||
LLM_SRCS = src/llm/llm.cpp
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "mcp.h"
|
||||
namespace detz::mcp {
|
||||
void McpServer::add(McpTool t){tools_.push_back(t);}
|
||||
std::vector<std::string> McpServer::list() const {std::vector<std::string> 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
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
namespace detz::mcp {
|
||||
struct McpTool { std::string name, desc; };
|
||||
class McpServer { public: void add(McpTool t); std::vector<std::string> list() const; std::string serve(const std::string& req) const; private: std::vector<McpTool> tools_; };
|
||||
} // namespace detz::mcp
|
||||
@@ -0,0 +1 @@
|
||||
SRCS += src/mcp/mcp.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
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
SRCS += src/plexer/plexer.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
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
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<Lane> lanes_;
|
||||
};
|
||||
class Glow {
|
||||
public:
|
||||
static std::string highlight(const std::string& text);
|
||||
};
|
||||
std::string speak_text(const std::string& text);
|
||||
} // namespace detz::shell
|
||||
@@ -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
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
namespace detz::shell {
|
||||
using ChunkSink = std::function<bool(const std::string&)>;
|
||||
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<std::string> chunks_;
|
||||
};
|
||||
} // namespace detz::shell
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "pty.h"
|
||||
#include <cstdio>
|
||||
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
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
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
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "session.h"
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
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<std::string> ShellSession::segment_targets(const std::vector<std::string>& p) { return p; }
|
||||
std::vector<std::string> ShellSession::target_paths(const std::string& spec) {
|
||||
std::vector<std::string> 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
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
|
||||
@@ -0,0 +1,138 @@
|
||||
#include "store.h"
|
||||
#include <sqlite3.h>
|
||||
#include <stdexcept>
|
||||
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<Record> 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<Record> 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<SearchHit> Store::search(const std::string& q, int limit) {
|
||||
std::vector<SearchHit> 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
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "tui.h"
|
||||
namespace detz::tui {
|
||||
std::string Renderer::render(const std::vector<Span>& 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
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
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<Span>& spans, const Theme& t) const; std::string renderText(const std::string& s) const; };
|
||||
} // namespace detz::tui
|
||||
@@ -0,0 +1 @@
|
||||
SRCS += src/tui/tui.cpp
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
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
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
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<std::string> 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::vector<std::string>, std::string> SplitLeadingTags(const std::string& text);
|
||||
std::string ExpandFileMentions(const std::string& text, const std::string& directory);
|
||||
std::vector<std::string> CompleteFiles(const std::string& prefix, const std::string& directory, size_t cap = 100);
|
||||
} // namespace detz
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "../src/cli/cli.h"
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user