Add Kconfig and more tools
This commit is contained in:
parent
779ac57f50
commit
53a1a043c7
|
|
@ -23,6 +23,30 @@ endif()
|
|||
|
||||
set(KOM_HAVE_PG ${KOM_HAVE_PG} CACHE INTERNAL "kom_dal has Postgres backend")
|
||||
|
||||
set(KOMPANION_KCONFIG_TARGET "")
|
||||
find_package(KF6Config QUIET)
|
||||
if (KF6Config_FOUND)
|
||||
set(KOMPANION_KCONFIG_TARGET KF6::ConfigCore)
|
||||
else()
|
||||
find_package(KF5Config QUIET)
|
||||
if (KF5Config_FOUND)
|
||||
set(KOMPANION_KCONFIG_TARGET KF5::ConfigCore)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (KOMPANION_KCONFIG_TARGET STREQUAL "")
|
||||
message(WARNING "KConfig (KF6/KF5) not found; defaulting to environment-based configuration.")
|
||||
endif()
|
||||
|
||||
option(BUILD_KOMPANION_CLI "Build Kompanion Qt command-line client" ON)
|
||||
if (BUILD_KOMPANION_CLI)
|
||||
find_package(Qt6 COMPONENTS Core QUIET)
|
||||
if (NOT Qt6_FOUND)
|
||||
message(WARNING "Qt6 Core not found; disabling Kompanion CLI.")
|
||||
set(BUILD_KOMPANION_CLI OFF)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Placeholder: find Qt and qtmcp when available
|
||||
# find_package(Qt6 COMPONENTS Core Network REQUIRED)
|
||||
# find_package(qtmcp REQUIRED)
|
||||
|
|
@ -34,10 +58,30 @@ add_executable(kom_mcp
|
|||
)
|
||||
target_include_directories(kom_mcp PRIVATE src)
|
||||
target_link_libraries(kom_mcp PRIVATE kom_dal)
|
||||
target_compile_definitions(kom_mcp PRIVATE PROJECT_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
|
||||
if (NOT KOMPANION_KCONFIG_TARGET STREQUAL "")
|
||||
target_link_libraries(kom_mcp PRIVATE ${KOMPANION_KCONFIG_TARGET})
|
||||
target_compile_definitions(kom_mcp PRIVATE HAVE_KCONFIG)
|
||||
endif()
|
||||
|
||||
install(TARGETS kom_mcp RUNTIME DESTINATION bin)
|
||||
|
||||
option(BUILD_TESTS "Build tests" ON)
|
||||
|
||||
if (BUILD_KOMPANION_CLI)
|
||||
add_executable(kompanion
|
||||
src/cli/KompanionApp.cpp
|
||||
)
|
||||
target_include_directories(kompanion PRIVATE src)
|
||||
target_link_libraries(kompanion PRIVATE Qt6::Core kom_dal)
|
||||
target_compile_definitions(kompanion PRIVATE PROJECT_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
|
||||
if (NOT KOMPANION_KCONFIG_TARGET STREQUAL "")
|
||||
target_link_libraries(kompanion PRIVATE ${KOMPANION_KCONFIG_TARGET})
|
||||
target_compile_definitions(kompanion PRIVATE HAVE_KCONFIG)
|
||||
endif()
|
||||
install(TARGETS kompanion RUNTIME DESTINATION bin)
|
||||
endif()
|
||||
|
||||
if (BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@
|
|||
- **Hybrid policy**: `pgSearchVector` will filter by caller capability (namespace scope, secret clearance) before ranking; contract tests must assert omission of secret-tagged items.
|
||||
- **CLI sketch**: plan for a Qt `QCoreApplication` tool (`kom_mctl`) exposing commands to list namespaces, tail episodic streams, trigger `sync_semantic`, and inspect resonance graphs—all wired through the new prepared statements.
|
||||
- **Observability**: CLI should read the `jobs/semantic_sync` state block to display cursors, pending counts, and last error logs; dry-run mode estimates embeddings without committing.
|
||||
- **Activation parity**: Long term, mirror the KDE `akonadiclient`/`akonadi-console` pattern—Kompanion CLI doubles as an MCP surface today and later as a DBus-activated helper so tools can be socket-triggered into the memory service.
|
||||
- **KConfig defaults**: `kom_mcp` and `kompanion` load `Database/PgDsn` from `~/.config/kompanionrc` (see `docs/configuration.md`) when `PG_DSN` is unset, keeping deployments kioskable.
|
||||
|
||||
## Next-Step Checklist
|
||||
- [x] Detect pqxx via CMake and plumb `HAVE_PG`.
|
||||
|
|
|
|||
42
src/main.cpp
42
src/main.cpp
|
|
@ -1,9 +1,17 @@
|
|||
// Minimal CLI runner that registers Kompanion MCP tools and dispatches requests.
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#ifdef HAVE_KCONFIG
|
||||
#include <KConfigGroup>
|
||||
#include <KSharedConfig>
|
||||
#endif
|
||||
|
||||
#include "mcp/KomMcpServer.hpp"
|
||||
#include "mcp/RegisterTools.hpp"
|
||||
|
||||
|
|
@ -42,13 +50,40 @@ void print_usage(const char* exe, KomMcpServer& server) {
|
|||
|
||||
} // namespace
|
||||
|
||||
#ifdef HAVE_KCONFIG
|
||||
std::optional<std::string> read_dsn_from_config() {
|
||||
auto config = KSharedConfig::openConfig(QStringLiteral("kompanionrc"));
|
||||
if (!config) return std::nullopt;
|
||||
KConfigGroup dbGroup(config, QStringLiteral("Database"));
|
||||
const QString dsn = dbGroup.readEntry(QStringLiteral("PgDsn"), QString());
|
||||
if (dsn.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return dsn.toStdString();
|
||||
}
|
||||
#else
|
||||
std::optional<std::string> read_dsn_from_config() {
|
||||
return std::nullopt;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
KomMcpServer server;
|
||||
register_default_tools(server);
|
||||
|
||||
const char* pgDsn = std::getenv("PG_DSN");
|
||||
if (!pgDsn || !*pgDsn) {
|
||||
std::cerr << "[kom_mcp] PG_DSN not set; fallback DAL will be used if available.\n";
|
||||
const char* envDsn = std::getenv("PG_DSN");
|
||||
std::optional<std::string> effectiveDsn;
|
||||
if (envDsn && *envDsn) {
|
||||
effectiveDsn = std::string(envDsn);
|
||||
} else {
|
||||
effectiveDsn = read_dsn_from_config();
|
||||
if (effectiveDsn) {
|
||||
::setenv("PG_DSN", effectiveDsn->c_str(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!effectiveDsn) {
|
||||
std::cerr << "[kom_mcp] PG_DSN not set; fallback DAL will be used if available (configure Database/PgDsn via KConfig).\n";
|
||||
}
|
||||
|
||||
if (argc < 2) {
|
||||
|
|
@ -80,4 +115,3 @@ int main(int argc, char** argv) {
|
|||
return 1;
|
||||
}
|
||||
}
|
||||
#include <cstdlib>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Handlers {
|
||||
namespace detail {
|
||||
|
||||
inline const std::filesystem::path& projectRoot() {
|
||||
static const std::filesystem::path root =
|
||||
#ifdef PROJECT_SOURCE_DIR
|
||||
std::filesystem::path(PROJECT_SOURCE_DIR);
|
||||
#else
|
||||
std::filesystem::current_path();
|
||||
#endif
|
||||
return root;
|
||||
}
|
||||
|
||||
inline std::string jsonEscape(const std::string& in) {
|
||||
std::ostringstream os;
|
||||
for (char c : in) {
|
||||
switch (c) {
|
||||
case '\"': os << "\\\""; break;
|
||||
case '\\': os << "\\\\"; break;
|
||||
case '\b': os << "\\b"; break;
|
||||
case '\f': os << "\\f"; break;
|
||||
case '\n': os << "\\n"; break;
|
||||
case '\r': os << "\\r"; break;
|
||||
case '\t': os << "\\t"; break;
|
||||
default:
|
||||
if (static_cast<unsigned char>(c) < 0x20) {
|
||||
os << "\\u" << std::hex << std::uppercase << static_cast<int>(c);
|
||||
} else {
|
||||
os << c;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
inline std::string readFileHead(const std::string& relativePath,
|
||||
int maxLines,
|
||||
std::size_t maxBytes) {
|
||||
const std::filesystem::path path = projectRoot() / relativePath;
|
||||
std::ifstream input(path);
|
||||
if (!input) {
|
||||
return std::string("missing: ") + relativePath;
|
||||
}
|
||||
|
||||
std::ostringstream oss;
|
||||
std::string line;
|
||||
int lineCount = 0;
|
||||
while (std::getline(input, line)) {
|
||||
oss << line << '\n';
|
||||
if (++lineCount >= maxLines || oss.tellp() >= static_cast<std::streampos>(maxBytes)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
inline std::string runCommandCapture(const char* cmd, std::size_t maxBytes = 8192) {
|
||||
#ifdef _WIN32
|
||||
(void)cmd;
|
||||
(void)maxBytes;
|
||||
return "git status capture not supported on this platform";
|
||||
#else
|
||||
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
|
||||
if (!pipe) {
|
||||
return "git status unavailable";
|
||||
}
|
||||
std::ostringstream oss;
|
||||
char buffer[256];
|
||||
std::size_t total = 0;
|
||||
while (fgets(buffer, sizeof(buffer), pipe.get())) {
|
||||
const std::size_t len = std::strlen(buffer);
|
||||
total += len;
|
||||
if (total > maxBytes) {
|
||||
oss.write(buffer, static_cast<std::streamsize>(maxBytes - (total - len)));
|
||||
break;
|
||||
}
|
||||
oss.write(buffer, static_cast<std::streamsize>(len));
|
||||
}
|
||||
return oss.str();
|
||||
#endif
|
||||
}
|
||||
|
||||
inline std::optional<std::string> currentDsnSource() {
|
||||
const char* env = std::getenv("PG_DSN");
|
||||
if (env && *env) {
|
||||
return std::string(env);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Produces a JSON response summarising project state: memory docs, task table, git status.
|
||||
inline std::string project_snapshot(const std::string& reqJson) {
|
||||
(void)reqJson;
|
||||
|
||||
const std::string memorySummary =
|
||||
detail::readFileHead("docs/memory-architecture.md", 40, 4096);
|
||||
const std::string tasksSummary =
|
||||
detail::readFileHead("tasks-table.md", 40, 4096);
|
||||
const std::string gitStatus =
|
||||
detail::runCommandCapture("git status --short --branch 2>/dev/null");
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{";
|
||||
json << "\"sections\":[";
|
||||
json << "{\"title\":\"memory_architecture\",\"body\":\"" << detail::jsonEscape(memorySummary) << "\"},";
|
||||
json << "{\"title\":\"tasks_table\",\"body\":\"" << detail::jsonEscape(tasksSummary) << "\"},";
|
||||
json << "{\"title\":\"git_status\",\"body\":\"" << detail::jsonEscape(gitStatus) << "\"}";
|
||||
json << "]";
|
||||
|
||||
if (auto dsn = detail::currentDsnSource()) {
|
||||
json << ",\"pg_dsn\":\"" << detail::jsonEscape(*dsn) << "\"";
|
||||
}
|
||||
|
||||
json << ",\"notes\":\"Project snapshot generated by Kompanion to aid MCP agents.\"";
|
||||
json << "}";
|
||||
return json.str();
|
||||
}
|
||||
|
||||
} // namespace Handlers
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
#include "KomMcpServer.hpp"
|
||||
#include "HandlersIntrospection.hpp"
|
||||
#include "HandlersLocalBackup.hpp"
|
||||
#include "HandlersMemory.hpp"
|
||||
|
||||
|
|
@ -8,4 +9,5 @@ inline void register_default_tools(KomMcpServer& server) {
|
|||
server.registerTool("kom.memory.v1.search_memory", Handlers::search_memory);
|
||||
server.registerTool("kom.local.v1.backup.export_encrypted", Handlers::backup_export_encrypted);
|
||||
server.registerTool("kom.local.v1.backup.import_encrypted", Handlers::backup_import_encrypted);
|
||||
server.registerTool("kom.meta.v1.project_snapshot", Handlers::project_snapshot);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,5 +192,35 @@
|
|||
"required": ["status"]
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
"kom.meta.v1.project_snapshot": {
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"includeGitStatus": {"type": "boolean"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sections": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"body": {"type": "string"}
|
||||
},
|
||||
"required": ["title", "body"]
|
||||
}
|
||||
},
|
||||
"pg_dsn": {"type": "string"},
|
||||
"notes": {"type": "string"}
|
||||
},
|
||||
"required": ["sections"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ int main() {
|
|||
std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq);
|
||||
if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1;
|
||||
|
||||
const std::string snapshotResp = server.dispatch("kom.meta.v1.project_snapshot", "{}");
|
||||
if (!expect_contains(snapshotResp, "\"sections\"", "snapshot sections key")) return 1;
|
||||
|
||||
const std::string exportReq = R"({"namespace":"tests","destination":"/tmp/example.enc"})";
|
||||
std::string exportResp = server.dispatch("kom.local.v1.backup.export_encrypted", exportReq);
|
||||
if (!expect_contains(exportResp, "\"status\":\"queued\"", "export status")) return 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue