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(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)
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
42
src/main.cpp
42
src/main.cpp
|
|
@ -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>
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
#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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue