Add Kconfig and more tools

This commit is contained in:
Χγφτ Kompanion 2025-10-15 12:07:21 +13:00
parent 779ac57f50
commit 53a1a043c7
7 changed files with 254 additions and 4 deletions

View File

@ -23,6 +23,30 @@ endif()
set(KOM_HAVE_PG ${KOM_HAVE_PG} CACHE INTERNAL "kom_dal has Postgres backend") 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 # Placeholder: find Qt and qtmcp when available
# find_package(Qt6 COMPONENTS Core Network REQUIRED) # find_package(Qt6 COMPONENTS Core Network REQUIRED)
# find_package(qtmcp REQUIRED) # find_package(qtmcp REQUIRED)
@ -34,10 +58,30 @@ add_executable(kom_mcp
) )
target_include_directories(kom_mcp PRIVATE src) target_include_directories(kom_mcp PRIVATE src)
target_link_libraries(kom_mcp PRIVATE kom_dal) 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) install(TARGETS kom_mcp RUNTIME DESTINATION bin)
option(BUILD_TESTS "Build tests" ON) 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) if (BUILD_TESTS)
enable_testing() enable_testing()
add_subdirectory(tests) add_subdirectory(tests)

View File

@ -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. - **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. - **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. - **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 ## Next-Step Checklist
- [x] Detect pqxx via CMake and plumb `HAVE_PG`. - [x] Detect pqxx via CMake and plumb `HAVE_PG`.

View File

@ -1,9 +1,17 @@
// Minimal CLI runner that registers Kompanion MCP tools and dispatches requests. // Minimal CLI runner that registers Kompanion MCP tools and dispatches requests.
#include <cstdlib>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <optional>
#include <sstream> #include <sstream>
#include <string> #include <string>
#ifdef HAVE_KCONFIG
#include <KConfigGroup>
#include <KSharedConfig>
#endif
#include "mcp/KomMcpServer.hpp" #include "mcp/KomMcpServer.hpp"
#include "mcp/RegisterTools.hpp" #include "mcp/RegisterTools.hpp"
@ -42,13 +50,40 @@ void print_usage(const char* exe, KomMcpServer& server) {
} // namespace } // 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) { int main(int argc, char** argv) {
KomMcpServer server; KomMcpServer server;
register_default_tools(server); register_default_tools(server);
const char* pgDsn = std::getenv("PG_DSN"); const char* envDsn = std::getenv("PG_DSN");
if (!pgDsn || !*pgDsn) { std::optional<std::string> effectiveDsn;
std::cerr << "[kom_mcp] PG_DSN not set; fallback DAL will be used if available.\n"; 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) { if (argc < 2) {
@ -80,4 +115,3 @@ int main(int argc, char** argv) {
return 1; return 1;
} }
} }
#include <cstdlib>

View File

@ -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

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "KomMcpServer.hpp" #include "KomMcpServer.hpp"
#include "HandlersIntrospection.hpp"
#include "HandlersLocalBackup.hpp" #include "HandlersLocalBackup.hpp"
#include "HandlersMemory.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.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.export_encrypted", Handlers::backup_export_encrypted);
server.registerTool("kom.local.v1.backup.import_encrypted", Handlers::backup_import_encrypted); server.registerTool("kom.local.v1.backup.import_encrypted", Handlers::backup_import_encrypted);
server.registerTool("kom.meta.v1.project_snapshot", Handlers::project_snapshot);
} }

View File

@ -192,5 +192,35 @@
"required": ["status"] "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"]
}
}
} }
} }

View File

@ -49,6 +49,9 @@ int main() {
std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq); std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq);
if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1; 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"})"; const std::string exportReq = R"({"namespace":"tests","destination":"/tmp/example.enc"})";
std::string exportResp = server.dispatch("kom.local.v1.backup.export_encrypted", exportReq); std::string exportResp = server.dispatch("kom.local.v1.backup.export_encrypted", exportReq);
if (!expect_contains(exportResp, "\"status\":\"queued\"", "export status")) return 1; if (!expect_contains(exportResp, "\"status\":\"queued\"", "export status")) return 1;