From 53a1a043c7a4308234996feac0d360f63d0569f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=A7=CE=B3=CF=86=CF=84=20Kompanion?=
 <chigammaphitau@chelydra.at>
Date: Wed, 15 Oct 2025 12:07:21 +1300
Subject: [PATCH] Add Kconfig and more tools

---
 CMakeLists.txt                    |  44 ++++++++++
 docs/memory-architecture.md       |   2 +
 src/main.cpp                      |  42 +++++++++-
 src/mcp/HandlersIntrospection.hpp | 135 ++++++++++++++++++++++++++++++
 src/mcp/RegisterTools.hpp         |   2 +
 src/mcp/ToolSchemas.json          |  30 +++++++
 tests/contract/test_mcp_tools.cpp |   3 +
 7 files changed, 254 insertions(+), 4 deletions(-)
 create mode 100644 src/mcp/HandlersIntrospection.hpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 559ae1f..5a02469 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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)
diff --git a/docs/memory-architecture.md b/docs/memory-architecture.md
index 6e91ac9..399897c 100644
--- a/docs/memory-architecture.md
+++ b/docs/memory-architecture.md
@@ -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`.
diff --git a/src/main.cpp b/src/main.cpp
index c17666d..50072fd 100644
--- a/src/main.cpp
+++ b/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>
diff --git a/src/mcp/HandlersIntrospection.hpp b/src/mcp/HandlersIntrospection.hpp
new file mode 100644
index 0000000..3e41ae2
--- /dev/null
+++ b/src/mcp/HandlersIntrospection.hpp
@@ -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
diff --git a/src/mcp/RegisterTools.hpp b/src/mcp/RegisterTools.hpp
index 3a5e7d0..2251099 100644
--- a/src/mcp/RegisterTools.hpp
+++ b/src/mcp/RegisterTools.hpp
@@ -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);
 }
diff --git a/src/mcp/ToolSchemas.json b/src/mcp/ToolSchemas.json
index 8c287ae..00dc98f 100644
--- a/src/mcp/ToolSchemas.json
+++ b/src/mcp/ToolSchemas.json
@@ -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"]
+      }
+    }
   }
 }
diff --git a/tests/contract/test_mcp_tools.cpp b/tests/contract/test_mcp_tools.cpp
index 11a3ca4..d69e412 100644
--- a/tests/contract/test_mcp_tools.cpp
+++ b/tests/contract/test_mcp_tools.cpp
@@ -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;
