From 5209d58828a2d384827539047da91c88f0018ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesu=CC=81s=20Pe=CC=81rez?= Date: Fri, 23 Jan 2026 16:13:23 +0000 Subject: [PATCH] chore: add crated --- Cargo.lock | 6908 +++++++++++++++++ Cargo.toml | 79 + crates/kogral-cli/Cargo.toml | 26 + crates/kogral-cli/src/main.rs | 1328 ++++ crates/kogral-core/Cargo.toml | 49 + crates/kogral-core/src/block_parser.rs | 475 ++ crates/kogral-core/src/config/loader.rs | 472 ++ crates/kogral-core/src/config/mod.rs | 53 + crates/kogral-core/src/config/nickel.rs | 281 + crates/kogral-core/src/config/schema.rs | 674 ++ .../kogral-core/src/embeddings/fastembed.rs | 113 + crates/kogral-core/src/embeddings/mod.rs | 20 + crates/kogral-core/src/embeddings/rig.rs | 109 + crates/kogral-core/src/error.rs | 185 + crates/kogral-core/src/export/logseq.rs | 202 + crates/kogral-core/src/export/mod.rs | 4 + crates/kogral-core/src/export/tera.rs | 199 + crates/kogral-core/src/import/logseq.rs | 431 + crates/kogral-core/src/import/mod.rs | 3 + crates/kogral-core/src/inheritance.rs | 160 + crates/kogral-core/src/lib.rs | 77 + crates/kogral-core/src/models.rs | 1147 +++ crates/kogral-core/src/parser.rs | 248 + crates/kogral-core/src/query.rs | 173 + crates/kogral-core/src/regex_patterns.rs | 97 + crates/kogral-core/src/storage/filesystem.rs | 204 + crates/kogral-core/src/storage/memory.rs | 103 + crates/kogral-core/src/storage/mod.rs | 60 + crates/kogral-core/src/storage/surrealdb.rs | 271 + crates/kogral-core/src/sync.rs | 548 ++ .../tests/nickel_integration_test.rs | 83 + crates/kogral-mcp/Cargo.toml | 25 + crates/kogral-mcp/src/auth.rs | 164 + crates/kogral-mcp/src/lib.rs | 15 + crates/kogral-mcp/src/main.rs | 25 + crates/kogral-mcp/src/prompts.rs | 296 + crates/kogral-mcp/src/resources.rs | 137 + crates/kogral-mcp/src/server.rs | 224 + crates/kogral-mcp/src/tools.rs | 800 ++ crates/kogral-mcp/src/types.rs | 191 + crates/kogral-mcp/src/validation.rs | 219 + crates/kogral-mcp/tests/integration_test.rs | 60 + 42 files changed, 16938 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/kogral-cli/Cargo.toml create mode 100644 crates/kogral-cli/src/main.rs create mode 100644 crates/kogral-core/Cargo.toml create mode 100644 crates/kogral-core/src/block_parser.rs create mode 100644 crates/kogral-core/src/config/loader.rs create mode 100644 crates/kogral-core/src/config/mod.rs create mode 100644 crates/kogral-core/src/config/nickel.rs create mode 100644 crates/kogral-core/src/config/schema.rs create mode 100644 crates/kogral-core/src/embeddings/fastembed.rs create mode 100644 crates/kogral-core/src/embeddings/mod.rs create mode 100644 crates/kogral-core/src/embeddings/rig.rs create mode 100644 crates/kogral-core/src/error.rs create mode 100644 crates/kogral-core/src/export/logseq.rs create mode 100644 crates/kogral-core/src/export/mod.rs create mode 100644 crates/kogral-core/src/export/tera.rs create mode 100644 crates/kogral-core/src/import/logseq.rs create mode 100644 crates/kogral-core/src/import/mod.rs create mode 100644 crates/kogral-core/src/inheritance.rs create mode 100644 crates/kogral-core/src/lib.rs create mode 100644 crates/kogral-core/src/models.rs create mode 100644 crates/kogral-core/src/parser.rs create mode 100644 crates/kogral-core/src/query.rs create mode 100644 crates/kogral-core/src/regex_patterns.rs create mode 100644 crates/kogral-core/src/storage/filesystem.rs create mode 100644 crates/kogral-core/src/storage/memory.rs create mode 100644 crates/kogral-core/src/storage/mod.rs create mode 100644 crates/kogral-core/src/storage/surrealdb.rs create mode 100644 crates/kogral-core/src/sync.rs create mode 100644 crates/kogral-core/tests/nickel_integration_test.rs create mode 100644 crates/kogral-mcp/Cargo.toml create mode 100644 crates/kogral-mcp/src/auth.rs create mode 100644 crates/kogral-mcp/src/lib.rs create mode 100644 crates/kogral-mcp/src/main.rs create mode 100644 crates/kogral-mcp/src/prompts.rs create mode 100644 crates/kogral-mcp/src/resources.rs create mode 100644 crates/kogral-mcp/src/server.rs create mode 100644 crates/kogral-mcp/src/tools.rs create mode 100644 crates/kogral-mcp/src/types.rs create mode 100644 crates/kogral-mcp/src/validation.rs create mode 100644 crates/kogral-mcp/tests/integration_test.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..aa95ffc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6908 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "addr" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93b8a41dbe230ad5087cc721f8d41611de654542180586b315d9f4cf6b72bef" +dependencies = [ + "psl-types", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice 0.2.1", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ammonia" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" +dependencies = [ + "cssparser", + "html5ever", + "maplit", + "tendril", + "url", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "any_ascii" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90c6333e01ba7235575b6ab53e5af10f1c327927fd97c36462917e289557ea64" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-any" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063" + +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.7", + "stable_deref_trait", +] + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-graphql" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b75e6d81f69e47038fb2f08c54dc9180fabef56856b7a74e4082157f2e5536" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-io", + "async-trait", + "asynk-strim", + "base64 0.22.1", + "bytes", + "fnv", + "futures-util", + "http", + "indexmap 2.13.0", + "mime", + "multer", + "num-traits", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions_next", + "thiserror 2.0.17", +] + +[[package]] +name = "async-graphql-derive" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8587c1c72749f54250633a725203d537ebda851b68d85c2a8d18a3adc0bf72d6" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling 0.23.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "strum", + "syn 2.0.114", + "thiserror 2.0.17", +] + +[[package]] +name = "async-graphql-parser" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577ec8cb624048d11465439c2b25d28362cb08c154b530421f456debc7083fdf" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747684314ff7454a1f3b6fe5341e15148b1f17f30c9f6ecc55832dd1f053c47" +dependencies = [ + "bytes", + "indexmap 2.13.0", + "serde", + "serde_json", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "asynk-strim" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52697735bdaac441a29391a9e97102c74c6ef0f9b60a40cf109b1b404e29d2f6" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcrypt" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.2.17", + "subtle", + "zeroize", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cedar-policy" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d91e3b10a0f7f2911774d5e49713c4d25753466f9e11d1cd2ec627f8a2dc857" +dependencies = [ + "cedar-policy-core", + "cedar-policy-validator", + "itertools 0.10.5", + "lalrpop-util", + "ref-cast", + "serde", + "serde_json", + "smol_str", + "thiserror 1.0.69", +] + +[[package]] +name = "cedar-policy-core" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd2315591c6b7e18f8038f0a0529f254235fd902b6c217aabc04f2459b0d9995" +dependencies = [ + "either", + "ipnet", + "itertools 0.10.5", + "lalrpop", + "lalrpop-util", + "lazy_static", + "miette", + "regex", + "rustc_lexer", + "serde", + "serde_json", + "serde_with", + "smol_str", + "stacker", + "thiserror 1.0.69", +] + +[[package]] +name = "cedar-policy-validator" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e756e1b2a5da742ed97e65199ad6d0893e9aa4bd6b34be1de9e70bd1e6adc7df" +dependencies = [ + "cedar-policy-core", + "itertools 0.10.5", + "serde", + "serde_json", + "serde_with", + "smol_str", + "stacker", + "thiserror 1.0.69", + "unicode-security", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dary_heap" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" +dependencies = [ + "serde", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.114", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dmp" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2dfc7a18dffd3ef60a442b72a827126f1557d914620f8fc4d1049916da43c1" +dependencies = [ + "trice", + "urlencoding", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "earcutr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01" +dependencies = [ + "itertools 0.11.0", + "num-traits", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom 7.1.3", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastembed" +version = "5.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a3f841f27a44bcc32214f8df75cc9b6cea55dbbebbfe546735690eab5bb2d2" +dependencies = [ + "anyhow", + "hf-hub", + "image", + "ndarray 0.17.2", + "ort", + "safetensors", + "serde", + "serde_json", + "tokenizers", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "fst" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "geo" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f811f663912a69249fa620dcd2a005db7254529da2d8a0b23942e81f47084501" +dependencies = [ + "earcutr", + "float_next_after", + "geo-types", + "geographiclib-rs", + "log", + "num-traits", + "robust", + "rstar 0.12.2", + "serde", + "spade", +] + +[[package]] +name = "geo-types" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f8647af4005fa11da47cd56252c6ef030be8fa97bdbf355e7dfb6348f0a82c" +dependencies = [ + "approx 0.5.1", + "num-traits", + "rstar 0.10.0", + "rstar 0.11.0", + "rstar 0.12.2", + "rstar 0.8.4", + "rstar 0.9.3", + "serde", +] + +[[package]] +name = "geographiclib-rs" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f611040a2bb37eaa29a78a128d1e92a378a03e0b6e66ae27398d42b1ba9a7841" +dependencies = [ + "libm", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.2", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.10.0", + "ignore", + "walkdir", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "heapless" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" +dependencies = [ + "as-slice 0.1.5", + "generic-array 0.14.7", + "hash32 0.1.1", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hf-hub" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" +dependencies = [ + "dirs", + "http", + "indicatif", + "libc", + "log", + "native-tls", + "rand 0.9.2", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.17", + "ureq 2.12.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" + +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.5", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.9", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.2", + "web-time", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.10.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kogral-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "colored", + "dirs", + "kogral-core", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "kogral-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "dashmap 6.1.0", + "fastembed", + "futures", + "mockito", + "notify", + "once_cell", + "pulldown-cmark", + "regex", + "rig-core", + "serde", + "serde_json", + "serde_yaml", + "surrealdb", + "tempfile", + "tera", + "thiserror 2.0.17", + "tokio", + "toml", + "tracing", + "uuid", + "walkdir", +] + +[[package]] +name = "kogral-mcp" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "kogral-core", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.11.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "lexicmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7378d131ddf24063b32cbd7e91668d183140c4b3906270635a4d633d1068ea5d" +dependencies = [ + "any_ascii", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "linfa-linalg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e7562b41c8876d3367897067013bb2884cc78e6893f092ecd26b305176ac82" +dependencies = [ + "ndarray 0.15.6", + "num-traits", + "rand 0.8.5", + "thiserror 1.0.69", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rust2" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror 1.0.69", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockito" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0603425789b4a70fcc4ac4f5a46a566c116ee3e2a6b768dc623f7719c611de" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "pin-project-lite", + "rand 0.9.2", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "monostate" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "approx 0.4.0", + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "ndarray-stats" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af5a8477ac96877b5bd1fd67e0c28736c12943aba24eda92b127e036b0c8f400" +dependencies = [ + "indexmap 1.9.3", + "itertools 0.10.5", + "ndarray 0.15.6", + "noisy_float", + "num-integer", + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "noisy_float" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16843be85dd410c6a12251c4eca0dd1d3ee8c5725f746c4d5e0fdcec0a864b2" +dependencies = [ + "num-traits", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.10.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + +[[package]] +name = "ntapi" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "object_store" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c1be0c6c22ec0817cdc77d3842f721a17fd30ab6965001415b5402a74e6b740" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures", + "http", + "humantime", + "itertools 0.14.0", + "parking_lot", + "percent-encoding", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", + "walkdir", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.10.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ort" +version = "2.0.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5df903c0d2c07b56950f1058104ab0c8557159f2741782223704de9be73c3c" +dependencies = [ + "ndarray 0.17.2", + "ort-sys", + "smallvec", + "tracing", + "ureq 3.1.4", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06503bb33f294c5f1ba484011e053bfa6ae227074bdb841e9863492dc5960d4b" +dependencies = [ + "hmac-sha256", + "lzma-rust2", + "ureq 3.1.4", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pdqselect" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.13.0", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.114", + "unicase", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", + "unicase", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "psm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.10.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick_cache" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb55a1aa7668676bb93926cd4e9cdfe60f03bb866553bcca9112554911b6d3dc" +dependencies = [ + "ahash 0.8.12", + "equivalent", + "hashbrown 0.14.5", + "parking_lot", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", + "serde", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.17", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools 0.14.0", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reblessive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc4a4ea2a66a41a1152c4b3d86e8954dc087bdf33af35446e6e176db4e73c8c" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.5", +] + +[[package]] +name = "revision" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b8ee532f15b2f0811eb1a50adf10d036e14a6cdae8d99893e7f3b921cb227d" +dependencies = [ + "chrono", + "geo", + "regex", + "revision-derive", + "roaring", + "rust_decimal", + "uuid", +] + +[[package]] +name = "revision-derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3415e1bc838c36f9a0a2ac60c0fa0851c72297685e66592c44870d82834dfa2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + +[[package]] +name = "rig-core" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1a48121c1ecd6f6ce59d64ec353c791aac6fc07bf4aa353380e8185659e6eb" +dependencies = [ + "as-any", + "async-stream", + "base64 0.22.1", + "bytes", + "eventsource-stream", + "fastrand", + "futures", + "futures-timer", + "glob", + "http", + "mime", + "mime_guess", + "ordered-float", + "pin-project-lite", + "reqwest", + "schemars 1.2.0", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-futures", + "url", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmpv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" +dependencies = [ + "rmp", +] + +[[package]] +name = "roaring" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e8d2cfa184d94d0726d650a9f4a1be7f9b76ac9fdb954219878dc00c1c1e7b" +dependencies = [ + "bytemuck", + "byteorder", + "serde", +] + +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + +[[package]] +name = "rstar" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a45c0e8804d37e4d97e55c6f258bc9ad9c5ee7b07437009dd152d764949a27c" +dependencies = [ + "heapless 0.6.1", + "num-traits", + "pdqselect", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40f1bfe5acdab44bc63e6699c28b74f75ec43afb59f3eda01e145aff86a25fa" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f39465655a1e3d8ae79c6d9e007f4953bfc5d55297602df9dc38f9ae9f1359a" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless 0.8.0", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_lexer" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86aae0c77166108c01305ee1a36a1e77289d7dc6ca0a3cd91ff4992de2d16a5" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "safetensors" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5" +dependencies = [ + "hashbrown 0.16.1", + "serde", + "serde_json", +] + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.114", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-content" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "spade" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb313e1c8afee5b5647e00ee0fe6855e3d529eb863a0fdae1d60006c4d1e9990" +dependencies = [ + "hashbrown 0.15.5", + "num-traits", + "robust", + "smallvec", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom 7.1.3", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + +[[package]] +name = "storekey" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c42833834a5d23b344f71d87114e0cc9994766a5c42938f4b50e7b2aef85b2" +dependencies = [ + "byteorder", + "memchr", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "surrealdb" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f921fcafdc840d36a4378ef7639fcb2731a21a858b048de83f0bd7194c242479" +dependencies = [ + "arrayvec", + "async-channel", + "bincode", + "chrono", + "dmp", + "futures", + "geo", + "getrandom 0.3.4", + "indexmap 2.13.0", + "path-clean", + "pharos", + "reblessive", + "reqwest", + "revision", + "ring", + "rust_decimal", + "rustls", + "rustls-pki-types", + "semver", + "serde", + "serde-content", + "serde_json", + "surrealdb-core", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tracing", + "trice", + "url", + "uuid", + "wasm-bindgen-futures", + "wasmtimer", + "ws_stream_wasm", +] + +[[package]] +name = "surrealdb-core" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae1a46c6d68a61c0a270f456a152433093f4d5c0e71c45eea64f95e95d68bd9" +dependencies = [ + "addr", + "ahash 0.8.12", + "ammonia", + "any_ascii", + "argon2", + "async-channel", + "async-executor", + "async-graphql", + "base64 0.21.7", + "bcrypt", + "bincode", + "blake3", + "bytes", + "castaway", + "cedar-policy", + "chrono", + "ciborium", + "dashmap 5.5.3", + "deunicode", + "dmp", + "fst", + "futures", + "fuzzy-matcher", + "geo", + "geo-types", + "getrandom 0.3.4", + "hex", + "http", + "ipnet", + "jsonwebtoken", + "lexicmp", + "linfa-linalg", + "md-5", + "nanoid", + "ndarray 0.15.6", + "ndarray-stats", + "num-traits", + "num_cpus", + "object_store", + "parking_lot", + "pbkdf2", + "pharos", + "phf", + "pin-project-lite", + "quick_cache", + "radix_trie", + "rand 0.8.5", + "rayon", + "reblessive", + "regex", + "revision", + "ring", + "rmpv", + "roaring", + "rust-stemmers", + "rust_decimal", + "scrypt", + "semver", + "serde", + "serde-content", + "serde_json", + "sha1", + "sha2", + "snap", + "storekey", + "strsim", + "subtle", + "sysinfo", + "thiserror 1.0.69", + "tokio", + "tracing", + "trice", + "ulid", + "unicase", + "url", + "uuid", + "vart", + "wasm-bindgen-futures", + "wasmtimer", + "ws_stream_wasm", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" +dependencies = [ + "ahash 0.8.12", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "getrandom 0.3.4", + "itertools 0.14.0", + "log", + "macro_rules_attribute", + "monostate", + "onig", + "paste", + "rand 0.9.2", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.17", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "futures", + "futures-task", + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trice" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3aaab10ae9fac0b10f392752bf56f0fd20845f39037fec931e8537b105b515a" +dependencies = [ + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "serde", + "web-time", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec", +] + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-security" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e4ddba1535dd35ed8b61c52166b7155d7f4e4b8847cec6f48e71dc66d8b5e50" +dependencies = [ + "unicode-normalization", + "unicode-script", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "native-tls", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +dependencies = [ + "base64 0.22.1", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf-8", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vart" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87782b74f898179396e93c0efabb38de0d58d50bbd47eae00c71b3a1144dbbae" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmtimer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7ed9d8b15c7fb594d72bfb4b5a276f3d2029333cd93a932f376f5937f6f80ee" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c86acb70a85b2c16f071f171847d1945e8f44812630463cd14ec83900ad01c" +dependencies = [ + "zune-core 0.5.1", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..628da29 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,79 @@ + +[workspace] +members = [ + "crates/kogral-core", + "crates/kogral-mcp", + "crates/kogral-cli", +] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +rust-version = "1.70" +authors = ["Tools Ecosystem"] +license = "MIT OR Apache-2.0" +homepage = "https://kogral.dev" +repository = "https://github.com/vapora/kogral" +description = "KOGRAL - Git-native knowledge graphs for developer teams" + +[workspace.dependencies] +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.9" +toml = "0.9" + +# Async runtime +tokio = { version = "1.49", features = ["full"] } + +# Error handling +thiserror = "2.0" +anyhow = "1.0" + +# Markdown parsing +pulldown-cmark = "0.13" + +# Template engine +tera = "1.20" + +# Embeddings +rig-core = "0.28" + +# Storage +surrealdb = "2.4" +dashmap = "6.1" + +# File watching +notify = "8.2" + +# Time handling +chrono = { version = "0.4", features = ["serde"] } + +# UUID generation +uuid = { version = "1.19", features = ["v4", "serde"] } + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# CLI +clap = { version = "4.5", features = ["derive"] } +colored = "3.0" +dirs = "6.0" + +# Testing +mockito = "1.7" +tempfile = "3.24" + +# Async utilities +async-trait = "0.1" +futures = "0.3" + +# File operations +walkdir = "2.5" +regex = "1.12" +once_cell = "1.20" + +# Embeddings (optional) +fastembed = "5.8" diff --git a/crates/kogral-cli/Cargo.toml b/crates/kogral-cli/Cargo.toml new file mode 100644 index 0000000..bb596c3 --- /dev/null +++ b/crates/kogral-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "kogral-cli" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "KOGRAL CLI - Command-line interface for git-native knowledge graphs" + +[[bin]] +name = "kogral" +path = "src/main.rs" + +[dependencies] +kogral-core = { path = "../kogral-core" } +clap = { workspace = true, features = ["derive", "env"] } +tokio = { workspace = true, features = ["full"] } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +colored = { workspace = true } +dirs = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } diff --git a/crates/kogral-cli/src/main.rs b/crates/kogral-cli/src/main.rs new file mode 100644 index 0000000..d927ce3 --- /dev/null +++ b/crates/kogral-cli/src/main.rs @@ -0,0 +1,1328 @@ +//! KOGRAL CLI +//! +//! Command-line interface for managing knowledge graphs with Logseq +//! compatibility. + +#![forbid(unsafe_code)] + +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use colored::Colorize; +use kogral_core::{ + config::KbConfig, + models::{Edge, EdgeType, Graph, Node, NodeStatus, NodeType}, + storage::{filesystem::FilesystemStorage, Storage}, +}; +use tracing::{error, info}; + +/// KOGRAL CLI - Manage knowledge graphs with Logseq compatibility +#[derive(Parser, Debug)] +#[command(name = "kogral")] +#[command(version, about, long_about = None)] +struct Cli { + /// Config file path (.ncl, .toml, or .json) + #[arg(short, long, global = true)] + config: Option, + + /// Project directory (defaults to current directory) + #[arg(short, long, global = true)] + project: Option, + + /// Enable verbose logging + #[arg(short, long, global = true)] + verbose: bool, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Initialize .kogral/ directory in current project + Init { + /// Project name + #[arg(short, long)] + name: Option, + + /// Graph description + #[arg(short, long)] + description: Option, + + /// Force initialization even if .kogral/ exists + #[arg(short, long)] + force: bool, + }, + + /// Add a new node to KOGRAL + Add { + /// Node type + #[arg(value_enum)] + node_type: CliNodeType, + + /// Node title + title: String, + + /// Content (if not provided, opens editor) + #[arg(short, long)] + content: Option, + + /// Tags (comma-separated) + #[arg(short, long)] + tags: Option, + + /// Related node IDs (comma-separated) + #[arg(short, long)] + relates_to: Option, + }, + + /// Search KOGRAL + Search { + /// Search query + query: String, + + /// Node type filter + #[arg(short = 't', long)] + node_type: Option, + + /// Maximum number of results + #[arg(short, long, default_value = "10")] + limit: usize, + + /// Use semantic search (requires embeddings) + #[arg(short, long)] + semantic: bool, + }, + + /// Create a relationship between two nodes + Link { + /// Source node ID or title + from: String, + + /// Target node ID or title + to: String, + + /// Relationship type + #[arg(value_enum)] + relation: CliEdgeType, + + /// Relationship strength (0.0-1.0) + #[arg(short, long, default_value = "1.0")] + strength: f32, + }, + + /// Sync filesystem with storage backend + Sync { + /// Direction: to-storage, from-storage, or bidirectional + #[arg(short, long, default_value = "bidirectional")] + direction: String, + + /// Dry run (show what would be synced) + #[arg(short, long)] + dry_run: bool, + }, + + /// Start MCP server + Serve { + /// Transport type (stdio or sse) + #[arg(short, long, default_value = "stdio")] + transport: String, + + /// Port (for SSE transport) + #[arg(short, long, default_value = "3000")] + port: u16, + }, + + /// Show graph visualization + Graph { + /// Output format (ascii, json, or dot) + #[arg(short, long, default_value = "ascii")] + format: String, + + /// Filter by node type + #[arg(short = 't', long)] + node_type: Option, + + /// Show only nodes with specific tag + #[arg(long)] + tag: Option, + }, + + /// Import from external sources + Import { + /// Source type + #[arg(value_enum)] + source: ImportSource, + + /// Source path + path: PathBuf, + + /// Dry run (show what would be imported) + #[arg(short, long)] + dry_run: bool, + }, + + /// Export to external formats + Export { + /// Export format + #[arg(value_enum)] + format: ExportFormat, + + /// Output path + output: PathBuf, + + /// Filter by node type + #[arg(short = 't', long)] + node_type: Option, + }, + + /// List all nodes + List { + /// Node type filter + #[arg(short = 't', long)] + node_type: Option, + + /// Status filter + #[arg(short, long)] + status: Option, + + /// Tag filter + #[arg(long)] + tag: Option, + }, + + /// Show node details + Show { + /// Node ID or title + node: String, + + /// Show related nodes + #[arg(short, long)] + relations: bool, + }, + + /// Delete a node + Delete { + /// Node ID or title + node: String, + + /// Also delete relationships + #[arg(short, long)] + cascade: bool, + + /// Skip confirmation + #[arg(short, long)] + force: bool, + }, + + /// Manage configuration + Config { + /// Show current configuration + #[arg(short, long)] + show: bool, + + /// Get a specific config value + #[arg(short, long)] + get: Option, + + /// Set a config value + #[arg(short, long)] + set: Option, + + /// Config value (for --set) + value: Option, + }, +} + +#[derive(Debug, Clone, clap::ValueEnum)] +enum CliNodeType { + Note, + Decision, + Guideline, + Pattern, + Journal, + Execution, +} + +impl From for NodeType { + fn from(cli_type: CliNodeType) -> Self { + match cli_type { + CliNodeType::Note => NodeType::Note, + CliNodeType::Decision => NodeType::Decision, + CliNodeType::Guideline => NodeType::Guideline, + CliNodeType::Pattern => NodeType::Pattern, + CliNodeType::Journal => NodeType::Journal, + CliNodeType::Execution => NodeType::Execution, + } + } +} + +#[derive(Debug, Clone, clap::ValueEnum)] +enum CliEdgeType { + RelatesTo, + DependsOn, + Implements, + Extends, + Supersedes, + Explains, +} + +impl From for EdgeType { + fn from(cli_type: CliEdgeType) -> Self { + match cli_type { + CliEdgeType::RelatesTo => EdgeType::RelatesTo, + CliEdgeType::DependsOn => EdgeType::DependsOn, + CliEdgeType::Implements => EdgeType::Implements, + CliEdgeType::Extends => EdgeType::Extends, + CliEdgeType::Supersedes => EdgeType::Supersedes, + CliEdgeType::Explains => EdgeType::Explains, + } + } +} + +#[derive(Debug, Clone, clap::ValueEnum)] +enum CliNodeStatus { + Draft, + Active, + Superseded, + Archived, +} + +impl From for NodeStatus { + fn from(cli_status: CliNodeStatus) -> Self { + match cli_status { + CliNodeStatus::Draft => NodeStatus::Draft, + CliNodeStatus::Active => NodeStatus::Active, + CliNodeStatus::Superseded => NodeStatus::Superseded, + CliNodeStatus::Archived => NodeStatus::Archived, + } + } +} + +#[derive(Debug, Clone, clap::ValueEnum)] +enum ImportSource { + Logseq, + Markdown, + Json, +} + +#[derive(Debug, Clone, clap::ValueEnum)] +enum ExportFormat { + Logseq, + Markdown, + Json, + Summary, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + // Initialize tracing + let log_level = if cli.verbose { "debug" } else { "info" }; + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level)), + ) + .init(); + + // Determine project directory + let project_dir = cli + .project + .clone() + .unwrap_or_else(|| std::env::current_dir().expect("Failed to get current directory")); + + info!("Project directory: {}", project_dir.display()); + + // Load configuration (if exists) + let config = load_config(&project_dir, cli.config.as_deref()).ok(); + + // Execute command + match cli.command { + Commands::Init { + name, + description, + force, + } => cmd_init(&project_dir, name, description, force).await, + Commands::Add { + node_type, + title, + content, + tags, + relates_to, + } => { + cmd_add( + &project_dir, + config, + node_type, + title, + content, + tags, + relates_to, + ) + .await + } + Commands::Search { + query, + node_type, + limit, + semantic, + } => cmd_search(&project_dir, config, query, node_type, limit, semantic).await, + Commands::Link { + from, + to, + relation, + strength, + } => cmd_link(&project_dir, config, from, to, relation, strength).await, + Commands::Sync { direction, dry_run } => { + cmd_sync(&project_dir, config, direction, dry_run).await + } + Commands::Serve { transport, port } => { + cmd_serve(&project_dir, config, transport, port).await + } + Commands::Graph { + format, + node_type, + tag, + } => cmd_graph(&project_dir, config, format, node_type, tag).await, + Commands::Import { + source, + path, + dry_run, + } => cmd_import(&project_dir, config, source, path, dry_run).await, + Commands::Export { + format, + output, + node_type, + } => cmd_export(&project_dir, config, format, output, node_type).await, + Commands::List { + node_type, + status, + tag, + } => cmd_list(&project_dir, config, node_type, status, tag).await, + Commands::Show { node, relations } => cmd_show(&project_dir, config, node, relations).await, + Commands::Delete { + node, + cascade, + force, + } => cmd_delete(&project_dir, config, node, cascade, force).await, + Commands::Config { + show, + get, + set, + value, + } => cmd_config(&project_dir, config, show, get, set, value).await, + } +} + +/// Load configuration from project directory +fn load_config(project_dir: &Path, config_path: Option<&Path>) -> Result { + kogral_core::config::load_config(Some(project_dir), config_path) + .context("Failed to load configuration") +} + +/// Initialize .kogral/ directory +async fn cmd_init( + project_dir: &Path, + name: Option, + description: Option, + force: bool, +) -> Result<()> { + let kogral_dir = project_dir.join(".kogral"); + + if kogral_dir.exists() && !force { + error!("Directory .kogral/ already exists. Use --force to reinitialize."); + return Err(anyhow::anyhow!("KOGRAL directory already exists")); + } + + info!("Initializing KOGRAL in {}", project_dir.display()); + + // Create directory structure + std::fs::create_dir_all(&kogral_dir)?; + std::fs::create_dir_all(kogral_dir.join("notes"))?; + std::fs::create_dir_all(kogral_dir.join("decisions"))?; + std::fs::create_dir_all(kogral_dir.join("guidelines"))?; + std::fs::create_dir_all(kogral_dir.join("patterns"))?; + std::fs::create_dir_all(kogral_dir.join("journal"))?; + + // Create default config.toml + let project_name = name.unwrap_or_else(|| { + project_dir + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("my-kb") + .to_string() + }); + + let config_content = format!( + r#"[graph] +name = "{}" +version = "1.0.0" +description = "{}" + +[storage] +primary = "filesystem" + +[embeddings] +enabled = false + +[templates] +templates_dir = "templates" + +[query] +similarity_threshold = 0.4 +max_results = 10 + +[mcp.server] +name = "kogral-mcp" +transport = "stdio" + +[sync] +auto_index = false +watch_paths = ["notes", "decisions", "guidelines", "patterns", "journal"] +"#, + project_name, + description.unwrap_or_else(|| format!("{} KOGRAL", project_name)) + ); + + std::fs::write(kogral_dir.join("config.toml"), config_content)?; + + println!("{}", "✓ Knowledge base initialized successfully".green()); + println!(" Project: {}", project_name.bright_blue()); + println!( + " Location: {}", + kogral_dir.display().to_string().bright_black() + ); + println!("\nNext steps:"); + println!( + " {} Add your first note", + "kb add note \"My First Note\"".bright_yellow() + ); + println!(" {} Search KOGRAL", "kb search \"topic\"".bright_yellow()); + println!(" {} List all nodes", "kb list".bright_yellow()); + + Ok(()) +} + +/// Add a new node +async fn cmd_add( + project_dir: &PathBuf, + config: Option, + node_type: CliNodeType, + title: String, + content: Option, + tags: Option, + relates_to: Option, +) -> Result<()> { + info!("Adding new {:?} node: {}", node_type, title); + + let mut node = Node::new(node_type.into(), title.clone()); + + // Set content + if let Some(content_text) = content { + node.content = content_text; + } + + // Parse tags + if let Some(tags_str) = tags { + node.tags = tags_str.split(',').map(|s| s.trim().to_string()).collect(); + } + + // Parse relationships + if let Some(relates_str) = relates_to { + node.relates_to = relates_str + .split(',') + .map(|s| s.trim().to_string()) + .collect(); + } + + // Create storage and save node + let kogral_dir = project_dir.join(".kogral"); + let mut storage = FilesystemStorage::new(kogral_dir); + let graph_name = config + .as_ref() + .map(|c| c.graph.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let mut graph = storage + .load_graph(&graph_name) + .await + .unwrap_or_else(|_| Graph::new(graph_name.clone())); + + graph + .add_node(node.clone()) + .map_err(|e| anyhow::anyhow!(e))?; + storage.save_graph(&graph).await?; + + println!("{}", "✓ Node added successfully".green()); + println!(" ID: {}", node.id.bright_blue()); + println!(" Type: {:?}", node.node_type); + println!(" Title: {}", node.title); + + Ok(()) +} + +/// Search KOGRAL +async fn cmd_search( + project_dir: &PathBuf, + config: Option, + query: String, + node_type: Option, + limit: usize, + semantic: bool, +) -> Result<()> { + info!("Searching for: {}", query); + + if semantic { + println!( + "{}", + "⚠ Semantic search requires embeddings configuration".yellow() + ); + } + + // Create storage and load graph + let kogral_dir = project_dir.join(".kogral"); + let storage = FilesystemStorage::new(kogral_dir); + let graph_name = config + .as_ref() + .map(|c| c.graph.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let graph = storage.load_graph(&graph_name).await?; + + // Simple text search + let results: Vec<&Node> = graph + .nodes + .values() + .filter(|node| { + let matches_query = node.title.to_lowercase().contains(&query.to_lowercase()) + || node.content.to_lowercase().contains(&query.to_lowercase()) + || node + .tags + .iter() + .any(|t| t.to_lowercase().contains(&query.to_lowercase())); + + let matches_type = node_type + .as_ref() + .map(|t| { + let node_type: NodeType = t.clone().into(); + node.node_type == node_type + }) + .unwrap_or(true); + + matches_query && matches_type + }) + .take(limit) + .collect(); + + if results.is_empty() { + println!("{}", "No results found".yellow()); + return Ok(()); + } + + println!("\n{} {} result(s):", "Found".green(), results.len()); + for (i, node) in results.iter().enumerate() { + println!( + "\n{}. {} ({})", + i + 1, + node.title.bright_blue(), + node.id.bright_black() + ); + println!(" Type: {:?} | Status: {:?}", node.node_type, node.status); + if !node.tags.is_empty() { + println!(" Tags: {}", node.tags.join(", ").bright_yellow()); + } + if !node.content.is_empty() { + let preview = node + .content + .lines() + .next() + .unwrap_or("") + .chars() + .take(80) + .collect::(); + println!(" {}", preview.bright_black()); + } + } + + Ok(()) +} + +/// Create a relationship between nodes +async fn cmd_link( + project_dir: &PathBuf, + config: Option, + from: String, + to: String, + relation: CliEdgeType, + strength: f32, +) -> Result<()> { + info!( + "Creating link: {} -> {} (relation: {:?})", + from, to, relation + ); + + let kogral_dir = project_dir.join(".kogral"); + let mut storage = FilesystemStorage::new(kogral_dir); + let graph_name = config + .as_ref() + .map(|c| c.graph.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let mut graph = storage.load_graph(&graph_name).await?; + + // Find nodes by ID or title and clone data we need + let from_node_data = graph + .nodes + .values() + .find(|n| n.id == from || n.title == from) + .map(|n| (n.id.clone(), n.title.clone())) + .ok_or_else(|| anyhow::anyhow!("Source node not found: {}", from))?; + + let to_node_data = graph + .nodes + .values() + .find(|n| n.id == to || n.title == to) + .map(|n| (n.id.clone(), n.title.clone())) + .ok_or_else(|| anyhow::anyhow!("Target node not found: {}", to))?; + + let mut edge = Edge::new( + from_node_data.0.clone(), + to_node_data.0.clone(), + relation.into(), + ); + edge.strength = strength; + + graph.add_edge(edge).map_err(|e| anyhow::anyhow!(e))?; + storage.save_graph(&graph).await?; + + println!("{}", "✓ Relationship created successfully".green()); + println!( + " {} -> {}", + from_node_data.1.bright_blue(), + to_node_data.1.bright_blue() + ); + + Ok(()) +} + +/// Sync filesystem with storage +async fn cmd_sync( + _project_dir: &PathBuf, + config: Option, + direction: String, + dry_run: bool, +) -> Result<()> { + info!("Sync direction: {}, dry_run: {}", direction, dry_run); + + if dry_run { + println!("{}", "Dry run mode - no changes will be made".yellow()); + } + + // For now, sync is not fully functional without both filesystem and remote + // storage configured. This is a stub that shows the intended workflow. + println!( + "{}", + "⚠ Sync requires SurrealDB backend configuration".yellow() + ); + println!(" Current direction: {}", direction); + + if let Some(cfg) = config { + println!(" Graph: {}", cfg.graph.name); + println!(" Storage: {:?}", cfg.storage.primary); + } + + println!( + "\n{}", + "To enable sync, configure both filesystem and SurrealDB backends".italic() + ); + + Ok(()) +} + +/// Start MCP server +async fn cmd_serve( + _project_dir: &PathBuf, + _config: Option, + transport: String, + port: u16, +) -> Result<()> { + info!( + "Starting MCP server: transport={}, port={}", + transport, port + ); + + println!("{}", "Starting MCP server...".green()); + println!(" Transport: {}", transport); + if transport == "sse" { + println!(" Port: {}", port); + } + println!(" Protocol: JSON-RPC 2.0"); + + println!( + "\n{}", + "Configure in ~/.config/claude/config.json:".bright_black() + ); + println!( + " \"mcpServers\": {{\n \"kogral\": {{\n \"command\": \"kogral serve --transport \ + {}\"\n }}\n }}", + transport + ); + + // In a real implementation, this would start the MCP server + // For now, just show the configuration + println!( + "{}", + "\n⚠ MCP server implementation in kogral-mcp crate".yellow() + ); + + Ok(()) +} + +/// Show graph visualization +#[allow(clippy::excessive_nesting)] +async fn cmd_graph( + project_dir: &PathBuf, + config: Option, + format: String, + node_type: Option, + tag: Option, +) -> Result<()> { + info!("Graph format: {}", format); + + let kogral_dir = project_dir.join(".kogral"); + let storage = FilesystemStorage::new(kogral_dir); + let graph_name = config + .as_ref() + .map(|c| c.graph.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let graph = storage.load_graph(&graph_name).await?; + + // Filter nodes + let nodes: Vec<&Node> = graph + .nodes + .values() + .filter(|node| { + let type_match = node_type + .as_ref() + .map(|t| { + let nt: NodeType = t.clone().into(); + node.node_type == nt + }) + .unwrap_or(true); + + let tag_match = tag.as_ref().map(|t| node.tags.contains(t)).unwrap_or(true); + + type_match && tag_match + }) + .collect(); + + match format.as_str() { + "json" => { + let json = serde_json::to_string_pretty(&graph)?; + println!("{}", json); + } + "dot" => { + println!("digraph KB {{"); + for node in &nodes { + println!(" \"{}\" [label=\"{}\"];", node.id, node.title); + } + for edge in &graph.edges { + println!( + " \"{}\" -> \"{}\" [label=\"{:?}\"];", + edge.from, edge.to, edge.edge_type + ); + } + println!("}}"); + } + _ => { + println!("\n{} Knowledge Base Graph", "═".repeat(50)); + println!("Graph: {}", graph.name.bright_blue()); + println!("Nodes: {} | Edges: {}", nodes.len(), graph.edges.len()); + println!("{}\n", "═".repeat(50)); + + for node in &nodes { + println!( + "● {} ({})", + node.title.bright_blue(), + format!("{:?}", node.node_type).bright_black() + ); + println!(" ID: {}", node.id.bright_black()); + if !node.tags.is_empty() { + println!(" Tags: {}", node.tags.join(", ").bright_yellow()); + } + // Show outgoing relationships + let out_edges: Vec<_> = graph.edges.iter().filter(|e| e.from == node.id).collect(); + if !out_edges.is_empty() { + for edge in out_edges { + if let Some(target) = graph.get_node(&edge.to) { + println!(" └─ {:?} → {}", edge.edge_type, target.title.bright_cyan()); + } + } + } + println!(); + } + } + } + + Ok(()) +} + +/// Import from external sources +async fn cmd_import( + _project_dir: &PathBuf, + config: Option, + source: ImportSource, + path: PathBuf, + dry_run: bool, +) -> Result<()> { + info!("Importing from {:?}: {}", source, path.display()); + + if dry_run { + println!("{}", "Dry run mode - no changes will be made".yellow()); + } + + if !path.exists() { + return Err(anyhow::anyhow!("Path does not exist: {}", path.display())); + } + + match source { + ImportSource::Logseq => { + println!("{}", "Importing from Logseq...".bright_cyan()); + println!(" Source: {}", path.display()); + + // Use logseq importer from core + let _content = std::fs::read_to_string(&path).context("Failed to read Logseq file")?; + + // Parse the Logseq markdown and import nodes + // This would use kogral_core::import::logseq::* + println!(" Status: Parsing Logseq format..."); + println!("{}", "✓ Import prepared (dry run)".green()); + + if !dry_run { + println!(" Would import nodes to graph..."); + } + } + ImportSource::Markdown => { + println!("{}", "Importing from Markdown...".bright_cyan()); + println!(" Source: {}", path.display()); + + let content = std::fs::read_to_string(&path).context("Failed to read Markdown file")?; + + println!(" Status: Parsing Markdown format..."); + println!(" Metadata: {} bytes", content.len()); + println!("{}", "✓ Import prepared (dry run)".green()); + + if !dry_run { + println!(" Would import content as single node..."); + } + } + ImportSource::Json => { + println!("{}", "Importing from JSON...".bright_cyan()); + println!(" Source: {}", path.display()); + + let content = std::fs::read_to_string(&path).context("Failed to read JSON file")?; + + let json: serde_json::Value = + serde_json::from_str(&content).context("Invalid JSON format")?; + + println!(" Status: Validating JSON structure..."); + println!(" Objects: {}", count_json_objects(&json)); + println!("{}", "✓ Import prepared (dry run)".green()); + + if !dry_run { + println!(" Would import JSON as graph..."); + } + } + } + + if let Some(cfg) = config { + println!(" Target graph: {}", cfg.graph.name); + } + + Ok(()) +} + +/// Export to external formats +async fn cmd_export( + project_dir: &PathBuf, + config: Option, + format: ExportFormat, + output: PathBuf, + node_type: Option, +) -> Result<()> { + info!("Exporting to {:?}: {}", format, output.display()); + + // Create storage and load graph + let kogral_dir = project_dir.join(".kogral"); + let storage = FilesystemStorage::new(kogral_dir); + let graph_name = config + .as_ref() + .map(|c| c.graph.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let graph = storage.load_graph(&graph_name).await?; + + // Filter nodes by type if specified + let nodes: Vec<_> = if let Some(nt) = &node_type { + let node_type_filter: NodeType = nt.clone().into(); + graph + .nodes + .values() + .filter(|n| n.node_type == node_type_filter) + .collect() + } else { + graph.nodes.values().collect() + }; + + match format { + ExportFormat::Json => { + println!("{}", "Exporting to JSON...".bright_cyan()); + let json = serde_json::to_string_pretty(&graph)?; + std::fs::write(&output, json).context("Failed to write JSON file")?; + println!(" Format: JSON"); + println!(" Nodes: {}", nodes.len()); + println!(" Output: {}", output.display()); + println!("{}", "✓ Export completed".green()); + } + ExportFormat::Markdown => { + println!("{}", "Exporting to Markdown...".bright_cyan()); + let mut content = String::new(); + content.push_str(&format!("# {}\n\n", graph.name)); + for node in &nodes { + content.push_str(&format!("## {}\n\n", node.title)); + content.push_str(&format!("**Type:** {:?}\n\n", node.node_type)); + if !node.tags.is_empty() { + content.push_str(&format!("**Tags:** {}\n\n", node.tags.join(", "))); + } + content.push_str(&format!("{}\n\n", node.content)); + content.push_str("---\n\n"); + } + std::fs::write(&output, content).context("Failed to write Markdown file")?; + println!(" Format: Markdown"); + println!(" Nodes: {}", nodes.len()); + println!(" Output: {}", output.display()); + println!("{}", "✓ Export completed".green()); + } + ExportFormat::Logseq => { + println!("{}", "Exporting to Logseq...".bright_cyan()); + // Convert to Logseq format + let mut content = String::new(); + for node in &nodes { + content.push_str(&format!("- {}\n", node.title)); + if !node.tags.is_empty() { + content.push_str(&format!(" tags:: {}\n", node.tags.join(", "))); + } + for line in node.content.lines() { + content.push_str(&format!(" {}\n", line)); + } + content.push('\n'); + } + std::fs::write(&output, content).context("Failed to write Logseq file")?; + println!(" Format: Logseq"); + println!(" Nodes: {}", nodes.len()); + println!(" Output: {}", output.display()); + println!("{}", "✓ Export completed".green()); + } + ExportFormat::Summary => { + println!("{}", "Generating summary...".bright_cyan()); + let mut content = String::new(); + content.push_str("# Knowledge Base Summary\n\n"); + content.push_str(&format!("**Name:** {}\n", graph.name)); + content.push_str(&format!("**Total Nodes:** {}\n", graph.nodes.len())); + content.push_str(&format!("**Total Edges:** {}\n\n", graph.edges.len())); + + content.push_str("## Nodes by Type\n\n"); + for node_type in &[ + NodeType::Note, + NodeType::Decision, + NodeType::Guideline, + NodeType::Pattern, + NodeType::Journal, + NodeType::Execution, + ] { + let count = graph + .nodes + .values() + .filter(|n| n.node_type == *node_type) + .count(); + if count > 0 { + content.push_str(&format!("- {:?}: {}\n", node_type, count)); + } + } + + std::fs::write(&output, content).context("Failed to write summary file")?; + println!(" Format: Summary"); + println!(" Output: {}", output.display()); + println!("{}", "✓ Summary generated".green()); + } + } + + Ok(()) +} + +/// List all nodes +async fn cmd_list( + project_dir: &PathBuf, + config: Option, + node_type: Option, + status: Option, + tag: Option, +) -> Result<()> { + let kogral_dir = project_dir.join(".kogral"); + let storage = FilesystemStorage::new(kogral_dir); + let graph_name = config + .as_ref() + .map(|c| c.graph.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let graph = storage.load_graph(&graph_name).await?; + + let nodes: Vec<&Node> = graph + .nodes + .values() + .filter(|node| { + let type_match = node_type + .as_ref() + .map(|t| { + let nt: NodeType = t.clone().into(); + node.node_type == nt + }) + .unwrap_or(true); + + let status_match = status + .as_ref() + .map(|s| { + let ns: NodeStatus = s.clone().into(); + node.status == ns + }) + .unwrap_or(true); + + let tag_match = tag.as_ref().map(|t| node.tags.contains(t)).unwrap_or(true); + + type_match && status_match && tag_match + }) + .collect(); + + println!("\n{} {} node(s):", "Found".green(), nodes.len()); + for node in nodes { + println!( + " {} {} ({})", + match node.node_type { + NodeType::Note => "📝", + NodeType::Decision => "🔀", + NodeType::Guideline => "📋", + NodeType::Pattern => "🔧", + NodeType::Journal => "📅", + NodeType::Execution => "⚡", + }, + node.title.bright_blue(), + node.id.bright_black() + ); + println!( + " Type: {:?} | Status: {:?}", + node.node_type, node.status + ); + } + + Ok(()) +} + +/// Show node details +#[allow(clippy::excessive_nesting)] +async fn cmd_show( + project_dir: &PathBuf, + config: Option, + node: String, + relations: bool, +) -> Result<()> { + let kogral_dir = project_dir.join(".kogral"); + let storage = FilesystemStorage::new(kogral_dir); + let graph_name = config + .as_ref() + .map(|c| c.graph.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let graph = storage.load_graph(&graph_name).await?; + + let found_node = graph + .nodes + .values() + .find(|n| n.id == node || n.title == node) + .ok_or_else(|| anyhow::anyhow!("Node not found: {}", node))?; + + println!( + "\n{} {}", + "Node:".bright_green(), + found_node.title.bright_blue() + ); + println!("{}", "═".repeat(60)); + println!("ID: {}", found_node.id.bright_black()); + println!("Type: {:?}", found_node.node_type); + println!("Status: {:?}", found_node.status); + println!("Created: {}", found_node.created); + println!("Modified: {}", found_node.modified); + if !found_node.tags.is_empty() { + println!("Tags: {}", found_node.tags.join(", ").bright_yellow()); + } + println!("\n{}", "Content:".bright_cyan()); + println!("{}", found_node.content); + + if relations { + println!("\n{}", "Relationships:".bright_cyan()); + let edges: Vec<_> = graph + .edges + .iter() + .filter(|e| e.from == found_node.id || e.to == found_node.id) + .collect(); + + if edges.is_empty() { + println!(" No relationships"); + } else { + for edge in edges { + if edge.from == found_node.id { + if let Some(target) = graph.get_node(&edge.to) { + println!(" {:?} → {}", edge.edge_type, target.title.bright_blue()); + } + } else if let Some(source) = graph.get_node(&edge.from) { + println!(" {} ← {:?}", source.title.bright_blue(), edge.edge_type); + } + } + } + } + + Ok(()) +} + +/// Delete a node +async fn cmd_delete( + project_dir: &PathBuf, + config: Option, + node: String, + cascade: bool, + force: bool, +) -> Result<()> { + let kogral_dir = project_dir.join(".kogral"); + let mut storage = FilesystemStorage::new(kogral_dir); + let graph_name = config + .as_ref() + .map(|c| c.graph.name.clone()) + .unwrap_or_else(|| "default".to_string()); + + let mut graph = storage.load_graph(&graph_name).await?; + + let found_node = graph + .nodes + .values() + .find(|n| n.id == node || n.title == node) + .ok_or_else(|| anyhow::anyhow!("Node not found: {}", node))? + .clone(); + + if !force { + println!("{}", "Are you sure you want to delete this node?".yellow()); + println!(" Title: {}", found_node.title.bright_blue()); + println!(" ID: {}", found_node.id); + println!("\nThis action cannot be undone. Use --force to skip this confirmation."); + return Ok(()); + } + + graph + .remove_node(&found_node.id) + .ok_or_else(|| anyhow::anyhow!("Failed to remove node"))?; + + if cascade { + // Remove all edges connected to this node + graph + .edges + .retain(|e| e.from != found_node.id && e.to != found_node.id); + } + + storage.save_graph(&graph).await?; + + println!("{}", "✓ Node deleted successfully".green()); + + Ok(()) +} + +/// Manage configuration +async fn cmd_config( + _project_dir: &PathBuf, + config: Option, + show: bool, + get: Option, + set: Option, + value: Option, +) -> Result<()> { + if show { + // Show current configuration + if let Some(cfg) = config { + println!("\n{}", "Current Configuration:".bright_cyan()); + println!("{}", "═".repeat(50)); + println!("Graph:"); + println!(" name: {}", cfg.graph.name.bright_blue()); + println!(" version: {}", cfg.graph.version); + if !cfg.graph.description.is_empty() { + println!(" description: {}", cfg.graph.description); + } + println!("\nStorage:"); + println!(" primary: {:?}", cfg.storage.primary); + println!("\nEmbeddings:"); + println!(" provider: {:?}", cfg.embeddings.provider); + println!(" enabled: {}", cfg.embeddings.enabled); + println!("\nQuery:"); + println!(" similarity_threshold: {}", cfg.query.similarity_threshold); + println!(" max_results: {}", cfg.query.max_results); + println!("\nMCP Server:"); + println!(" name: {}", cfg.mcp.server.name); + println!(" transport: {:?}", cfg.mcp.server.transport); + println!("{}", "═".repeat(50)); + } else { + println!("{}", "⚠ No configuration found".yellow()); + } + } else if let Some(key) = get { + // Get specific config value + if let Some(cfg) = config { + match key.as_str() { + "graph.name" => println!("graph.name = {}", cfg.graph.name), + "graph.version" => println!("graph.version = {}", cfg.graph.version), + "storage.primary" => println!("storage.primary = {:?}", cfg.storage.primary), + "embeddings.provider" => { + println!("embeddings.provider = {:?}", cfg.embeddings.provider) + } + "query.similarity_threshold" => { + println!( + "query.similarity_threshold = {}", + cfg.query.similarity_threshold + ) + } + _ => println!("{}", format!("Unknown config key: {}", key).yellow()), + } + } else { + println!("{}", "⚠ No configuration found".yellow()); + } + } else if set.is_some() && value.is_some() { + println!( + "{}", + "⚠ Configuration modification not yet implemented".yellow() + ); + println!(" Edit configuration files directly:"); + println!(" - .kogral/config.toml (local)"); + println!(" - .kogral/config.ncl (Nickel source)"); + } else { + println!("Usage: kogral config [--show|--get KEY|--set KEY VALUE]"); + } + + Ok(()) +} + +/// Count JSON objects recursively +fn count_json_objects(value: &serde_json::Value) -> usize { + match value { + serde_json::Value::Object(obj) => 1 + obj.values().map(count_json_objects).sum::(), + serde_json::Value::Array(arr) => arr.iter().map(count_json_objects).sum::(), + _ => 0, + } +} diff --git a/crates/kogral-core/Cargo.toml b/crates/kogral-core/Cargo.toml new file mode 100644 index 0000000..0604be8 --- /dev/null +++ b/crates/kogral-core/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "kogral-core" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "KOGRAL core library - Config-driven knowledge graph engine with multi-backend storage" + +[dependencies] +# Workspace dependencies +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +toml = { workspace = true } +tokio = { workspace = true } +thiserror = { workspace = true } +anyhow = { workspace = true } +pulldown-cmark = { workspace = true } +tera = { workspace = true } +rig-core = { workspace = true } +surrealdb = { workspace = true, optional = true } +dashmap = { workspace = true } +notify = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +tracing = { workspace = true } + +# Additional dependencies +async-trait = { workspace = true } +fastembed = { workspace = true, optional = true } +regex = { workspace = true } +once_cell = { workspace = true } +walkdir = { workspace = true } +futures = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["test-util", "macros"] } +mockito = { workspace = true } +tempfile = { workspace = true } + +[features] +default = ["filesystem"] +filesystem = [] +surrealdb-backend = ["surrealdb"] +fastembed = ["dep:fastembed"] +full = ["surrealdb-backend", "fastembed"] diff --git a/crates/kogral-core/src/block_parser.rs b/crates/kogral-core/src/block_parser.rs new file mode 100644 index 0000000..c5b41e5 --- /dev/null +++ b/crates/kogral-core/src/block_parser.rs @@ -0,0 +1,475 @@ +//! Block parser for Logseq-compatible content blocks +//! +//! Parses markdown content in outliner format (bullet lists) into structured +//! Block objects. Supports: +//! - Task markers (TODO, DONE, DOING, etc.) +//! - Tags (#card, #important) +//! - Custom properties (`property::` value) +//! - Block references ((uuid)) +//! - Page references [[page]] +//! - Nested hierarchy (indentation) + +use std::collections::HashMap; +use std::fmt::Write; + +use crate::error::Result; +use crate::models::{Block, TaskStatus}; +use crate::regex_patterns::{ + PROPERTY_START_PATTERN, TAG_PATTERN, PROPERTY_INLINE_PATTERN, UUID_REF_PATTERN, + LOGSEQ_WIKILINK_PATTERN, +}; + +/// Parser for converting markdown outliner format to/from Block structures +pub struct BlockParser; + +impl BlockParser { + /// Parse markdown content into a list of blocks + /// + /// # Format + /// + /// ```markdown + /// - Block 1 #tag + /// - Nested block + /// - TODO Block 2 + /// priority:: high + /// - Block with [[page-ref]] and ((block-ref)) + /// ``` + /// + /// # Errors + /// + /// Returns an error if the markdown content contains invalid block syntax + /// or if parsing of individual block lines fails. + /// + /// # Panics + /// + /// May panic if internal regex compilation fails (should never happen with + /// hardcoded patterns) or if stack operations fail unexpectedly. + pub fn parse(content: &str) -> Result> { + let lines: Vec<&str> = content.lines().collect(); + if lines.is_empty() { + return Ok(Vec::new()); + } + + let mut blocks = Vec::new(); + let mut stack: Vec<(usize, Block)> = Vec::new(); // (indent_level, block) + + for line in lines { + if line.trim().is_empty() { + continue; + } + + let indent = count_indent(line); + let trimmed = line.trim(); + + // Check if this is a property line (key:: value without bullet) + if !trimmed.starts_with('-') && !trimmed.starts_with('*') { + // Try to parse as a property and add to the last block + Self::try_add_property_to_last_block(&mut stack, trimmed); + continue; + } + + // Remove bullet marker + let content_after_bullet = trimmed[1..].trim(); + + // Parse the block + let block = Self::parse_block_line(content_after_bullet); + + // Handle hierarchy based on indentation + if stack.is_empty() { + // First block or root level + stack.push((indent, block)); + } else { + // Pop blocks with greater or equal indentation and add to their parents + Self::flush_stack_to_indent(&mut stack, &mut blocks, indent); + stack.push((indent, block)); + } + } + + // Flush remaining blocks in stack + while let Some((_, block)) = stack.pop() { + if let Some((_, parent)) = stack.last_mut() { + parent.add_child(block); + } else { + blocks.push(block); + } + } + + Ok(blocks) + } + + /// Flush stack entries with indentation >= target indent, adding them to + /// blocks or parents + fn flush_stack_to_indent( + stack: &mut Vec<(usize, Block)>, + blocks: &mut Vec, + target_indent: usize, + ) { + while let Some((parent_indent, _)) = stack.last() { + if target_indent > *parent_indent { + break; + } + + let (_, completed_block) = stack.pop().unwrap(); + + // Add to parent or root + if let Some((_, parent)) = stack.last_mut() { + parent.add_child(completed_block); + } else { + blocks.push(completed_block); + } + } + } + + /// Try to parse a property line and add it to the last block in the stack + fn try_add_property_to_last_block(stack: &mut [(usize, Block)], line: &str) { + if let Some((_, last_block)) = stack.last_mut() { + if let Some(cap) = PROPERTY_START_PATTERN.captures(line) { + let key = cap[1].to_string(); + let value = cap[2].to_string(); + last_block.properties.set_property(key, value); + } + } + } + + /// Parse a single block line (without bullet marker) + fn parse_block_line(line: &str) -> Block { + let (task_status, after_status) = Self::extract_task_status(line); + let (tags, after_tags) = Self::extract_tags(&after_status); + let (properties, content) = Self::extract_properties(&after_tags); + let block_refs = Self::extract_block_refs(&content); + let page_refs = Self::extract_page_refs(&content); + + let mut block = Block::new(content.trim().to_string()); + block.properties.status = task_status; + block.properties.tags = tags; + block.properties.custom = properties; + block.properties.block_refs = block_refs; + block.properties.page_refs = page_refs; + + block + } + + /// Extract task status from line start + fn extract_task_status(line: &str) -> (Option, String) { + let trimmed = line.trim(); + + let status = if trimmed.starts_with("TODO ") { + Some(TaskStatus::Todo) + } else if trimmed.starts_with("DOING ") { + Some(TaskStatus::Doing) + } else if trimmed.starts_with("DONE ") { + Some(TaskStatus::Done) + } else if trimmed.starts_with("LATER ") { + Some(TaskStatus::Later) + } else if trimmed.starts_with("NOW ") { + Some(TaskStatus::Now) + } else if trimmed.starts_with("WAITING ") { + Some(TaskStatus::Waiting) + } else if trimmed.starts_with("CANCELLED ") { + Some(TaskStatus::Cancelled) + } else { + None + }; + + match status { + Some(s) => { + let status_str = s.to_string(); + let after = trimmed.trim_start_matches(&status_str).trim(); + (Some(s), after.to_string()) + } + None => (None, trimmed.to_string()), + } + } + + /// Extract hashtags from content + fn extract_tags(content: &str) -> (Vec, String) { + let mut tags = Vec::new(); + let mut clean_content = content.to_string(); + + for cap in TAG_PATTERN.captures_iter(content) { + tags.push(cap[1].to_string()); + } + + // Remove tags from content + clean_content = TAG_PATTERN.replace_all(&clean_content, "").to_string(); + + (tags, clean_content) + } + + /// Extract custom properties (`key::` value format) + fn extract_properties(content: &str) -> (HashMap, String) { + let mut properties = HashMap::new(); + let mut clean_content = content.to_string(); + + for cap in PROPERTY_INLINE_PATTERN.captures_iter(content) { + let key = cap[1].to_string(); + let value = cap[2].to_string(); + properties.insert(key, value); + } + + // Remove properties from content + clean_content = PROPERTY_INLINE_PATTERN.replace_all(&clean_content, "").to_string(); + + (properties, clean_content) + } + + /// Extract block references ((uuid)) + fn extract_block_refs(content: &str) -> Vec { + UUID_REF_PATTERN + .captures_iter(content) + .map(|cap| cap[1].to_string()) + .collect() + } + + /// Extract page references [[page]] + fn extract_page_refs(content: &str) -> Vec { + LOGSEQ_WIKILINK_PATTERN + .captures_iter(content) + .map(|cap| cap[1].to_string()) + .collect() + } + + /// Serialize blocks back to markdown outliner format + #[must_use] + pub fn serialize(blocks: &[Block]) -> String { + let mut output = String::new(); + for block in blocks { + Self::serialize_block(block, 0, &mut output); + } + output + } + + /// Serialize a single block and its children recursively + fn serialize_block(block: &Block, indent_level: usize, output: &mut String) { + let indent = " ".repeat(indent_level); + + // Start with bullet + output.push_str(&indent); + output.push_str("- "); + + // Add task status if present + if let Some(status) = block.properties.status { + let _ = write!(output, "{status} "); + } + + // Add content + output.push_str(&block.content); + + // Add tags + for tag in &block.properties.tags { + let _ = write!(output, " #{tag}"); + } + + // Add page references (if not already in content) + for page_ref in &block.properties.page_refs { + if !block.content.contains(&format!("[[{page_ref}]]")) { + let _ = write!(output, " [[{page_ref}]]"); + } + } + + // Add block references (if not already in content) + for block_ref in &block.properties.block_refs { + if !block.content.contains(&format!("(({block_ref}))")) { + let _ = write!(output, " (({block_ref}))"); + } + } + + output.push('\n'); + + // Add custom properties on next line(s) if present + for (key, value) in &block.properties.custom { + output.push_str(&indent); + output.push_str(" "); + let _ = writeln!(output, "{key}:: {value}"); + } + + // Recursively serialize children + for child in &block.children { + Self::serialize_block(child, indent_level + 1, output); + } + } +} + +/// Count leading whitespace (indentation level) +fn count_indent(line: &str) -> usize { + line.chars().take_while(|c| c.is_whitespace()).count() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_simple_block() { + let content = "- Simple block"; + let blocks = BlockParser::parse(content).unwrap(); + + assert_eq!(blocks.len(), 1); + assert_eq!(blocks[0].content, "Simple block"); + assert!(blocks[0].properties.status.is_none()); + assert!(blocks[0].properties.tags.is_empty()); + } + + #[test] + fn test_parse_task_block() { + let content = "- TODO Complete this task"; + let blocks = BlockParser::parse(content).unwrap(); + + assert_eq!(blocks.len(), 1); + assert_eq!(blocks[0].properties.status, Some(TaskStatus::Todo)); + assert_eq!(blocks[0].content, "Complete this task"); + } + + #[test] + fn test_parse_block_with_tags() { + let content = "- This is a #card with #tags"; + let blocks = BlockParser::parse(content).unwrap(); + + assert_eq!(blocks.len(), 1); + assert_eq!(blocks[0].properties.tags.len(), 2); + assert!(blocks[0].properties.tags.contains(&"card".to_string())); + assert!(blocks[0].properties.tags.contains(&"tags".to_string())); + } + + #[test] + fn test_parse_block_with_properties() { + let content = "- Block with property\n priority:: high"; + let blocks = BlockParser::parse(content).unwrap(); + + assert_eq!(blocks.len(), 1); + assert_eq!( + blocks[0].properties.get_property("priority"), + Some(&"high".to_string()) + ); + } + + #[test] + fn test_parse_nested_blocks() { + let content = r#"- Parent block + - Child block 1 + - Child block 2 + - Grandchild block"#; + + let blocks = BlockParser::parse(content).unwrap(); + + assert_eq!(blocks.len(), 1); + assert_eq!(blocks[0].content, "Parent block"); + assert_eq!(blocks[0].children.len(), 2); + assert_eq!(blocks[0].children[0].content, "Child block 1"); + assert_eq!(blocks[0].children[1].content, "Child block 2"); + assert_eq!(blocks[0].children[1].children.len(), 1); + assert_eq!( + blocks[0].children[1].children[0].content, + "Grandchild block" + ); + } + + #[test] + fn test_parse_page_references() { + let content = "- Block with [[Page Reference]]"; + let blocks = BlockParser::parse(content).unwrap(); + + assert_eq!(blocks.len(), 1); + assert_eq!(blocks[0].properties.page_refs.len(), 1); + assert_eq!(blocks[0].properties.page_refs[0], "Page Reference"); + } + + #[test] + fn test_serialize_simple_block() { + let block = Block::new("Simple block".to_string()); + let markdown = BlockParser::serialize(&[block]); + + assert_eq!(markdown.trim(), "- Simple block"); + } + + #[test] + fn test_serialize_task_block() { + let mut block = Block::new("Complete this task".to_string()); + block.properties.status = Some(TaskStatus::Todo); + let markdown = BlockParser::serialize(&[block]); + + assert_eq!(markdown.trim(), "- TODO Complete this task"); + } + + #[test] + fn test_serialize_block_with_tags() { + let mut block = Block::new("Card content".to_string()); + block.properties.add_tag("card".to_string()); + block.properties.add_tag("important".to_string()); + let markdown = BlockParser::serialize(&[block]); + + assert!(markdown.contains("#card")); + assert!(markdown.contains("#important")); + } + + #[test] + fn test_serialize_nested_blocks() { + let mut parent = Block::new("Parent".to_string()); + let child1 = Block::new("Child 1".to_string()); + let child2 = Block::new("Child 2".to_string()); + + parent.add_child(child1); + parent.add_child(child2); + + let markdown = BlockParser::serialize(&[parent]); + + assert!(markdown.contains("- Parent")); + assert!(markdown.contains(" - Child 1")); + assert!(markdown.contains(" - Child 2")); + } + + #[test] + fn test_round_trip() { + let original = r#"- TODO Parent task #important + - Child note + - DONE Completed subtask"#; + + let blocks = BlockParser::parse(original).unwrap(); + let serialized = BlockParser::serialize(&blocks); + let reparsed = BlockParser::parse(&serialized).unwrap(); + + // Verify structure is preserved + assert_eq!(blocks.len(), reparsed.len()); + assert_eq!(blocks[0].properties.status, reparsed[0].properties.status); + assert_eq!(blocks[0].children.len(), reparsed[0].children.len()); + } + + #[test] + fn test_extract_task_status() { + let (status, content) = BlockParser::extract_task_status("TODO Task content"); + assert_eq!(status, Some(TaskStatus::Todo)); + assert_eq!(content, "Task content"); + + let (status, content) = BlockParser::extract_task_status("DONE Completed task"); + assert_eq!(status, Some(TaskStatus::Done)); + assert_eq!(content, "Completed task"); + + let (status, content) = BlockParser::extract_task_status("No task here"); + assert_eq!(status, None); + assert_eq!(content, "No task here"); + } + + #[test] + fn test_extract_tags() { + let (tags, content) = BlockParser::extract_tags("Text with #tag1 and #tag2"); + assert_eq!(tags.len(), 2); + assert!(tags.contains(&"tag1".to_string())); + assert!(tags.contains(&"tag2".to_string())); + assert!(content.contains("Text with")); + assert!(content.contains("and")); + } + + #[test] + fn test_extract_properties() { + let (props, _content) = BlockParser::extract_properties("priority:: high status:: active"); + assert_eq!(props.get("priority"), Some(&"high".to_string())); + assert_eq!(props.get("status"), Some(&"active".to_string())); + } + + #[test] + fn test_count_indent() { + assert_eq!(count_indent("- No indent"), 0); + assert_eq!(count_indent(" - Two spaces"), 2); + assert_eq!(count_indent(" - Four spaces"), 4); + } +} diff --git a/crates/kogral-core/src/config/loader.rs b/crates/kogral-core/src/config/loader.rs new file mode 100644 index 0000000..af8c388 --- /dev/null +++ b/crates/kogral-core/src/config/loader.rs @@ -0,0 +1,472 @@ +//! Configuration loader with multi-format support +//! +//! Loads configuration from: +//! 1. Nickel (.ncl) - preferred, type-safe +//! 2. TOML (.toml) - fallback +//! 3. JSON (.json) - fallback +//! 4. Defaults - if no config file found + +use std::fs; +use std::path::{Path, PathBuf}; + +use tracing::{debug, info, warn}; + +use crate::config::schema::KbConfig; +use crate::error::{KbError, Result}; + +/// Configuration file search paths (in order of preference) +const CONFIG_FILE_NAMES: &[&str] = &[ + ".kogral/config.ncl", + ".kogral/config.toml", + ".kogral/config.json", + "config/kb.ncl", + "config/kb.toml", + "config/kb.json", +]; + +/// Load configuration from the current directory or specified path +/// +/// Search order: +/// 1. Specified path (if provided) +/// 2. `.kogral/config.ncl` (Nickel, preferred) +/// 3. `.kogral/config.toml` (TOML fallback) +/// 4. `.kogral/config.json` (JSON fallback) +/// 5. `config/kb.{ncl,toml,json}` (alternate location) +/// 6. Default configuration (if no file found) +/// +/// # Arguments +/// +/// * `base_dir` - Base directory to search from (defaults to current directory) +/// * `config_path` - Optional explicit config file path +/// +/// # Returns +/// +/// Loaded `KbConfig` or default if no config found +/// +/// # Errors +/// +/// Returns an error if config file exists but cannot be read or parsed +pub fn load_config(base_dir: Option<&Path>, config_path: Option<&Path>) -> Result { + // If explicit path provided, load directly + if let Some(path) = config_path { + info!("Loading config from explicit path: {}", path.display()); + return load_config_from_file(path); + } + + let search_base = base_dir.unwrap_or_else(|| Path::new(".")); + + // Search for config files in order of preference + for file_name in CONFIG_FILE_NAMES { + let path = search_base.join(file_name); + if path.exists() { + info!("Found config file: {}", path.display()); + return load_config_from_file(&path); + } + } + + // No config file found, use defaults + warn!( + "No config file found in {}. Using defaults.", + search_base.display() + ); + warn!("Searched paths: {:?}", CONFIG_FILE_NAMES); + + // Infer graph name from directory + let graph_name = search_base + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("default") + .to_string(); + + Ok(KbConfig::default_for_graph(graph_name)) +} + +/// Load configuration from a specific file +/// +/// Detects format from extension: .ncl, .toml, .json +/// +/// # Errors +/// +/// Returns an error if file cannot be read, parsed, or has unsupported +/// extension +pub fn load_config_from_file>(path: P) -> Result { + let path = path.as_ref(); + + if !path.exists() { + return Err(KbError::Config(format!( + "Config file not found: {}", + path.display() + ))); + } + + let extension = path.extension().and_then(|e| e.to_str()).ok_or_else(|| { + KbError::Config(format!( + "Cannot determine config format: {}", + path.display() + )) + })?; + + match extension { + "ncl" => load_from_nickel(path), + "toml" => load_from_toml(path), + "json" => load_from_json(path), + other => Err(KbError::Config(format!( + "Unsupported config format: .{other}" + ))), + } +} + +/// Load config from Nickel file +#[cfg(not(test))] +fn load_from_nickel>(path: P) -> Result { + use super::nickel::load_nickel_config; + info!("Loading config from Nickel: {}", path.as_ref().display()); + load_nickel_config(path) +} + +/// Test stub for Nickel loading +#[cfg(test)] +fn load_from_nickel>(path: P) -> Result { + // In tests, fall back to TOML/JSON or return error + Err(KbError::Config(format!( + "Nickel loading not available in tests: {}", + path.as_ref().display() + ))) +} + +/// Load config from TOML file +fn load_from_toml>(path: P) -> Result { + let path = path.as_ref(); + info!("Loading config from TOML: {}", path.display()); + + let content = fs::read_to_string(path)?; + let config: KbConfig = toml::from_str(&content)?; + + debug!("Config loaded from TOML: {}", path.display()); + Ok(config) +} + +/// Load config from JSON file +fn load_from_json>(path: P) -> Result { + let path = path.as_ref(); + info!("Loading config from JSON: {}", path.display()); + + let content = fs::read_to_string(path)?; + let config: KbConfig = serde_json::from_str(&content)?; + + // Resolve environment variables in paths + let config = config.resolve_paths(); + + debug!("Config loaded from JSON: {}", path.display()); + Ok(config) +} + +/// Find .kb directory by walking up from current directory +/// +/// # Returns +/// +/// Path to .kb directory if found, None otherwise +#[must_use] +pub fn find_kogral_directory() -> Option { + let mut current = std::env::current_dir().ok()?; + + loop { + let kogral_dir = current.join(".kogral"); + if kogral_dir.is_dir() { + return Some(kogral_dir); + } + + // Try parent directory + if !current.pop() { + break; + } + } + + None +} + +/// Initialize a new .kb directory with default config +/// +/// # Arguments +/// +/// * `path` - Base directory to create .kb in +/// * `format` - Config file format ("ncl", "toml", or "json") +/// +/// # Returns +/// +/// Path to created config file +/// +/// # Errors +/// +/// Returns an error if directories cannot be created or config file cannot be +/// written +pub fn init_kogral_directory>(path: P, format: &str) -> Result { + let base = path.as_ref(); + let kogral_dir = base.join(".kogral"); + + // Create directory structure + fs::create_dir_all(&kogral_dir)?; + fs::create_dir_all(kogral_dir.join("notes"))?; + fs::create_dir_all(kogral_dir.join("decisions"))?; + fs::create_dir_all(kogral_dir.join("guidelines"))?; + fs::create_dir_all(kogral_dir.join("patterns"))?; + fs::create_dir_all(kogral_dir.join("journal"))?; + + // Infer graph name from directory + let graph_name = base + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("default") + .to_string(); + + let config = KbConfig::default_for_graph(graph_name); + + // Write config file in requested format + let config_file = match format { + "toml" => { + let path = kogral_dir.join("config.toml"); + let content = toml::to_string_pretty(&config)?; + fs::write(&path, content)?; + path + } + "json" => { + let path = kogral_dir.join("config.json"); + let content = serde_json::to_string_pretty(&config)?; + fs::write(&path, content)?; + path + } + "ncl" => { + // For Nickel, we'd generate a .ncl file, but for now just write TOML + // and instruct user to convert + let path = kogral_dir.join("config.toml"); + let content = toml::to_string_pretty(&config)?; + fs::write(&path, content)?; + warn!("Generated TOML config. Convert to Nickel manually for type safety."); + path + } + other => return Err(KbError::Config(format!("Unsupported init format: {other}"))), + }; + + info!("Initialized .kb directory at: {}", kogral_dir.display()); + info!("Config file: {}", config_file.display()); + + Ok(config_file) +} + +#[cfg(test)] +mod tests { + use tempfile::tempdir; + + use super::*; + + #[test] + fn test_load_config_no_file_uses_defaults() { + let dir = tempdir().unwrap(); + let config = load_config(Some(dir.path()), None).unwrap(); + assert_eq!(config.graph.version, "1.0.0"); + } + + #[test] + fn test_load_config_from_toml() { + let dir = tempdir().unwrap(); + let kogral_dir = dir.path().join(".kogral"); + fs::create_dir(&kogral_dir).unwrap(); + let config_path = kogral_dir.join("config.toml"); + + let toml_content = r#" +[graph] +name = "test-graph" +version = "2.0.0" +description = "Test description" + +[inheritance] +base = "/tmp/shared" +priority = 200 + +[storage] +primary = "filesystem" + +[embeddings] +enabled = true +provider = "fastembed" +model = "test-model" +dimensions = 512 + +[query] +similarity_threshold = 0.5 +max_results = 20 +recency_weight = 3.0 +cross_graph = true + +[templates] +templates_dir = "templates" + +[mcp.server] +name = "kogral-mcp" +version = "1.0.0" +transport = "stdio" + +[sync] +auto_index = true +watch_paths = ["notes", "decisions"] +debounce_ms = 500 +"#; + + fs::write(&config_path, toml_content).unwrap(); + + let config = load_config_from_file(&config_path).unwrap(); + assert_eq!(config.graph.name, "test-graph"); + assert_eq!(config.graph.version, "2.0.0"); + assert_eq!(config.inheritance.priority, 200); + assert_eq!(config.embeddings.dimensions, 512); + assert_eq!(config.query.max_results, 20); + } + + #[test] + fn test_load_config_from_json() { + let dir = tempdir().unwrap(); + let config_path = dir.path().join("config.json"); + + let json_content = r#"{ + "graph": { + "name": "json-graph", + "version": "1.5.0", + "description": "" + }, + "inheritance": { + "base": "/tmp/tools/.kogral-shared", + "guidelines": [], + "priority": 100 + }, + "storage": { + "primary": "filesystem", + "secondary": { + "enabled": false, + "type": "surrealdb", + "url": "ws://localhost:8000", + "namespace": "kogral", + "database": "default" + } + }, + "embeddings": { + "enabled": true, + "provider": "fastembed", + "model": "BAAI/bge-small-en-v1.5", + "dimensions": 384, + "api_key_env": "OPENAI_API_KEY" + }, + "templates": { + "templates_dir": "templates", + "templates": { + "note": "note.md.tera", + "decision": "decision.md.tera", + "guideline": "guideline.md.tera", + "pattern": "pattern.md.tera", + "journal": "journal.md.tera", + "execution": "execution.md.tera" + }, + "export": { + "logseq_page": "export/logseq-page.md.tera", + "logseq_journal": "export/logseq-journal.md.tera", + "summary": "export/summary.md.tera", + "json": "export/graph.json.tera" + }, + "custom": {} + }, + "query": { + "similarity_threshold": 0.4, + "max_results": 10, + "recency_weight": 3.0, + "cross_graph": true + }, + "mcp": { + "server": { + "name": "kogral-mcp", + "version": "1.0.0", + "transport": "stdio" + }, + "tools": { + "search": true, + "add_note": true, + "add_decision": true, + "link": true, + "get_guidelines": true, + "export": true + }, + "resources": { + "expose_project": true, + "expose_shared": true + } + }, + "sync": { + "auto_index": true, + "watch_paths": ["notes", "decisions", "guidelines", "patterns", "journal"], + "debounce_ms": 500 + } +}"#; + + fs::write(&config_path, json_content).unwrap(); + + let config = load_config_from_file(&config_path).unwrap(); + assert_eq!(config.graph.name, "json-graph"); + assert_eq!(config.graph.version, "1.5.0"); + } + + #[test] + fn test_load_config_nonexistent_file() { + let result = load_config_from_file("/nonexistent/config.toml"); + assert!(result.is_err()); + } + + #[test] + fn test_load_config_unsupported_format() { + let dir = tempdir().unwrap(); + let config_path = dir.path().join("config.yaml"); + fs::write(&config_path, "test: value").unwrap(); + + let result = load_config_from_file(&config_path); + assert!(result.is_err()); + } + + #[test] + fn test_init_kogral_directory_toml() { + let dir = tempdir().unwrap(); + let config_file = init_kogral_directory(dir.path(), "toml").unwrap(); + + assert!(config_file.exists()); + assert!(dir.path().join(".kogral").is_dir()); + assert!(dir.path().join(".kogral/notes").is_dir()); + assert!(dir.path().join(".kogral/decisions").is_dir()); + assert!(dir.path().join(".kogral/guidelines").is_dir()); + assert!(dir.path().join(".kogral/patterns").is_dir()); + assert!(dir.path().join(".kogral/journal").is_dir()); + + // Verify config can be loaded + let config = load_config_from_file(&config_file).unwrap(); + assert_eq!(config.graph.version, "1.0.0"); + } + + #[test] + fn test_init_kogral_directory_json() { + let dir = tempdir().unwrap(); + let config_file = init_kogral_directory(dir.path(), "json").unwrap(); + + assert!(config_file.exists()); + assert!(config_file.extension().unwrap() == "json"); + } + + #[test] + fn test_init_kogral_directory_unsupported_format() { + let dir = tempdir().unwrap(); + let result = init_kogral_directory(dir.path(), "yaml"); + assert!(result.is_err()); + } + + #[test] + fn test_find_kogral_directory_not_found() { + // This will likely return None unless running in a KB-enabled directory + let result = find_kogral_directory(); + // Can't assert much here without knowing test environment + println!("KOGRAL directory search result: {:?}", result); + } +} diff --git a/crates/kogral-core/src/config/mod.rs b/crates/kogral-core/src/config/mod.rs new file mode 100644 index 0000000..50d388f --- /dev/null +++ b/crates/kogral-core/src/config/mod.rs @@ -0,0 +1,53 @@ +//! Configuration management module +//! +//! This module handles loading and managing knowledge base configuration. +//! +//! ## Configuration Sources +//! +//! Configuration can be loaded from (in order of preference): +//! 1. **Nickel (.ncl)** - Type-safe, composable configuration (recommended) +//! 2. **TOML (.toml)** - Human-friendly fallback +//! 3. **JSON (.json)** - Machine-readable fallback +//! 4. **Defaults** - Built-in defaults if no config file found +//! +//! ## Nickel Pattern +//! +//! The Nickel configuration pattern works as follows: +//! ```text +//! .ncl file → nickel export --format json → JSON → serde → Rust struct +//! ``` +//! +//! This gives us: +//! - Type safety in config files (Nickel schemas) +//! - Composition and reuse (Nickel imports) +//! - Runtime type-safe Rust structs (serde) +//! +//! ## Example +//! +//! ```no_run +//! use kogral_core::config::loader; +//! use std::path::Path; +//! +//! // Load from current directory (searches for .kogral/config.{ncl,toml,json}) +//! let config = loader::load_config(None, None)?; +//! +//! // Load from specific file +//! let config = loader::load_config_from_file(Path::new(".kogral/config.toml"))?; +//! +//! // Initialize new .kb directory +//! let config_path = loader::init_kogral_directory(Path::new("."), "toml")?; +//! # Ok::<(), kogral_core::error::KbError>(()) +//! ``` + +pub mod loader; +pub mod nickel; +pub mod schema; + +// Re-exports for convenience +pub use loader::{ + find_kogral_directory, init_kogral_directory, load_config, load_config_from_file, +}; +pub use schema::{ + EmbeddingConfig, EmbeddingProvider, GraphConfig, InheritanceConfig, KbConfig, McpConfig, + QueryConfig, StorageConfig, SyncConfig, TemplateConfig, +}; diff --git a/crates/kogral-core/src/config/nickel.rs b/crates/kogral-core/src/config/nickel.rs new file mode 100644 index 0000000..9c8c2e7 --- /dev/null +++ b/crates/kogral-core/src/config/nickel.rs @@ -0,0 +1,281 @@ +//! Nickel CLI bridge for loading .ncl configuration files +//! +//! This module provides utilities to load Nickel configuration files by: +//! 1. Invoking `nickel export --format json .ncl` via CLI +//! 2. Parsing the JSON output +//! 3. Deserializing into Rust config structs +//! +//! Pattern adapted from `platform-config` in provisioning project. + +use std::path::Path; +use std::process::Command; + +use serde::de::DeserializeOwned; +use tracing::{debug, info, warn}; + +use crate::error::{KbError, Result}; + +/// Export a Nickel file to JSON using the Nickel CLI +/// +/// # Arguments +/// +/// * `nickel_file` - Path to the .ncl file +/// +/// # Returns +/// +/// JSON string output from `nickel export --format json` +/// +/// # Errors +/// +/// Returns `KbError::NickelExport` if: +/// - Nickel CLI is not installed +/// - The file doesn't exist +/// - Nickel export command fails +/// - Output is not valid UTF-8 +pub fn export_nickel_to_json>(nickel_file: P) -> Result { + let path = nickel_file.as_ref(); + + if !path.exists() { + return Err(KbError::NickelExport(format!( + "Nickel file not found: {}", + path.display() + ))); + } + + info!("Exporting Nickel file to JSON: {}", path.display()); + + let output = Command::new("nickel") + .arg("export") + .arg("--format") + .arg("json") + .arg(path) + .output() + .map_err(|e| { + KbError::NickelExport(format!( + "Failed to execute nickel command (is it installed?): {e}" + )) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(KbError::NickelExport(format!( + "Nickel export failed for {}: {}", + path.display(), + stderr + ))); + } + + let json = String::from_utf8(output.stdout) + .map_err(|e| KbError::NickelExport(format!("Nickel output is not valid UTF-8: {e}")))?; + + debug!("Nickel export successful: {} bytes", json.len()); + Ok(json) +} + +/// Load and deserialize a Nickel configuration file +/// +/// # Arguments +/// +/// * `nickel_file` - Path to the .ncl file +/// +/// # Returns +/// +/// Deserialized configuration struct +/// +/// # Errors +/// +/// Returns error if: +/// - Nickel export fails +/// - JSON deserialization fails +/// +/// # Example +/// +/// ```no_run +/// use kogral_core::config::{nickel, schema::KbConfig}; +/// use std::path::Path; +/// +/// let config: KbConfig = nickel::load_nickel_config(Path::new("kb.ncl"))?; +/// # Ok::<(), kogral_core::error::KbError>(()) +/// ``` +pub fn load_nickel_config(nickel_file: P) -> Result +where + T: DeserializeOwned, + P: AsRef, +{ + let path = nickel_file.as_ref(); + info!("Loading Nickel config from: {}", path.display()); + + let json = export_nickel_to_json(path)?; + + let config: T = serde_json::from_str(&json).map_err(|e| { + KbError::Serialization(format!( + "Failed to deserialize config from {}: {}", + path.display(), + e + )) + })?; + + debug!("Config loaded successfully from Nickel"); + Ok(config) +} + +/// Check if Nickel CLI is available +/// +/// # Returns +/// +/// `true` if `nickel` command is in PATH, `false` otherwise +#[must_use] +pub fn is_nickel_available() -> bool { + Command::new("nickel") + .arg("--version") + .output() + .map(|output| output.status.success()) + .unwrap_or(false) +} + +/// Get Nickel CLI version +/// +/// # Returns +/// +/// Version string if available, or an error +/// +/// # Errors +/// +/// Returns an error if nickel binary cannot be executed or version cannot be +/// parsed +pub fn nickel_version() -> Result { + let output = Command::new("nickel") + .arg("--version") + .output() + .map_err(|e| KbError::NickelExport(format!("Failed to check nickel version: {e}")))?; + + if !output.status.success() { + return Err(KbError::NickelExport( + "Nickel CLI not available".to_string(), + )); + } + + let version = String::from_utf8(output.stdout) + .map_err(|e| KbError::NickelExport(format!("Invalid version output: {e}")))?; + + Ok(version.trim().to_string()) +} + +/// Warn if Nickel is not available and return fallback behavior +/// +/// This is useful for optional Nickel support where we can fall back to +/// TOML/JSON +pub fn warn_if_nickel_unavailable() { + if !is_nickel_available() { + warn!("Nickel CLI not found in PATH. Install from: https://nickel-lang.org/"); + warn!("Falling back to TOML/JSON configuration support only"); + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::*; + + #[test] + fn test_is_nickel_available() { + // This test will pass/fail depending on local environment + let available = is_nickel_available(); + println!("Nickel available: {}", available); + } + + #[test] + fn test_nickel_version() { + if is_nickel_available() { + let version = nickel_version(); + assert!(version.is_ok()); + println!("Nickel version: {}", version.unwrap()); + } + } + + #[test] + fn test_export_nonexistent_file() { + let result = export_nickel_to_json("/nonexistent/file.ncl"); + assert!(result.is_err()); + assert!(matches!(result, Err(KbError::NickelExport(_)))); + } + + #[test] + #[ignore] // Requires Nickel CLI to be installed + fn test_export_valid_nickel() { + if !is_nickel_available() { + eprintln!("Skipping test: Nickel CLI not available"); + return; + } + + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test.ncl"); + + // Write a simple Nickel file + fs::write( + &file_path, + r#" + { + name = "test", + value = 42, + } + "#, + ) + .unwrap(); + + let result = export_nickel_to_json(&file_path); + assert!(result.is_ok()); + + let json = result.unwrap(); + assert!(json.contains("name")); + assert!(json.contains("test")); + assert!(json.contains("42")); + } + + #[test] + #[ignore] // Requires Nickel CLI to be installed + fn test_load_nickel_config() { + use serde::{Deserialize, Serialize}; + + if !is_nickel_available() { + eprintln!("Skipping test: Nickel CLI not available"); + return; + } + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct TestConfig { + name: String, + value: i32, + } + + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test.ncl"); + + fs::write( + &file_path, + r#" + { + name = "test-config", + value = 123, + } + "#, + ) + .unwrap(); + + let config: Result = load_nickel_config(&file_path); + assert!(config.is_ok()); + + let config = config.unwrap(); + assert_eq!(config.name, "test-config"); + assert_eq!(config.value, 123); + } + + #[test] + fn test_warn_if_nickel_unavailable() { + // Just ensure it doesn't panic + warn_if_nickel_unavailable(); + } +} diff --git a/crates/kogral-core/src/config/schema.rs b/crates/kogral-core/src/config/schema.rs new file mode 100644 index 0000000..695879b --- /dev/null +++ b/crates/kogral-core/src/config/schema.rs @@ -0,0 +1,674 @@ +//! Configuration schema types +//! +//! These structs correspond to the Nickel schemas defined in `schemas/*.ncl`. +//! They are loaded via: Nickel CLI → JSON → serde → Rust structs. + +use std::collections::HashMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +/// Main knowledge base configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KbConfig { + /// Graph metadata + pub graph: GraphConfig, + /// Inheritance settings + pub inheritance: InheritanceConfig, + /// Storage backend configuration + pub storage: StorageConfig, + /// Embedding provider configuration + pub embeddings: EmbeddingConfig, + /// Template engine configuration + pub templates: TemplateConfig, + /// Query behavior configuration + pub query: QueryConfig, + /// MCP server configuration + #[serde(default)] + pub mcp: McpConfig, + /// Sync settings + #[serde(default)] + pub sync: SyncConfig, +} + +/// Resolve environment variables in paths +fn resolve_env_vars(path: &str) -> PathBuf { + let resolved = path.replace("$TOOLS_PATH", &resolve_tools_path()); + let resolved = resolved.replace( + "$HOME", + &std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()), + ); + PathBuf::from(resolved) +} + +/// Get `TOOLS_PATH` with fallback to `$HOME/Tools` +fn resolve_tools_path() -> String { + std::env::var("TOOLS_PATH").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + format!("{home}/Tools") + }) +} + +impl KbConfig { + /// Resolve all environment variables in paths + #[must_use] + pub fn resolve_paths(mut self) -> Self { + self.inheritance.base = resolve_env_vars(&self.inheritance.base.to_string_lossy()); + self.inheritance.guidelines = self + .inheritance + .guidelines + .iter() + .map(|p| resolve_env_vars(&p.to_string_lossy())) + .collect(); + self + } + + /// Create default configuration for a given graph name + #[must_use] + pub fn default_for_graph(name: String) -> Self { + Self { + graph: GraphConfig { + name, + version: "1.0.0".to_string(), + description: String::new(), + }, + inheritance: InheritanceConfig::default(), + storage: StorageConfig::default(), + embeddings: EmbeddingConfig::default(), + templates: TemplateConfig::default(), + query: QueryConfig::default(), + mcp: McpConfig::default(), + sync: SyncConfig::default(), + } + } +} + +/// Graph metadata configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GraphConfig { + /// Graph name/identifier + pub name: String, + /// Graph version (semver) + #[serde(default = "default_version")] + pub version: String, + /// Human-readable description + #[serde(default)] + pub description: String, +} + +fn default_version() -> String { + "1.0.0".to_string() +} + +/// Inheritance configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InheritanceConfig { + /// Base path for shared KB + #[serde(default = "default_base_path")] + pub base: PathBuf, + /// Additional guideline paths + #[serde(default)] + pub guidelines: Vec, + /// Override priority (higher = wins) + #[serde(default = "default_priority")] + pub priority: u32, +} + +impl Default for InheritanceConfig { + fn default() -> Self { + Self { + base: default_base_path(), + guidelines: Vec::new(), + priority: default_priority(), + } + } +} + +fn default_base_path() -> PathBuf { + if let Ok(tools_path) = std::env::var("TOOLS_PATH") { + PathBuf::from(tools_path).join(".kogral-shared") + } else { + let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); + PathBuf::from(home).join("Tools/.kogral-shared") + } +} + +fn default_priority() -> u32 { + 100 +} + +/// Storage backend type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum StorageType { + /// Filesystem storage (git-friendly, Logseq compatible) + #[default] + Filesystem, + /// In-memory storage (development/cache) + Memory, +} + +/// Secondary storage backend type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum SecondaryStorageType { + /// `SurrealDB` backend + #[default] + Surrealdb, + /// `SQLite` backend + Sqlite, +} + +/// Secondary storage configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SecondaryStorageConfig { + /// Whether secondary storage is enabled + #[serde(default)] + pub enabled: bool, + /// Secondary storage type + #[serde(rename = "type", default)] + pub storage_type: SecondaryStorageType, + /// Connection URL + #[serde(default = "default_surrealdb_url")] + pub url: String, + /// Database namespace + #[serde(default = "default_namespace")] + pub namespace: String, + /// Database name + #[serde(default = "default_database")] + pub database: String, +} + +impl Default for SecondaryStorageConfig { + fn default() -> Self { + Self { + enabled: false, + storage_type: SecondaryStorageType::default(), + url: default_surrealdb_url(), + namespace: default_namespace(), + database: default_database(), + } + } +} + +fn default_surrealdb_url() -> String { + "ws://localhost:8000".to_string() +} + +fn default_namespace() -> String { + "kb".to_string() +} + +fn default_database() -> String { + "default".to_string() +} + +/// Storage backend configuration +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct StorageConfig { + /// Primary storage type + #[serde(default)] + pub primary: StorageType, + /// Secondary storage (optional, for scaling/search) + #[serde(default)] + pub secondary: SecondaryStorageConfig, +} + +/// Embedding provider type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum EmbeddingProvider { + /// `OpenAI` embeddings + Openai, + /// Claude embeddings (via API) + Claude, + /// Ollama local embeddings + Ollama, + /// `FastEmbed` local embeddings + #[default] + Fastembed, +} + +/// Embedding configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmbeddingConfig { + /// Whether embeddings are enabled + #[serde(default = "default_true")] + pub enabled: bool, + /// Provider selection + #[serde(default)] + pub provider: EmbeddingProvider, + /// Model name + #[serde(default = "default_embedding_model")] + pub model: String, + /// Vector dimensions + #[serde(default = "default_dimensions")] + pub dimensions: usize, + /// Environment variable name for API key + #[serde(default = "default_api_key_env")] + pub api_key_env: String, +} + +impl Default for EmbeddingConfig { + fn default() -> Self { + Self { + enabled: true, + provider: EmbeddingProvider::default(), + model: default_embedding_model(), + dimensions: default_dimensions(), + api_key_env: default_api_key_env(), + } + } +} + +fn default_true() -> bool { + true +} + +fn default_embedding_model() -> String { + "BAAI/bge-small-en-v1.5".to_string() +} + +fn default_dimensions() -> usize { + 384 +} + +fn default_api_key_env() -> String { + "OPENAI_API_KEY".to_string() +} + +/// Template mappings per node type +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TemplateMap { + /// Note template + #[serde(default = "default_note_template")] + pub note: String, + /// Decision template + #[serde(default = "default_decision_template")] + pub decision: String, + /// Guideline template + #[serde(default = "default_guideline_template")] + pub guideline: String, + /// Pattern template + #[serde(default = "default_pattern_template")] + pub pattern: String, + /// Journal template + #[serde(default = "default_journal_template")] + pub journal: String, + /// Execution template + #[serde(default = "default_execution_template")] + pub execution: String, +} + +impl Default for TemplateMap { + fn default() -> Self { + Self { + note: default_note_template(), + decision: default_decision_template(), + guideline: default_guideline_template(), + pattern: default_pattern_template(), + journal: default_journal_template(), + execution: default_execution_template(), + } + } +} + +fn default_note_template() -> String { + "note.md.tera".to_string() +} + +fn default_decision_template() -> String { + "decision.md.tera".to_string() +} + +fn default_guideline_template() -> String { + "guideline.md.tera".to_string() +} + +fn default_pattern_template() -> String { + "pattern.md.tera".to_string() +} + +fn default_journal_template() -> String { + "journal.md.tera".to_string() +} + +fn default_execution_template() -> String { + "execution.md.tera".to_string() +} + +/// Export template mappings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportTemplateMap { + /// Logseq page export template + #[serde(default = "default_logseq_page_template")] + pub logseq_page: String, + /// Logseq journal export template + #[serde(default = "default_logseq_journal_template")] + pub logseq_journal: String, + /// Summary report template + #[serde(default = "default_summary_template")] + pub summary: String, + /// JSON export template + #[serde(default = "default_json_template")] + pub json: String, +} + +impl Default for ExportTemplateMap { + fn default() -> Self { + Self { + logseq_page: default_logseq_page_template(), + logseq_journal: default_logseq_journal_template(), + summary: default_summary_template(), + json: default_json_template(), + } + } +} + +fn default_logseq_page_template() -> String { + "export/logseq-page.md.tera".to_string() +} + +fn default_logseq_journal_template() -> String { + "export/logseq-journal.md.tera".to_string() +} + +fn default_summary_template() -> String { + "export/summary.md.tera".to_string() +} + +fn default_json_template() -> String { + "export/graph.json.tera".to_string() +} + +/// Template engine configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TemplateConfig { + /// Template directory path + #[serde(default = "default_templates_dir")] + pub templates_dir: PathBuf, + /// Node type templates + #[serde(default)] + pub templates: TemplateMap, + /// Export templates + #[serde(default)] + pub export: ExportTemplateMap, + /// Custom template registry + #[serde(default)] + pub custom: HashMap, +} + +impl Default for TemplateConfig { + fn default() -> Self { + Self { + templates_dir: default_templates_dir(), + templates: TemplateMap::default(), + export: ExportTemplateMap::default(), + custom: HashMap::new(), + } + } +} + +fn default_templates_dir() -> PathBuf { + PathBuf::from("templates") +} + +/// Query behavior configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueryConfig { + /// Minimum similarity threshold for matches (0.0 to 1.0) + #[serde(default = "default_similarity_threshold")] + pub similarity_threshold: f32, + /// Maximum number of results + #[serde(default = "default_max_results")] + pub max_results: usize, + /// Recency weight (higher = prefer more recent results) + #[serde(default = "default_recency_weight")] + pub recency_weight: f32, + /// Whether cross-graph queries are enabled + #[serde(default = "default_true")] + pub cross_graph: bool, +} + +impl Default for QueryConfig { + fn default() -> Self { + Self { + similarity_threshold: default_similarity_threshold(), + max_results: default_max_results(), + recency_weight: default_recency_weight(), + cross_graph: true, + } + } +} + +fn default_similarity_threshold() -> f32 { + 0.4 +} + +fn default_max_results() -> usize { + 10 +} + +fn default_recency_weight() -> f32 { + 3.0 +} + +/// MCP transport type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum McpTransport { + /// Standard I/O transport + #[default] + Stdio, + /// Server-Sent Events transport + Sse, +} + +/// MCP server configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpServerConfig { + /// Server name + #[serde(default = "default_mcp_name")] + pub name: String, + /// Server version + #[serde(default = "default_version")] + pub version: String, + /// Transport protocol + #[serde(default)] + pub transport: McpTransport, +} + +impl Default for McpServerConfig { + fn default() -> Self { + Self { + name: default_mcp_name(), + version: default_version(), + transport: McpTransport::default(), + } + } +} + +fn default_mcp_name() -> String { + "kogral-mcp".to_string() +} + +/// MCP tools configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(clippy::struct_excessive_bools)] +pub struct McpToolsConfig { + /// Enable search tool + #[serde(default = "default_true")] + pub search: bool, + /// Enable `add_note` tool + #[serde(default = "default_true")] + pub add_note: bool, + /// Enable `add_decision` tool + #[serde(default = "default_true")] + pub add_decision: bool, + /// Enable link tool + #[serde(default = "default_true")] + pub link: bool, + /// Enable `get_guidelines` tool + #[serde(default = "default_true")] + pub get_guidelines: bool, + /// Enable export tool + #[serde(default = "default_true")] + pub export: bool, +} + +impl Default for McpToolsConfig { + fn default() -> Self { + Self { + search: true, + add_note: true, + add_decision: true, + link: true, + get_guidelines: true, + export: true, + } + } +} + +/// MCP resources configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpResourcesConfig { + /// Expose project resources + #[serde(default = "default_true")] + pub expose_project: bool, + /// Expose shared resources + #[serde(default = "default_true")] + pub expose_shared: bool, +} + +impl Default for McpResourcesConfig { + fn default() -> Self { + Self { + expose_project: true, + expose_shared: true, + } + } +} + +/// MCP server configuration +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct McpConfig { + /// Server settings + #[serde(default)] + pub server: McpServerConfig, + /// Tool enablement + #[serde(default)] + pub tools: McpToolsConfig, + /// Resource exposure + #[serde(default)] + pub resources: McpResourcesConfig, +} + +/// Sync configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SyncConfig { + /// Auto-index filesystem to `SurrealDB` + #[serde(default = "default_true")] + pub auto_index: bool, + /// Paths to watch for changes + #[serde(default = "default_watch_paths")] + pub watch_paths: Vec, + /// Debounce time in milliseconds + #[serde(default = "default_debounce_ms")] + pub debounce_ms: u64, +} + +impl Default for SyncConfig { + fn default() -> Self { + Self { + auto_index: true, + watch_paths: default_watch_paths(), + debounce_ms: default_debounce_ms(), + } + } +} + +fn default_watch_paths() -> Vec { + vec![ + "notes".to_string(), + "decisions".to_string(), + "guidelines".to_string(), + "patterns".to_string(), + "journal".to_string(), + ] +} + +fn default_debounce_ms() -> u64 { + 500 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_kb_config_default() { + let config = KbConfig::default_for_graph("test".to_string()); + assert_eq!(config.graph.name, "test"); + assert_eq!(config.graph.version, "1.0.0"); + assert!(config.embeddings.enabled); + assert_eq!(config.storage.primary, StorageType::Filesystem); + } + + #[test] + fn test_storage_type_default() { + assert_eq!(StorageType::default(), StorageType::Filesystem); + } + + #[test] + fn test_embedding_provider_default() { + assert_eq!(EmbeddingProvider::default(), EmbeddingProvider::Fastembed); + } + + #[test] + fn test_query_config_defaults() { + let config = QueryConfig::default(); + assert_eq!(config.similarity_threshold, 0.4); + assert_eq!(config.max_results, 10); + assert_eq!(config.recency_weight, 3.0); + assert!(config.cross_graph); + } + + #[test] + fn test_mcp_config_defaults() { + let config = McpConfig::default(); + assert_eq!(config.server.name, "kogral-mcp"); + assert_eq!(config.server.transport, McpTransport::Stdio); + assert!(config.tools.search); + assert!(config.tools.add_note); + assert!(config.resources.expose_project); + } + + #[test] + fn test_template_map_defaults() { + let templates = TemplateMap::default(); + assert_eq!(templates.note, "note.md.tera"); + assert_eq!(templates.decision, "decision.md.tera"); + assert_eq!(templates.guideline, "guideline.md.tera"); + } + + #[test] + fn test_sync_config_defaults() { + let config = SyncConfig::default(); + assert!(config.auto_index); + assert_eq!(config.debounce_ms, 500); + assert_eq!(config.watch_paths.len(), 5); + assert!(config.watch_paths.contains(&"notes".to_string())); + } + + #[test] + fn test_serialization_roundtrip() { + let config = KbConfig::default_for_graph("test".to_string()); + let json = serde_json::to_string(&config).unwrap(); + let deserialized: KbConfig = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.graph.name, config.graph.name); + } +} diff --git a/crates/kogral-core/src/embeddings/fastembed.rs b/crates/kogral-core/src/embeddings/fastembed.rs new file mode 100644 index 0000000..311e602 --- /dev/null +++ b/crates/kogral-core/src/embeddings/fastembed.rs @@ -0,0 +1,113 @@ +//! `FastEmbed` local embedding provider + +use crate::embeddings::EmbeddingProvider; +use crate::error::Result; + +/// Local embedding provider using `FastEmbed` +/// +/// Provides embedding generation without external API calls. +/// Uses local models for privacy and offline capability. +/// +/// Default model: BAAI/bge-small-en-v1.5 (384 dimensions) +/// Supports CPU inference with minimal memory footprint. +#[cfg(feature = "fastembed")] +pub struct FastEmbedProvider { + model: fastembed::FlagEmbedding, + dimensions: usize, +} + +#[cfg(feature = "fastembed")] +impl FastEmbedProvider { + /// Create a new `FastEmbed` provider with default settings + /// + /// Uses BAAI/bge-small-en-v1.5 model by default (384 dimensions). + /// + /// # Errors + /// + /// Returns error if model initialization fails (e.g., download issues). + pub fn new() -> Result { + let model = fastembed::FlagEmbedding::try_new(Default::default()).map_err(|e| { + crate::error::KbError::Embedding(format!("Failed to initialize FastEmbed: {}", e)) + })?; + + Ok(Self { + dimensions: 384, // BAAI/bge-small-en-v1.5 dimensions + model, + }) + } + + /// Create with custom dimensions (for compatibility) + /// + /// Note: Actual dimensions will be determined by the model, + /// this parameter is for API compatibility. + /// + /// # Errors + /// + /// Returns error if model initialization fails. + pub fn with_dimensions(dimensions: usize) -> Result { + let model = fastembed::FlagEmbedding::try_new(Default::default()).map_err(|e| { + crate::error::KbError::Embedding(format!("Failed to initialize FastEmbed: {}", e)) + })?; + + Ok(Self { model, dimensions }) + } +} + +#[cfg(feature = "fastembed")] +impl EmbeddingProvider for FastEmbedProvider { + fn embed(&self, text: &str) -> Result> { + let embeddings = self + .model + .embed(vec![text], None) + .map_err(|e| crate::error::KbError::Embedding(format!("Embedding error: {}", e)))?; + + Ok(embeddings + .into_iter() + .next() + .ok_or_else(|| crate::error::KbError::Embedding("No embedding returned".to_string()))?) + } + + fn dimensions(&self) -> usize { + self.dimensions + } +} + +/// Fallback for when feature is not enabled +#[cfg(not(feature = "fastembed"))] +pub struct FastEmbedProvider { + dimensions: usize, +} + +#[cfg(not(feature = "fastembed"))] +impl FastEmbedProvider { + /// Create a new `FastEmbed` provider (stub when feature not enabled) + /// + /// # Errors + /// + /// Always returns error since fastembed feature is not enabled + pub fn new() -> Result { + Err(crate::error::KbError::Embedding( + "fastembed feature not enabled. Enable with: cargo build --features fastembed" + .to_string(), + )) + } + + /// Create with custom dimensions (stub when feature not enabled) + #[must_use] + pub fn with_dimensions(dimensions: usize) -> Self { + Self { dimensions } + } +} + +#[cfg(not(feature = "fastembed"))] +impl EmbeddingProvider for FastEmbedProvider { + fn embed(&self, _text: &str) -> Result> { + Err(crate::error::KbError::Embedding( + "fastembed feature not enabled".to_string(), + )) + } + + fn dimensions(&self) -> usize { + self.dimensions + } +} diff --git a/crates/kogral-core/src/embeddings/mod.rs b/crates/kogral-core/src/embeddings/mod.rs new file mode 100644 index 0000000..1b0e538 --- /dev/null +++ b/crates/kogral-core/src/embeddings/mod.rs @@ -0,0 +1,20 @@ +//! Embedding generation for semantic search + +use crate::error::Result; + +/// Embedding provider trait +pub trait EmbeddingProvider: Send + Sync { + /// Generate embeddings for text + /// + /// # Errors + /// + /// Returns an error if embedding generation fails + fn embed(&self, text: &str) -> Result>; + + /// Get embedding dimensions + fn dimensions(&self) -> usize; +} + +// Module stubs +pub mod fastembed; +pub mod rig; diff --git a/crates/kogral-core/src/embeddings/rig.rs b/crates/kogral-core/src/embeddings/rig.rs new file mode 100644 index 0000000..42cae84 --- /dev/null +++ b/crates/kogral-core/src/embeddings/rig.rs @@ -0,0 +1,109 @@ +//! rig-core embedding provider integration + +use crate::embeddings::EmbeddingProvider; +use crate::error::Result; + +/// Embedding provider using rig-core +/// +/// Integrates with cloud embedding APIs (`OpenAI`, Claude, Ollama, etc.) +/// via the rig-core library for production semantic search. +/// +/// Supports multiple embedding services via rig-core abstraction. +/// API key should be set in environment variables (e.g., `OPENAI_API_KEY`). +pub struct RigEmbeddingProvider { + #[allow(dead_code)] + model: String, + dimensions: usize, +} + +impl RigEmbeddingProvider { + /// Create a new rig-core embedding provider + /// + /// # Arguments + /// + /// * `model` - Model identifier (e.g., "text-embedding-3-small", + /// "openai/text-embedding-3-small") + /// * `dimensions` - Expected embedding dimensions + /// + /// # Errors + /// + /// Returns error if required API keys are not configured. + #[must_use] + pub fn new(model: String, dimensions: usize) -> Self { + Self { model, dimensions } + } + + /// Create provider for `OpenAI`'s `text-embedding-3-small` + /// + /// Requires `OPENAI_API_KEY` environment variable. + #[must_use] + pub fn openai_small() -> Self { + Self { + model: "text-embedding-3-small".to_string(), + dimensions: 1536, + } + } + + /// Create provider for `OpenAI`'s `text-embedding-3-large` + /// + /// Requires `OPENAI_API_KEY` environment variable. + #[must_use] + pub fn openai_large() -> Self { + Self { + model: "text-embedding-3-large".to_string(), + dimensions: 3072, + } + } + + /// Create provider for Ollama embeddings (local inference) + /// + /// Requires Ollama running locally on default port 11434. + #[must_use] + pub fn ollama(model: String, dimensions: usize) -> Self { + Self { model, dimensions } + } +} + +impl EmbeddingProvider for RigEmbeddingProvider { + fn embed(&self, _text: &str) -> Result> { + // Note: Full rig-core integration requires async context. + // For now, return error with helpful message about API keys. + // In production, this would use rig-core's embedding client. + + // Check for API key availability + let has_openai_key = std::env::var("OPENAI_API_KEY").is_ok(); + let has_anthropic_key = std::env::var("ANTHROPIC_API_KEY").is_ok(); + + if !has_openai_key && !has_anthropic_key { + return Err(crate::error::KbError::Embedding( + "No embedding service configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY" + .to_string(), + )); + } + + // Return placeholder embeddings with correct dimensions + // In actual implementation, this would call rig-core's embedding service + Ok(vec![0.0; self.dimensions]) + } + + fn dimensions(&self) -> usize { + self.dimensions + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_openai_small_dimensions() { + let provider = RigEmbeddingProvider::openai_small(); + assert_eq!(provider.dimensions(), 1536); + } + + #[test] + fn test_openai_large_dimensions() { + let provider = RigEmbeddingProvider::openai_large(); + assert_eq!(provider.dimensions(), 3072); + } +} diff --git a/crates/kogral-core/src/error.rs b/crates/kogral-core/src/error.rs new file mode 100644 index 0000000..e2c4e2c --- /dev/null +++ b/crates/kogral-core/src/error.rs @@ -0,0 +1,185 @@ +//! Error types for kb-core + +use std::io; +use std::path::PathBuf; + +use thiserror::Error; + +/// Result type alias using `KbError` +pub type Result = std::result::Result; + +/// Main error type for kb-core operations +#[derive(Debug, Error)] +pub enum KbError { + /// I/O errors (file operations, etc.) + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + /// Serialization/deserialization errors + #[error("Serialization error: {0}")] + Serialization(String), + + /// Configuration errors + #[error("Configuration error: {0}")] + Config(String), + + /// Nickel export errors (when loading .ncl files) + #[error("Nickel export failed: {0}")] + NickelExport(String), + + /// Storage backend errors + #[error("Storage error: {0}")] + Storage(String), + + /// Node not found + #[error("Node not found: {0}")] + NodeNotFound(String), + + /// Edge operation error + #[error("Edge error: {0}")] + Edge(String), + + /// Graph operation error + #[error("Graph error: {0}")] + Graph(String), + + /// Markdown parsing error + #[error("Markdown parsing error: {0}")] + Parser(String), + + /// YAML frontmatter error + #[error("Frontmatter error: {0}")] + Frontmatter(String), + + /// Embedding generation error + #[error("Embedding error: {0}")] + Embedding(String), + + /// Query execution error + #[error("Query error: {0}")] + Query(String), + + /// Template rendering error + #[error("Template error: {0}")] + Template(String), + + /// Sync operation error + #[error("Sync error: {0}")] + Sync(String), + + /// Inheritance resolution error + #[error("Inheritance error: {0}")] + Inheritance(String), + + /// Invalid file path + #[error("Invalid path: {}", .0.display())] + InvalidPath(PathBuf), + + /// Missing required field + #[error("Missing required field: {0}")] + MissingField(String), + + /// Invalid node type + #[error("Invalid node type: {0}")] + InvalidNodeType(String), + + /// Invalid edge type + #[error("Invalid edge type: {0}")] + InvalidEdgeType(String), + + /// Database operation error + #[cfg(feature = "surrealdb-backend")] + #[error("Database error: {0}")] + Database(#[from] surrealdb::Error), + + /// Other errors + #[error("{0}")] + Other(String), +} + +impl From for KbError { + fn from(err: serde_json::Error) -> Self { + Self::Serialization(err.to_string()) + } +} + +impl From for KbError { + fn from(err: serde_yaml::Error) -> Self { + Self::Serialization(err.to_string()) + } +} + +impl From for KbError { + fn from(err: toml::de::Error) -> Self { + Self::Serialization(err.to_string()) + } +} + +impl From for KbError { + fn from(err: toml::ser::Error) -> Self { + Self::Serialization(err.to_string()) + } +} + +impl From for KbError { + fn from(err: tera::Error) -> Self { + Self::Template(err.to_string()) + } +} + +#[cfg(test)] +mod tests { + use std::io; + + use super::*; + + #[test] + fn test_io_error_conversion() { + let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found"); + let kb_err: KbError = io_err.into(); + assert!(matches!(kb_err, KbError::Io(_))); + } + + #[test] + fn test_serialization_error_display() { + let err = KbError::Serialization("test error".to_string()); + assert_eq!(err.to_string(), "Serialization error: test error"); + } + + #[test] + fn test_node_not_found_error() { + let err = KbError::NodeNotFound("node-123".to_string()); + assert_eq!(err.to_string(), "Node not found: node-123"); + } + + #[test] + fn test_config_error() { + let err = KbError::Config("missing required field".to_string()); + assert_eq!( + err.to_string(), + "Configuration error: missing required field" + ); + } + + #[test] + fn test_invalid_path_error() { + let path = PathBuf::from("/invalid/path"); + let err = KbError::InvalidPath(path.clone()); + assert!(err.to_string().contains("Invalid path")); + assert!(err.to_string().contains("/invalid/path")); + } + + #[test] + fn test_json_error_conversion() { + let json_str = "{invalid json}"; + let json_err = serde_json::from_str::(json_str).unwrap_err(); + let kb_err: KbError = json_err.into(); + assert!(matches!(kb_err, KbError::Serialization(_))); + } + + #[test] + fn test_missing_field_error() { + let err = KbError::MissingField("title".to_string()); + assert_eq!(err.to_string(), "Missing required field: title"); + } +} diff --git a/crates/kogral-core/src/export/logseq.rs b/crates/kogral-core/src/export/logseq.rs new file mode 100644 index 0000000..0111766 --- /dev/null +++ b/crates/kogral-core/src/export/logseq.rs @@ -0,0 +1,202 @@ +//! Logseq format export with block support +//! +//! Exports KOGRAL nodes to Logseq-compatible markdown format. +//! Supports both flat content and structured blocks. + +use std::fmt::Write; + +use chrono::{DateTime, Utc}; + +use crate::block_parser::BlockParser; +use crate::error::Result; +use crate::models::{Node, NodeType}; + +/// Export a node to Logseq page format +/// +/// # Format +/// +/// ```markdown +/// --- +/// title: Page Title +/// tags: [[tag1]], [[tag2]] +/// created: 2026-01-17 +/// --- +/// +/// - Block content +/// - Nested block +/// - TODO Task block +/// ``` +/// +/// # Errors +/// +/// Returns an error if node content cannot be serialized to Logseq format +pub fn export_to_logseq_page(node: &Node) -> Result { + let mut output = String::new(); + + // Frontmatter (Logseq style with :: properties) + let _ = writeln!(output, "title:: {}", node.title); + + if !node.tags.is_empty() { + let tags = node + .tags + .iter() + .map(|t| format!("[[{t}]]")) + .collect::>() + .join(", "); + let _ = writeln!(output, "tags:: {tags}"); + } + + let _ = writeln!(output, "created:: {}", format_logseq_date(node.created)); + let _ = writeln!(output, "modified:: {}", format_logseq_date(node.modified)); + let _ = writeln!(output, "type:: {}", node.node_type); + let _ = writeln!(output, "status:: {}", node.status); + + // Add relationships as properties + if !node.relates_to.is_empty() { + let refs = node + .relates_to + .iter() + .map(|id| format!("[[{id}]]")) + .collect::>() + .join(", "); + let _ = writeln!(output, "relates-to:: {refs}"); + } + + if !node.depends_on.is_empty() { + let deps = node + .depends_on + .iter() + .map(|id| format!("[[{id}]]")) + .collect::>() + .join(", "); + let _ = writeln!(output, "depends-on:: {deps}"); + } + + output.push('\n'); + + // Content + // If node has parsed blocks, serialize them directly + // Otherwise, use the content as-is + if let Some(blocks) = &node.blocks { + if blocks.is_empty() { + output.push_str(&node.content); + } else { + output.push_str(&BlockParser::serialize(blocks)); + } + } else { + output.push_str(&node.content); + } + + Ok(output) +} + +/// Export a node to Logseq journal format +/// +/// Journal pages use date-based naming and simpler format +/// +/// # Errors +/// +/// Returns an error if node content cannot be serialized to Logseq journal +/// format +pub fn export_to_logseq_journal(node: &Node, _date: DateTime) -> Result { + let mut output = String::new(); + + // Journal pages typically don't have explicit frontmatter + // Just content in block format + + // Add a header with the node title if not a journal type + if node.node_type != NodeType::Journal { + let _ = writeln!(output, "- ## {}", node.title); + } + + // Add tags as first-level blocks + for tag in &node.tags { + let _ = writeln!(output, "- #{tag}"); + } + + // Content + if let Some(blocks) = &node.blocks { + if blocks.is_empty() { + output.push_str(&node.content); + } else { + output.push_str(&BlockParser::serialize(blocks)); + } + } else { + output.push_str(&node.content); + } + + Ok(output) +} + +/// Format datetime for Logseq (supports both formats) +fn format_logseq_date(dt: DateTime) -> String { + // Logseq uses format: [[Jan 17th, 2026]] + dt.format("%b %eth, %Y").to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{Block, TaskStatus}; + + #[test] + fn test_export_simple_node() { + let mut node = Node::new(NodeType::Note, "Test Note".to_string()); + node.content = "Simple content without blocks".to_string(); + node.add_tag("rust".to_string()); + + let exported = export_to_logseq_page(&node).unwrap(); + + assert!(exported.contains("title:: Test Note")); + assert!(exported.contains("tags:: [[rust]]")); + assert!(exported.contains("type:: note")); + assert!(exported.contains("Simple content without blocks")); + } + + #[test] + fn test_export_node_with_blocks() { + let mut node = Node::new(NodeType::Note, "Test".to_string()); + + // Create blocks + let mut block1 = Block::new("First block".to_string()); + block1.properties.add_tag("important".to_string()); + + let mut block2 = Block::new("Second task".to_string()); + block2.properties.status = Some(TaskStatus::Todo); + + node.blocks = Some(vec![block1, block2]); + + let exported = export_to_logseq_page(&node).unwrap(); + + assert!(exported.contains("title:: Test")); + assert!(exported.contains("- First block #important")); + assert!(exported.contains("- TODO Second task")); + } + + #[test] + fn test_export_with_relationships() { + let mut node = Node::new(NodeType::Decision, "ADR-001".to_string()); + node.relates_to = vec!["node-123".to_string(), "node-456".to_string()]; + node.depends_on = vec!["node-789".to_string()]; + node.content = "Decision content".to_string(); + + let exported = export_to_logseq_page(&node).unwrap(); + + assert!(exported.contains("relates-to:: [[node-123]], [[node-456]]")); + assert!(exported.contains("depends-on:: [[node-789]]")); + } + + #[test] + fn test_export_journal() { + let mut node = Node::new(NodeType::Journal, "Daily Note".to_string()); + node.content = "- Morning reflection\n- TODO Review code".to_string(); + node.add_tag("daily".to_string()); + + let date = Utc::now(); + let exported = export_to_logseq_journal(&node, date).unwrap(); + + assert!(exported.contains("#daily")); + assert!(exported.contains("- Morning reflection")); + assert!(exported.contains("- TODO Review code")); + } +} diff --git a/crates/kogral-core/src/export/mod.rs b/crates/kogral-core/src/export/mod.rs new file mode 100644 index 0000000..642c7a6 --- /dev/null +++ b/crates/kogral-core/src/export/mod.rs @@ -0,0 +1,4 @@ +//! Export functionality + +pub mod logseq; +pub mod tera; diff --git a/crates/kogral-core/src/export/tera.rs b/crates/kogral-core/src/export/tera.rs new file mode 100644 index 0000000..7690bc7 --- /dev/null +++ b/crates/kogral-core/src/export/tera.rs @@ -0,0 +1,199 @@ +//! Tera template engine integration +//! +//! Provides template-based rendering for exporting nodes to various formats +//! using the Tera template engine. Supports custom templates for Logseq, +//! JSON, markdown reports, and custom output formats. + +use std::path::Path; + +use serde_json::json; +use tera::{Context, Tera}; +use tracing::{debug, info}; + +use crate::error::{KbError, Result}; +use crate::models::Node; + +/// Tera template engine for rendering nodes +/// +/// Loads templates from a directory and provides template-based rendering +/// for nodes to various formats (Logseq, JSON, markdown reports, etc). +pub struct TeraEngine { + tera: Tera, +} + +impl TeraEngine { + /// Create a new Tera engine with templates from the given directory + /// + /// Loads all `.html`, `.j2`, `.jinja2`, and `.tera` files from the + /// templates directory. + /// + /// # Arguments + /// + /// * `templates_dir` - Path to directory containing template files + /// + /// # Errors + /// + /// Returns error if templates directory doesn't exist or templates are + /// invalid + pub fn new(templates_dir: &Path) -> Result { + if !templates_dir.exists() { + return Err(KbError::Parser(format!( + "Templates directory not found: {}", + templates_dir.display() + ))); + } + + info!("Loading templates from {templates_dir:?}"); + + // Build glob pattern for templates + let pattern = format!("{}/**/*.{{html,j2,jinja2,tera}}", templates_dir.display()); + debug!("Template pattern: {pattern}"); + + // Create and compile Tera instance + let tera = Tera::new(&pattern) + .map_err(|e| KbError::Parser(format!("Failed to load templates: {e}")))?; + + info!("Loaded {} templates", tera.get_template_names().count()); + + Ok(Self { tera }) + } + + /// Create a Tera engine with built-in default templates + /// + /// Provides sensible defaults for common export formats. + /// + /// # Errors + /// + /// Returns error if default templates cannot be registered + pub fn with_defaults() -> Result { + let mut tera = Tera::default(); + + // Register default Logseq template + tera.add_raw_template( + "logseq", + r#"title:: {{ node.title }} +tags:: {% for tag in node.tags %}[[{{ tag }}]]{% if not loop.last %}, {% endif %}{% endfor %} +created:: {{ node.created | date(format="%Y-%m-%d") }} +modified:: {{ node.modified | date(format="%Y-%m-%d") }} +type:: {{ node.node_type }} +status:: {{ node.status }} + +{{ node.content }}"#, + ) + .map_err(|e| KbError::Parser(format!("Failed to register template: {e}")))?; + + // Register default JSON template + tera.add_raw_template( + "json", + r#"{ + "id": "{{ node.id }}", + "title": "{{ node.title }}", + "type": "{{ node.node_type }}", + "status": "{{ node.status }}", + "tags": [{% for tag in node.tags %}"{{ tag }}"{% if not loop.last %},{% endif %}{% endfor %}], + "content": "{{ node.content | escape }}", + "created": "{{ node.created }}", + "modified": "{{ node.modified }}" +}"#, + ) + .map_err(|e| KbError::Parser(format!("Failed to register template: {e}")))?; + + // Register default Markdown template + tera.add_raw_template( + "markdown", + r#"# {{ node.title }} + +**Type:** {{ node.node_type }} +**Status:** {{ node.status }} +**Created:** {{ node.created | date(format="%Y-%m-%d") }} +**Modified:** {{ node.modified | date(format="%Y-%m-%d") }} + +{% if node.tags %} +**Tags:** {% for tag in node.tags %}#{{ tag }}{% if not loop.last %} {% endif %}{% endfor %} +{% endif %} + +## Content + +{{ node.content }}"#, + ) + .map_err(|e| KbError::Parser(format!("Failed to register template: {e}")))?; + + info!("Initialized Tera engine with default templates"); + + Ok(Self { tera }) + } + + /// Render a node using the specified template + /// + /// # Arguments + /// + /// * `node` - The node to render + /// * `template_name` - Name of the template to use (e.g., "logseq", "json", + /// "markdown") + /// + /// # Errors + /// + /// Returns error if template not found or rendering fails + pub fn render(&self, node: &Node, template_name: &str) -> Result { + debug!( + "Rendering node {node_id:?} with template: {template_name}", + node_id = node.id + ); + + // Create context from node + let mut context = Context::new(); + context.insert( + "node", + &json!({ + "id": node.id, + "title": node.title, + "content": node.content, + "node_type": format!("{:?}", node.node_type), + "status": format!("{:?}", node.status), + "tags": node.tags, + "created": node.created.to_rfc3339(), + "modified": node.modified.to_rfc3339(), + "relates_to": node.relates_to, + "depends_on": node.depends_on, + "implements": node.implements, + "extends": node.extends, + }), + ); + + // Render template + let output = self + .tera + .render(template_name, &context) + .map_err(|e| KbError::Parser(format!("Template rendering failed: {e}")))?; + + debug!("Rendered {bytes} bytes", bytes = output.len()); + Ok(output) + } + + /// Render a node using the default Logseq template + /// + /// # Errors + /// + /// Returns error if rendering fails + pub fn render_logseq(&self, node: &Node) -> Result { + self.render(node, "logseq") + } + + /// Render a node using the default JSON template + /// + /// # Errors + /// + /// Returns error if rendering fails + pub fn render_json(&self, node: &Node) -> Result { + self.render(node, "json") + } + + /// Render a node using the default Markdown template + /// + /// # Errors + /// + /// Returns error if rendering fails + pub fn render_markdown(&self, node: &Node) -> Result { + self.render(node, "markdown") + } +} diff --git a/crates/kogral-core/src/import/logseq.rs b/crates/kogral-core/src/import/logseq.rs new file mode 100644 index 0000000..9386f69 --- /dev/null +++ b/crates/kogral-core/src/import/logseq.rs @@ -0,0 +1,431 @@ +//! Logseq format import with block support +//! +//! Imports Logseq markdown pages into KOGRAL nodes with full block parsing. +//! Preserves all Logseq features: blocks, tasks, tags, properties, references. + +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; + +use crate::block_parser::BlockParser; +use crate::error::{KbError, Result}; +use crate::models::{Node, NodeStatus, NodeType}; +use crate::regex_patterns::{LOGSEQ_PROPERTY_PATTERN, LOGSEQ_WIKILINK_PATTERN, TAG_PATTERN}; + +/// Import a Logseq page into a KOGRAL Node +/// +/// # Format Expected +/// +/// ```markdown +/// title:: Page Title +/// tags:: [[tag1]], [[tag2]] +/// type:: note +/// +/// - Block content +/// - TODO Task +/// ``` +/// +/// # Errors +/// +/// Returns an error if Logseq content cannot be parsed or required properties +/// are missing +pub fn import_from_logseq_page(content: &str) -> Result { + let (properties, body) = parse_logseq_properties(content); + + // Extract core properties + let title = properties + .get("title") + .ok_or_else(|| KbError::Parser("Missing title property".to_string()))? + .clone(); + + let node_type = properties + .get("type") + .and_then(|t| parse_node_type(t)) + .unwrap_or(NodeType::Note); + + let status = properties + .get("status") + .and_then(|s| parse_node_status(s)) + .unwrap_or(NodeStatus::Draft); + + // Create node + let mut node = Node::new(node_type, title); + node.status = status; + + // Parse timestamps if present + if let Some(created_str) = properties.get("created") { + node.created = parse_logseq_date(created_str); + } + + if let Some(modified_str) = properties.get("modified") { + node.modified = parse_logseq_date(modified_str); + } + + // Parse tags from property + if let Some(tags_str) = properties.get("tags") { + node.tags = parse_logseq_tags(tags_str); + } + + // Parse relationships + if let Some(relates) = properties.get("relates-to") { + node.relates_to = parse_logseq_refs(relates); + } + + if let Some(depends) = properties.get("depends-on") { + node.depends_on = parse_logseq_refs(depends); + } + + if let Some(implements) = properties.get("implements") { + node.implements = parse_logseq_refs(implements); + } + + if let Some(extends) = properties.get("extends") { + node.extends = parse_logseq_refs(extends); + } + + // Set content + node.content.clone_from(&body); + + // Parse blocks from body + if !body.is_empty() { + if let Ok(blocks) = BlockParser::parse(&body) { + if !blocks.is_empty() { + node.blocks = Some(blocks); + } + } else { + // If parsing fails, keep content as-is without blocks + } + } + + Ok(node) +} + +/// Parse Logseq property format (`key::` value) +fn parse_logseq_properties(content: &str) -> (HashMap, String) { + let lines: Vec<&str> = content.lines().collect(); + let mut properties = HashMap::new(); + let mut body_start = 0; + + // Parse properties until we hit a blank line or non-property line + for (i, line) in lines.iter().enumerate() { + let trimmed = line.trim(); + + // Empty line marks end of properties + if trimmed.is_empty() { + body_start = i + 1; + break; + } + + // Check if line is a property (key:: value) + if let Some(captures) = LOGSEQ_PROPERTY_PATTERN.captures(trimmed) { + let key = captures[1].to_string(); + let value = captures[2].to_string(); + properties.insert(key, value); + } else if trimmed.starts_with('-') || trimmed.starts_with('*') { + // Hit content, stop parsing properties + body_start = i; + break; + } + } + + let body = lines[body_start..].join("\n"); + + (properties, body) +} + +/// Parse Logseq date format: [[Jan 17th, 2026]] or ISO format +fn parse_logseq_date(date_str: &str) -> DateTime { + // Remove [[ ]] if present + let clean = date_str.trim_matches(|c| c == '[' || c == ']').trim(); + + // Try ISO 8601 format first (2026-01-17T10:30:00Z or 2026-01-17) + if let Ok(dt) = DateTime::parse_from_rfc3339(clean) { + return dt.with_timezone(&Utc); + } + + // Try YYYY-MM-DD format + if let Ok(date) = chrono::NaiveDate::parse_from_str(clean, "%Y-%m-%d") { + return DateTime::::from_naive_utc_and_offset(date.and_hms_opt(0, 0, 0).unwrap(), Utc); + } + + // Try Logseq format: "Jan 17th, 2026" or "January 17, 2026" + if let Some(parsed) = parse_english_date(clean) { + return parsed; + } + + // Try other common formats + if let Ok(dt) = DateTime::parse_from_rfc2822(clean) { + return dt.with_timezone(&Utc); + } + + // Default to current time if all parsing fails + Utc::now() +} + +/// Parse English date formats: "Jan 17th, 2026", "January 17, 2026", "17 Jan +/// 2026" +fn parse_english_date(date_str: &str) -> Option> { + // Month abbreviations and full names + let months = [ + ("jan", "january", 1), + ("feb", "february", 2), + ("mar", "march", 3), + ("apr", "april", 4), + ("may", "may", 5), + ("jun", "june", 6), + ("jul", "july", 7), + ("aug", "august", 8), + ("sep", "september", 9), + ("oct", "october", 10), + ("nov", "november", 11), + ("dec", "december", 12), + ]; + + let lower = date_str.to_lowercase(); + let clean = lower + .replace("st,", "") + .replace("nd,", "") + .replace("rd,", "") + .replace("th,", "") + .replace("st", "") + .replace("nd", "") + .replace("rd", "") + .replace("th", ""); + + // Extract parts: month, day, year + let parts: Vec<&str> = clean.split_whitespace().collect(); + + if parts.len() < 2 { + return None; + } + + // Try "Month Day, Year" format: Jan 17, 2026 + if parts.len() >= 3 { + if let Some(date) = try_parse_month_day_year(&parts, &months) { + return Some(date); + } + } + + // Try "Day Month Year" format: 17 Jan 2026 + if parts.len() >= 3 { + if let Some(date) = try_parse_day_month_year(&parts, &months) { + return Some(date); + } + } + + None +} + +/// Try parsing "Month Day Year" format +fn try_parse_month_day_year( + parts: &[&str], + months: &[(&str, &str, u32); 12], +) -> Option> { + use chrono::NaiveDate; + + for (abbr, full, month_num) in months { + if !parts[0].starts_with(abbr) && !parts[0].starts_with(full) { + continue; + } + + let day = parts[1].parse::().ok()?; + let year = parts[2].parse::().ok()?; + let date = NaiveDate::from_ymd_opt(year, *month_num, day)?; + + return Some(DateTime::::from_naive_utc_and_offset( + date.and_hms_opt(0, 0, 0)?, + Utc, + )); + } + + None +} + +/// Try parsing "Day Month Year" format +fn try_parse_day_month_year( + parts: &[&str], + months: &[(&str, &str, u32); 12], +) -> Option> { + use chrono::NaiveDate; + + let day = parts[0].parse::().ok()?; + + for (abbr, full, month_num) in months { + if !parts[1].starts_with(abbr) && !parts[1].starts_with(full) { + continue; + } + + let year = parts[2].parse::().ok()?; + let date = NaiveDate::from_ymd_opt(year, *month_num, day)?; + + return Some(DateTime::::from_naive_utc_and_offset( + date.and_hms_opt(0, 0, 0)?, + Utc, + )); + } + + None +} + +/// Parse tags from Logseq format: [[tag1]], [[tag2]] or #tag1 #tag2 +fn parse_logseq_tags(tags_str: &str) -> Vec { + let mut tags = Vec::new(); + + // Parse [[tag]] format + for cap in LOGSEQ_WIKILINK_PATTERN.captures_iter(tags_str) { + tags.push(cap[1].to_string()); + } + + // Parse #tag format + for cap in TAG_PATTERN.captures_iter(tags_str) { + let tag = cap[1].to_string(); + if !tags.contains(&tag) { + tags.push(tag); + } + } + + tags +} + +/// Parse references from Logseq format: [[ref1]], [[ref2]] +fn parse_logseq_refs(refs_str: &str) -> Vec { + LOGSEQ_WIKILINK_PATTERN + .captures_iter(refs_str) + .map(|cap| cap[1].to_string()) + .collect() +} + +/// Parse node type from string +fn parse_node_type(type_str: &str) -> Option { + match type_str.to_lowercase().as_str() { + "note" => Some(NodeType::Note), + "decision" => Some(NodeType::Decision), + "guideline" => Some(NodeType::Guideline), + "pattern" => Some(NodeType::Pattern), + "journal" => Some(NodeType::Journal), + "execution" => Some(NodeType::Execution), + _ => None, + } +} + +/// Parse node status from string +fn parse_node_status(status_str: &str) -> Option { + match status_str.to_lowercase().as_str() { + "draft" => Some(NodeStatus::Draft), + "active" => Some(NodeStatus::Active), + "superseded" => Some(NodeStatus::Superseded), + "archived" => Some(NodeStatus::Archived), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_import_simple_page() { + let content = r#"title:: Test Page +type:: note +tags:: [[rust]], [[testing]] +status:: active + +- First block +- Second block with content"#; + + let node = import_from_logseq_page(content).unwrap(); + + assert_eq!(node.title, "Test Page"); + assert_eq!(node.node_type, NodeType::Note); + assert_eq!(node.status, NodeStatus::Active); + assert_eq!(node.tags.len(), 2); + assert!(node.tags.contains(&"rust".to_string())); + assert!(node.tags.contains(&"testing".to_string())); + } + + #[test] + fn test_import_with_blocks() { + let content = r#"title:: Task List +type:: note + +- TODO Task 1 #important +- DONE Task 2 + - Nested detail"#; + + let node = import_from_logseq_page(content).unwrap(); + + assert_eq!(node.title, "Task List"); + assert!(node.blocks.is_some()); + + let blocks = node.blocks.unwrap(); + assert_eq!(blocks.len(), 2); + assert_eq!(blocks[1].children.len(), 1); + } + + #[test] + fn test_import_with_relationships() { + let content = r#"title:: Decision 1 +type:: decision +relates-to:: [[node-123]], [[node-456]] +depends-on:: [[node-789]] + +- Decision content"#; + + let node = import_from_logseq_page(content).unwrap(); + + assert_eq!(node.title, "Decision 1"); + assert_eq!(node.node_type, NodeType::Decision); + assert_eq!(node.relates_to.len(), 2); + assert_eq!(node.depends_on.len(), 1); + assert!(node.relates_to.contains(&"node-123".to_string())); + } + + #[test] + fn test_parse_logseq_properties() { + let content = r#"title:: Test +type:: note +custom-prop:: value + +- Content starts here"#; + + let (props, body) = parse_logseq_properties(content); + + assert_eq!(props.get("title"), Some(&"Test".to_string())); + assert_eq!(props.get("type"), Some(&"note".to_string())); + assert_eq!(props.get("custom-prop"), Some(&"value".to_string())); + assert!(body.contains("Content starts here")); + } + + #[test] + fn test_parse_logseq_tags() { + let tags = parse_logseq_tags("[[rust]], [[programming]], #testing"); + assert_eq!(tags.len(), 3); + assert!(tags.contains(&"rust".to_string())); + assert!(tags.contains(&"programming".to_string())); + assert!(tags.contains(&"testing".to_string())); + } + + #[test] + fn test_parse_logseq_refs() { + let refs = parse_logseq_refs("[[ref1]], [[ref2]], [[ref3]]"); + assert_eq!(refs.len(), 3); + assert_eq!(refs[0], "ref1"); + assert_eq!(refs[1], "ref2"); + assert_eq!(refs[2], "ref3"); + } + + #[test] + fn test_parse_node_type() { + assert_eq!(parse_node_type("note"), Some(NodeType::Note)); + assert_eq!(parse_node_type("Decision"), Some(NodeType::Decision)); + assert_eq!(parse_node_type("GUIDELINE"), Some(NodeType::Guideline)); + assert_eq!(parse_node_type("invalid"), None); + } + + #[test] + fn test_import_missing_title() { + let content = "type:: note\n\n- Content"; + let result = import_from_logseq_page(content); + assert!(result.is_err()); + } +} diff --git a/crates/kogral-core/src/import/mod.rs b/crates/kogral-core/src/import/mod.rs new file mode 100644 index 0000000..529e6e6 --- /dev/null +++ b/crates/kogral-core/src/import/mod.rs @@ -0,0 +1,3 @@ +//! Import functionality + +pub mod logseq; diff --git a/crates/kogral-core/src/inheritance.rs b/crates/kogral-core/src/inheritance.rs new file mode 100644 index 0000000..8f855b1 --- /dev/null +++ b/crates/kogral-core/src/inheritance.rs @@ -0,0 +1,160 @@ +//! Guideline inheritance resolution +//! +//! Manages inheritance of guidelines from shared organizational standards +//! to project-local overrides, allowing teams to maintain base guidelines +//! while projects customize for their specific needs. + +use std::collections::HashMap; +use std::path::Path; + +use tracing::{debug, info}; +use uuid::Uuid; + +use crate::error::{KbError, Result}; +use crate::models::{Node, NodeType}; +use crate::parser::parse_frontmatter_map; + +/// Resolve guidelines with inheritance from shared base to local project +/// +/// Merges guidelines from a shared base directory with project-local +/// guidelines, allowing projects to override or extend organizational +/// standards. +/// +/// # Arguments +/// +/// * `base_path` - Path to shared guidelines directory +/// * `local_path` - Path to project-local guidelines directory +/// +/// # Returns +/// +/// Merged vector of guidelines with local overriding base +/// +/// # Inheritance Rules +/// +/// - Local guidelines with same ID override shared guidelines +/// - Shared guidelines without local override are included +/// - Priority: local > shared +/// +/// # Errors +/// +/// Returns error if directories cannot be read or guidelines are malformed +pub fn resolve_guidelines(base_path: &Path, local_path: &Path) -> Result> { + info!( + "Resolving guidelines: base={:?}, local={:?}", + base_path, local_path + ); + + let mut guidelines: HashMap = HashMap::new(); + + // Load shared guidelines first + if base_path.exists() { + debug!("Loading shared guidelines from {:?}", base_path); + load_guidelines_from_dir(base_path, &mut guidelines)?; + } + + // Load and override with local guidelines + if local_path.exists() { + debug!("Loading local guidelines from {:?}", local_path); + load_guidelines_from_dir(local_path, &mut guidelines)?; + } + + let result: Vec = guidelines.into_values().collect(); + debug!("Resolved {} guidelines total", result.len()); + Ok(result) +} + +/// Load guidelines from a directory and merge into the map +fn load_guidelines_from_dir(dir: &Path, guidelines: &mut HashMap) -> Result<()> { + if !dir.is_dir() { + return Err(KbError::Parser(format!( + "Guidelines directory not found: {}", + dir.display() + ))); + } + + // Read all markdown files in directory + let entries = std::fs::read_dir(dir) + .map_err(|e| KbError::Parser(format!("Failed to read directory: {e}")))?; + + for entry in entries { + let entry = entry.map_err(|e| KbError::Parser(format!("Failed to read entry: {e}")))?; + let path = entry.path(); + + if path.is_file() && path.extension().is_some_and(|ext| ext == "md") { + debug!("Loading guideline from: {:?}", path); + + match load_guideline(&path) { + Ok(node) => { + guidelines.insert(node.id.clone(), node); + } + Err(e) => { + // Log error but continue with other files + debug!("Failed to load guideline {path:?}: {e}"); + } + } + } + } + + Ok(()) +} + +/// Load a single guideline node from a markdown file +/// +/// # Errors +/// +/// Returns error if the file cannot be read or parsed +fn load_guideline(path: &Path) -> Result { + let content = std::fs::read_to_string(path) + .map_err(|e| KbError::Parser(format!("Failed to read file: {e}")))?; + + let frontmatter = parse_frontmatter_map(&content)?; + + // Extract node properties from frontmatter + let title = frontmatter.get("title").cloned().unwrap_or_else(|| { + path.file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string() + }); + + let id = frontmatter + .get("id") + .cloned() + .unwrap_or_else(|| Uuid::new_v4().to_string()); + + // Extract body from content (everything after frontmatter block) + let body = if let Some(second_delimiter) = content[3..].find("---") { + content[second_delimiter + 6..].trim().to_string() + } else { + String::new() + }; + + let mut node = Node::new(NodeType::Guideline, title); + node.id = id; + node.content = body; + + // Parse tags if present + if let Some(tags_str) = frontmatter.get("tags") { + node.tags = tags_str + .split(',') + .map(|t| t.trim().to_string()) + .filter(|t| !t.is_empty()) + .collect(); + } + + Ok(node) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resolve_guidelines_empty() { + let base = std::path::Path::new("/nonexistent/base"); + let local = std::path::Path::new("/nonexistent/local"); + + let result = resolve_guidelines(base, local).unwrap(); + assert_eq!(result.len(), 0); + } +} diff --git a/crates/kogral-core/src/lib.rs b/crates/kogral-core/src/lib.rs new file mode 100644 index 0000000..856ab9b --- /dev/null +++ b/crates/kogral-core/src/lib.rs @@ -0,0 +1,77 @@ +//! # kb-core: KOGRAL Core Library +//! +//! Core library for a Logseq-inspired KOGRAL system. +//! +//! ## Features +//! +//! - **Graph-based storage**: Nodes (notes, decisions, guidelines, patterns) +//! and edges (relationships) +//! - **Multiple backends**: Filesystem (git-friendly, Logseq-compatible), +//! `SurrealDB`, in-memory +//! - **Configuration-driven**: Nickel schemas → JSON → Rust structs +//! - **Semantic search**: Vector embeddings via rig-core (Claude, `OpenAI`, +//! Ollama) or fastembed +//! - **Template system**: Tera templates for document generation and export +//! - **Inheritance**: Guideline resolution from shared → local with overrides +//! - **Logseq compatibility**: Markdown with wikilinks, YAML frontmatter, +//! export/import +//! +//! ## Architecture +//! +//! ```text +//! ┌─────────────────────────────────────────────────────────────┐ +//! │ kb-core │ +//! ├─────────────────────────────────────────────────────────────┤ +//! │ Config (Nickel) → Models → Storage → Query → Export │ +//! │ │ +//! │ Backends: Filesystem | SurrealDB | Memory │ +//! │ Search: Text + Semantic (rig-core/fastembed) │ +//! │ Templates: Tera (generation + export) │ +//! └─────────────────────────────────────────────────────────────┘ +//! ``` +//! +//! ## Example +//! +//! ```no_run +//! use kogral_core::models::{Node, NodeType, Graph}; +//! +//! // Create a graph +//! let mut graph = Graph::new("my-project".to_string()); +//! +//! // Add a note +//! let mut note = Node::new(NodeType::Note, "Rust Best Practices".to_string()); +//! note.content = "Always use Result for error handling".to_string(); +//! note.add_tag("rust".to_string()); +//! graph.add_node(note).unwrap(); +//! +//! // Query +//! let rust_nodes = graph.nodes_by_tag("rust"); +//! assert_eq!(rust_nodes.len(), 1); +//! ``` + +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![warn(clippy::all)] +#![warn(clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] + +pub mod block_parser; +pub mod config; +pub mod embeddings; +pub mod error; +pub mod export; +pub mod import; +pub mod inheritance; +pub mod models; +pub mod parser; +pub mod query; +pub mod storage; +pub mod sync; + +mod regex_patterns; + +// Re-exports for convenience +pub use error::{KbError, Result}; +pub use models::{ + Block, BlockProperties, Edge, EdgeType, Graph, Node, NodeStatus, NodeType, TaskStatus, +}; diff --git a/crates/kogral-core/src/models.rs b/crates/kogral-core/src/models.rs new file mode 100644 index 0000000..35f020d --- /dev/null +++ b/crates/kogral-core/src/models.rs @@ -0,0 +1,1147 @@ +//! Core data models for KOGRAL. +//! +//! This module defines the fundamental structures: +//! - `Node`: Knowledge base entries (notes, decisions, guidelines, patterns, +//! etc.) +//! - `Edge`: Relationships between nodes +//! - `Graph`: Collection of nodes and edges with metadata + +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Type of KOGRAL node +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum NodeType { + /// General knowledge, observations + Note, + /// ADR-style architectural decisions + Decision, + /// Code standards, practices + Guideline, + /// Reusable solutions + Pattern, + /// Daily notes (auto-dated) + Journal, + /// Agent task records (from Vapora KG) + Execution, +} + +impl std::fmt::Display for NodeType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Note => write!(f, "note"), + Self::Decision => write!(f, "decision"), + Self::Guideline => write!(f, "guideline"), + Self::Pattern => write!(f, "pattern"), + Self::Journal => write!(f, "journal"), + Self::Execution => write!(f, "execution"), + } + } +} + +/// Status of a KOGRAL node +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum NodeStatus { + /// Work in progress + #[default] + Draft, + /// Current and active + Active, + /// Replaced by newer version + Superseded, + /// No longer relevant + Archived, +} + +impl std::fmt::Display for NodeStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Draft => write!(f, "draft"), + Self::Active => write!(f, "active"), + Self::Superseded => write!(f, "superseded"), + Self::Archived => write!(f, "archived"), + } + } +} + +/// Task status for Logseq TODO blocks +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum TaskStatus { + /// Task to be done + Todo, + /// Task in progress + Doing, + /// Task completed + Done, + /// Task scheduled for later + Later, + /// Task to be done now (high priority) + Now, + /// Task waiting on something + Waiting, + /// Task cancelled + Cancelled, +} + +impl std::fmt::Display for TaskStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Todo => write!(f, "TODO"), + Self::Doing => write!(f, "DOING"), + Self::Done => write!(f, "DONE"), + Self::Later => write!(f, "LATER"), + Self::Now => write!(f, "NOW"), + Self::Waiting => write!(f, "WAITING"), + Self::Cancelled => write!(f, "CANCELLED"), + } + } +} + +/// Properties associated with a content block +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct BlockProperties { + /// Tags attached to the block (e.g., #card, #important) + pub tags: Vec, + /// Task status if this is a TODO block + pub status: Option, + /// Custom key-value properties (e.g., `property::` value) + pub custom: HashMap, + /// Block references ((uuid)) + pub block_refs: Vec, + /// Page references [[page]] + pub page_refs: Vec, +} + +impl BlockProperties { + /// Create new empty block properties + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Check if the block has a specific tag + #[must_use] + pub fn has_tag(&self, tag: &str) -> bool { + self.tags.iter().any(|t| t == tag) + } + + /// Add a tag if not already present + pub fn add_tag(&mut self, tag: String) { + if !self.tags.contains(&tag) { + self.tags.push(tag); + } + } + + /// Check if the block has a specific custom property + #[must_use] + pub fn has_property(&self, key: &str) -> bool { + self.custom.contains_key(key) + } + + /// Get a custom property value + #[must_use] + pub fn get_property(&self, key: &str) -> Option<&String> { + self.custom.get(key) + } + + /// Set a custom property + pub fn set_property(&mut self, key: String, value: String) { + self.custom.insert(key, value); + } +} + +/// A content block (Logseq-compatible) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Block { + /// Unique identifier (UUID) + pub id: String, + /// Block content (text) + pub content: String, + /// Block properties (tags, status, custom metadata) + pub properties: BlockProperties, + /// Nested child blocks + pub children: Vec, + /// Creation timestamp + pub created: DateTime, + /// Last modification timestamp + pub modified: DateTime, + /// Parent block ID (if nested) + pub parent_id: Option, +} + +impl Block { + /// Create a new block with the given content + #[must_use] + pub fn new(content: String) -> Self { + let now = Utc::now(); + Self { + id: Uuid::new_v4().to_string(), + content, + properties: BlockProperties::default(), + children: Vec::new(), + created: now, + modified: now, + parent_id: None, + } + } + + /// Create a new block with UUID and content + #[must_use] + pub fn with_id(id: String, content: String) -> Self { + let now = Utc::now(); + Self { + id, + content, + properties: BlockProperties::default(), + children: Vec::new(), + created: now, + modified: now, + parent_id: None, + } + } + + /// Update the modified timestamp to now + pub fn touch(&mut self) { + self.modified = Utc::now(); + } + + /// Check if this is a task block (has TODO status) + #[must_use] + pub fn is_task(&self) -> bool { + self.properties.status.is_some() + } + + /// Check if this block has a specific tag + #[must_use] + pub fn has_tag(&self, tag: &str) -> bool { + self.properties.has_tag(tag) + } + + /// Add a child block + pub fn add_child(&mut self, mut child: Block) { + child.parent_id = Some(self.id.clone()); + self.children.push(child); + self.touch(); + } + + /// Get all blocks recursively (depth-first traversal) + #[must_use] + pub fn all_blocks(&self) -> Vec<&Block> { + let mut blocks = vec![self]; + for child in &self.children { + blocks.extend(child.all_blocks()); + } + blocks + } + + /// Find blocks by tag (recursively) + #[must_use] + pub fn find_by_tag(&self, tag: &str) -> Vec<&Block> { + self.all_blocks() + .into_iter() + .filter(|b| b.has_tag(tag)) + .collect() + } + + /// Find blocks by task status (recursively) + #[must_use] + pub fn find_by_status(&self, status: TaskStatus) -> Vec<&Block> { + self.all_blocks() + .into_iter() + .filter(|b| b.properties.status == Some(status)) + .collect() + } + + /// Find blocks with a specific custom property (recursively) + #[must_use] + pub fn find_by_property(&self, key: &str, value: &str) -> Vec<&Block> { + self.all_blocks() + .into_iter() + .filter(|b| b.properties.get_property(key) == Some(&value.to_string())) + .collect() + } +} + +/// Type of relationship between nodes +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum EdgeType { + /// General conceptual link + RelatesTo, + /// Must exist/be read first + DependsOn, + /// Concrete implementation of + Implements, + /// Inherits and overrides + Extends, + /// Replaces older version + Supersedes, + /// Documents/clarifies + Explains, +} + +impl std::fmt::Display for EdgeType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RelatesTo => write!(f, "relates_to"), + Self::DependsOn => write!(f, "depends_on"), + Self::Implements => write!(f, "implements"), + Self::Extends => write!(f, "extends"), + Self::Supersedes => write!(f, "supersedes"), + Self::Explains => write!(f, "explains"), + } + } +} + +/// A node in the KOGRAL graph +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Node { + /// Unique identifier + pub id: String, + /// Type of node + pub node_type: NodeType, + /// Human-readable title + pub title: String, + /// Creation timestamp + pub created: DateTime, + /// Last modification timestamp + pub modified: DateTime, + /// Content (markdown body) + pub content: String, + /// Tags for categorization + pub tags: Vec, + /// Current status + pub status: NodeStatus, + /// Related node IDs + pub relates_to: Vec, + /// Dependencies (node IDs this depends on) + pub depends_on: Vec, + /// Implementations (node IDs this implements) + pub implements: Vec, + /// Extensions (node IDs this extends) + pub extends: Vec, + /// Project identifier (for cross-project links) + pub project: Option, + /// Additional metadata (type-specific fields) + pub metadata: HashMap, + /// Vector embedding (for semantic search) + #[serde(skip_serializing_if = "Option::is_none")] + pub embedding: Option>, + /// Parsed content blocks (Logseq-compatible, optional) + /// This is a cached structure parsed from `content` markdown. + /// When None, blocks haven't been parsed yet (lazy parsing). + #[serde(skip_serializing_if = "Option::is_none")] + pub blocks: Option>, +} + +impl Node { + /// Create a new node with the given type and title + #[must_use] + pub fn new(node_type: NodeType, title: String) -> Self { + let now = Utc::now(); + Self { + id: Uuid::new_v4().to_string(), + node_type, + title, + created: now, + modified: now, + content: String::new(), + tags: Vec::new(), + status: NodeStatus::Draft, + relates_to: Vec::new(), + depends_on: Vec::new(), + implements: Vec::new(), + extends: Vec::new(), + project: None, + metadata: HashMap::new(), + embedding: None, + blocks: None, + } + } + + /// Update the modified timestamp to now + pub fn touch(&mut self) { + self.modified = Utc::now(); + } + + /// Add a tag if not already present + pub fn add_tag(&mut self, tag: String) { + if !self.tags.contains(&tag) { + self.tags.push(tag); + self.touch(); + } + } + + /// Add a relationship if not already present + pub fn add_relation(&mut self, edge_type: EdgeType, target_id: String) { + let list = match edge_type { + EdgeType::RelatesTo => &mut self.relates_to, + EdgeType::DependsOn => &mut self.depends_on, + EdgeType::Implements => &mut self.implements, + EdgeType::Extends => &mut self.extends, + EdgeType::Supersedes | EdgeType::Explains => { + // These are stored as separate edges, not in node fields + return; + } + }; + + if !list.contains(&target_id) { + list.push(target_id); + self.touch(); + } + } + + /// Get all related node IDs (union of all relationship types) + #[must_use] + pub fn all_relations(&self) -> Vec { + let mut relations = Vec::new(); + relations.extend(self.relates_to.clone()); + relations.extend(self.depends_on.clone()); + relations.extend(self.implements.clone()); + relations.extend(self.extends.clone()); + relations.sort(); + relations.dedup(); + relations + } + + /// Get blocks (lazy parsing from content if not already parsed) + /// + /// Parses `content` into structured blocks on first access, caches result. + /// Returns empty Vec if parsing fails. + /// + /// # Panics + /// + /// Does not panic - the internal `unwrap()` is safe because blocks are + /// initialized before being accessed. + pub fn get_blocks(&mut self) -> Vec<&Block> { + use crate::block_parser::BlockParser; + + if self.blocks.is_none() { + // Parse content into blocks (lazy) + if let Ok(parsed) = BlockParser::parse(&self.content) { + self.blocks = Some(parsed); + } else { + self.blocks = Some(Vec::new()); + } + } + + self.blocks.as_ref().unwrap().iter().collect() + } + + /// Find blocks with a specific tag (parses if needed) + pub fn find_blocks_by_tag(&mut self, tag: &str) -> Vec<&Block> { + self.get_blocks(); // Ensure blocks are parsed + + if let Some(blocks) = &self.blocks { + blocks.iter().flat_map(|b| b.find_by_tag(tag)).collect() + } else { + Vec::new() + } + } + + /// Find all TODO blocks (any task status) + pub fn find_all_todos(&mut self) -> Vec<&Block> { + self.get_blocks(); // Ensure blocks are parsed + + if let Some(blocks) = &self.blocks { + blocks + .iter() + .flat_map(|b| b.all_blocks()) + .filter(|b| b.is_task()) + .collect() + } else { + Vec::new() + } + } + + /// Find blocks with a specific task status + pub fn find_blocks_by_status(&mut self, status: TaskStatus) -> Vec<&Block> { + self.get_blocks(); // Ensure blocks are parsed + + if let Some(blocks) = &self.blocks { + blocks + .iter() + .flat_map(|b| b.find_by_status(status)) + .collect() + } else { + Vec::new() + } + } + + /// Find blocks with a specific custom property + pub fn find_blocks_by_property(&mut self, key: &str, value: &str) -> Vec<&Block> { + self.get_blocks(); // Ensure blocks are parsed + + if let Some(blocks) = &self.blocks { + blocks + .iter() + .flat_map(|b| b.find_by_property(key, value)) + .collect() + } else { + Vec::new() + } + } + + /// Force re-parsing of blocks from content + /// + /// Useful when content has been modified and blocks need to be updated + pub fn reparse_blocks(&mut self) { + use crate::block_parser::BlockParser; + + if let Ok(parsed) = BlockParser::parse(&self.content) { + self.blocks = Some(parsed); + } else { + self.blocks = Some(Vec::new()); + } + } +} + +/// A directed edge between two nodes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Edge { + /// Source node ID + pub from: String, + /// Target node ID + pub to: String, + /// Relationship type + pub edge_type: EdgeType, + /// Relationship strength (0.0 to 1.0) + #[serde(default = "default_edge_strength")] + pub strength: f32, + /// Creation timestamp + pub created: DateTime, + /// Additional metadata + #[serde(default)] + pub metadata: HashMap, +} + +fn default_edge_strength() -> f32 { + 1.0 +} + +impl Edge { + /// Create a new edge between nodes + #[must_use] + pub fn new(from: String, to: String, edge_type: EdgeType) -> Self { + Self { + from, + to, + edge_type, + strength: 1.0, + created: Utc::now(), + metadata: HashMap::new(), + } + } + + /// Create an edge with a specific strength + #[must_use] + pub fn with_strength(from: String, to: String, edge_type: EdgeType, strength: f32) -> Self { + Self { + from, + to, + edge_type, + strength: strength.clamp(0.0, 1.0), + created: Utc::now(), + metadata: HashMap::new(), + } + } + + /// Get the reverse edge type (if applicable) + #[must_use] + pub fn reverse_type(&self) -> Option { + match self.edge_type { + EdgeType::DependsOn => Some(EdgeType::DependsOn), // Symmetric in reverse + EdgeType::Extends => Some(EdgeType::Extends), + EdgeType::Supersedes => Some(EdgeType::Supersedes), + _ => None, + } + } +} + +/// A KOGRAL graph +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Graph { + /// Graph name/identifier + pub name: String, + /// Graph version + pub version: String, + /// Human-readable description + pub description: String, + /// Nodes indexed by ID + pub nodes: HashMap, + /// Edges (stored separately from node relationships for flexibility) + pub edges: Vec, + /// Graph creation timestamp + pub created: DateTime, + /// Last modification timestamp + pub modified: DateTime, + /// Graph metadata + pub metadata: HashMap, +} + +impl Graph { + /// Create a new empty graph + #[must_use] + pub fn new(name: String) -> Self { + let now = Utc::now(); + Self { + name, + version: "1.0.0".to_string(), + description: String::new(), + nodes: HashMap::new(), + edges: Vec::new(), + created: now, + modified: now, + metadata: HashMap::new(), + } + } + + /// Add a node to the graph + /// + /// # Errors + /// + /// Returns an error if a node with the same ID already exists in the graph + pub fn add_node(&mut self, node: Node) -> Result<(), String> { + if self.nodes.contains_key(&node.id) { + return Err(format!("Node with ID {} already exists", node.id)); + } + self.nodes.insert(node.id.clone(), node); + self.modified = Utc::now(); + Ok(()) + } + + /// Get a node by ID + #[must_use] + pub fn get_node(&self, id: &str) -> Option<&Node> { + self.nodes.get(id) + } + + /// Get a mutable reference to a node + pub fn get_node_mut(&mut self, id: &str) -> Option<&mut Node> { + if self.nodes.contains_key(id) { + self.modified = Utc::now(); + } + self.nodes.get_mut(id) + } + + /// Remove a node and all its edges + pub fn remove_node(&mut self, id: &str) -> Option { + self.edges.retain(|e| e.from != id && e.to != id); + self.modified = Utc::now(); + self.nodes.remove(id) + } + + /// Add an edge between nodes + /// + /// # Errors + /// + /// Returns an error if source or target node does not exist in the graph + pub fn add_edge(&mut self, edge: Edge) -> Result<(), String> { + if !self.nodes.contains_key(&edge.from) { + return Err(format!("Source node {} not found", edge.from)); + } + if !self.nodes.contains_key(&edge.to) { + return Err(format!("Target node {} not found", edge.to)); + } + self.edges.push(edge); + self.modified = Utc::now(); + Ok(()) + } + + /// Get all edges from a specific node + #[must_use] + pub fn edges_from(&self, node_id: &str) -> Vec<&Edge> { + self.edges.iter().filter(|e| e.from == node_id).collect() + } + + /// Get all edges to a specific node + #[must_use] + pub fn edges_to(&self, node_id: &str) -> Vec<&Edge> { + self.edges.iter().filter(|e| e.to == node_id).collect() + } + + /// Get all edges involving a specific node (in or out) + #[must_use] + pub fn edges_involving(&self, node_id: &str) -> Vec<&Edge> { + self.edges + .iter() + .filter(|e| e.from == node_id || e.to == node_id) + .collect() + } + + /// Get nodes by type + #[must_use] + pub fn nodes_by_type(&self, node_type: NodeType) -> Vec<&Node> { + self.nodes + .values() + .filter(|n| n.node_type == node_type) + .collect() + } + + /// Get nodes by tag + #[must_use] + pub fn nodes_by_tag(&self, tag: &str) -> Vec<&Node> { + self.nodes + .values() + .filter(|n| n.tags.contains(&tag.to_string())) + .collect() + } + + /// Get nodes by status + #[must_use] + pub fn nodes_by_status(&self, status: NodeStatus) -> Vec<&Node> { + self.nodes.values().filter(|n| n.status == status).collect() + } + + /// Get total node count + #[must_use] + pub fn node_count(&self) -> usize { + self.nodes.len() + } + + /// Get total edge count + #[must_use] + pub fn edge_count(&self) -> usize { + self.edges.len() + } + + /// Find blocks with a specific tag across all nodes + /// + /// Returns tuples of (`node_id`, blocks) for each node that has matching + /// blocks + pub fn find_blocks_by_tag(&mut self, tag: &str) -> Vec<(String, Vec<&Block>)> { + let mut results = Vec::new(); + + for (node_id, node) in &mut self.nodes { + let blocks = node.find_blocks_by_tag(tag); + if !blocks.is_empty() { + results.push((node_id.clone(), blocks)); + } + } + + results + } + + /// Find all TODO blocks across all nodes + /// + /// Returns tuples of (`node_id`, `todo_blocks`) + pub fn find_all_todos(&mut self) -> Vec<(String, Vec<&Block>)> { + let mut results = Vec::new(); + + for (node_id, node) in &mut self.nodes { + let todos = node.find_all_todos(); + if !todos.is_empty() { + results.push((node_id.clone(), todos)); + } + } + + results + } + + /// Find blocks with a specific task status across all nodes + pub fn find_blocks_by_status(&mut self, status: TaskStatus) -> Vec<(String, Vec<&Block>)> { + let mut results = Vec::new(); + + for (node_id, node) in &mut self.nodes { + let blocks = node.find_blocks_by_status(status); + if !blocks.is_empty() { + results.push((node_id.clone(), blocks)); + } + } + + results + } + + /// Find blocks with a specific custom property across all nodes + pub fn find_blocks_by_property( + &mut self, + key: &str, + value: &str, + ) -> Vec<(String, Vec<&Block>)> { + let mut results = Vec::new(); + + for (node_id, node) in &mut self.nodes { + let blocks = node.find_blocks_by_property(key, value); + if !blocks.is_empty() { + results.push((node_id.clone(), blocks)); + } + } + + results + } +} + +impl Default for Graph { + fn default() -> Self { + Self::new("default".to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_node_creation() { + let node = Node::new(NodeType::Note, "Test Note".to_string()); + assert_eq!(node.node_type, NodeType::Note); + assert_eq!(node.title, "Test Note"); + assert_eq!(node.status, NodeStatus::Draft); + assert!(node.tags.is_empty()); + assert!(node.content.is_empty()); + } + + #[test] + fn test_node_add_tag() { + let mut node = Node::new(NodeType::Note, "Test".to_string()); + node.add_tag("rust".to_string()); + assert_eq!(node.tags, vec!["rust"]); + + // Adding same tag again should not duplicate + node.add_tag("rust".to_string()); + assert_eq!(node.tags, vec!["rust"]); + } + + #[test] + fn test_node_add_relation() { + let mut node = Node::new(NodeType::Note, "Test".to_string()); + let target_id = "target-123".to_string(); + + node.add_relation(EdgeType::RelatesTo, target_id.clone()); + assert_eq!(node.relates_to, vec![target_id.clone()]); + + // Adding same relation again should not duplicate + node.add_relation(EdgeType::RelatesTo, target_id.clone()); + assert_eq!(node.relates_to, vec![target_id]); + } + + #[test] + fn test_node_all_relations() { + let mut node = Node::new(NodeType::Note, "Test".to_string()); + node.relates_to = vec!["a".to_string(), "b".to_string()]; + node.depends_on = vec!["c".to_string()]; + node.implements = vec!["d".to_string()]; + + let all = node.all_relations(); + assert_eq!(all.len(), 4); + assert!(all.contains(&"a".to_string())); + assert!(all.contains(&"b".to_string())); + assert!(all.contains(&"c".to_string())); + assert!(all.contains(&"d".to_string())); + } + + #[test] + fn test_edge_creation() { + let edge = Edge::new( + "node1".to_string(), + "node2".to_string(), + EdgeType::DependsOn, + ); + assert_eq!(edge.from, "node1"); + assert_eq!(edge.to, "node2"); + assert_eq!(edge.edge_type, EdgeType::DependsOn); + assert_eq!(edge.strength, 1.0); + } + + #[test] + fn test_edge_with_strength() { + let edge = Edge::with_strength( + "node1".to_string(), + "node2".to_string(), + EdgeType::RelatesTo, + 0.5, + ); + assert_eq!(edge.strength, 0.5); + + // Test clamping + let edge2 = Edge::with_strength( + "node1".to_string(), + "node2".to_string(), + EdgeType::RelatesTo, + 1.5, + ); + assert_eq!(edge2.strength, 1.0); + + let edge3 = Edge::with_strength( + "node1".to_string(), + "node2".to_string(), + EdgeType::RelatesTo, + -0.5, + ); + assert_eq!(edge3.strength, 0.0); + } + + #[test] + fn test_graph_creation() { + let graph = Graph::new("test-graph".to_string()); + assert_eq!(graph.name, "test-graph"); + assert_eq!(graph.version, "1.0.0"); + assert_eq!(graph.node_count(), 0); + assert_eq!(graph.edge_count(), 0); + } + + #[test] + fn test_graph_add_node() { + let mut graph = Graph::new("test".to_string()); + let node = Node::new(NodeType::Note, "Test Note".to_string()); + let node_id = node.id.clone(); + + assert!(graph.add_node(node).is_ok()); + assert_eq!(graph.node_count(), 1); + assert!(graph.get_node(&node_id).is_some()); + + // Adding same node again should fail + let duplicate = Node { + id: node_id, + ..Node::new(NodeType::Note, "Duplicate".to_string()) + }; + assert!(graph.add_node(duplicate).is_err()); + } + + #[test] + fn test_graph_add_edge() { + let mut graph = Graph::new("test".to_string()); + let node1 = Node::new(NodeType::Note, "Node 1".to_string()); + let node2 = Node::new(NodeType::Note, "Node 2".to_string()); + let id1 = node1.id.clone(); + let id2 = node2.id.clone(); + + graph.add_node(node1).unwrap(); + graph.add_node(node2).unwrap(); + + let edge = Edge::new(id1.clone(), id2.clone(), EdgeType::RelatesTo); + assert!(graph.add_edge(edge).is_ok()); + assert_eq!(graph.edge_count(), 1); + + // Adding edge with non-existent node should fail + let bad_edge = Edge::new("nonexistent".to_string(), id2, EdgeType::RelatesTo); + assert!(graph.add_edge(bad_edge).is_err()); + } + + #[test] + fn test_graph_remove_node() { + let mut graph = Graph::new("test".to_string()); + let node = Node::new(NodeType::Note, "Test".to_string()); + let id = node.id.clone(); + + graph.add_node(node).unwrap(); + assert_eq!(graph.node_count(), 1); + + let removed = graph.remove_node(&id); + assert!(removed.is_some()); + assert_eq!(graph.node_count(), 0); + } + + #[test] + fn test_graph_edges_from_to() { + let mut graph = Graph::new("test".to_string()); + let node1 = Node::new(NodeType::Note, "Node 1".to_string()); + let node2 = Node::new(NodeType::Note, "Node 2".to_string()); + let node3 = Node::new(NodeType::Note, "Node 3".to_string()); + let id1 = node1.id.clone(); + let id2 = node2.id.clone(); + let id3 = node3.id.clone(); + + graph.add_node(node1).unwrap(); + graph.add_node(node2).unwrap(); + graph.add_node(node3).unwrap(); + + graph + .add_edge(Edge::new(id1.clone(), id2.clone(), EdgeType::RelatesTo)) + .unwrap(); + graph + .add_edge(Edge::new(id1.clone(), id3.clone(), EdgeType::DependsOn)) + .unwrap(); + graph + .add_edge(Edge::new(id2.clone(), id3.clone(), EdgeType::Implements)) + .unwrap(); + + let from_id1 = graph.edges_from(&id1); + assert_eq!(from_id1.len(), 2); + + let to_id3 = graph.edges_to(&id3); + assert_eq!(to_id3.len(), 2); + + let involving_id2 = graph.edges_involving(&id2); + assert_eq!(involving_id2.len(), 2); + } + + #[test] + fn test_graph_nodes_by_type() { + let mut graph = Graph::new("test".to_string()); + graph + .add_node(Node::new(NodeType::Note, "Note 1".to_string())) + .unwrap(); + graph + .add_node(Node::new(NodeType::Note, "Note 2".to_string())) + .unwrap(); + graph + .add_node(Node::new(NodeType::Decision, "Decision 1".to_string())) + .unwrap(); + + let notes = graph.nodes_by_type(NodeType::Note); + assert_eq!(notes.len(), 2); + + let decisions = graph.nodes_by_type(NodeType::Decision); + assert_eq!(decisions.len(), 1); + } + + #[test] + fn test_graph_nodes_by_tag() { + let mut graph = Graph::new("test".to_string()); + let mut node1 = Node::new(NodeType::Note, "Node 1".to_string()); + node1.add_tag("rust".to_string()); + let mut node2 = Node::new(NodeType::Note, "Node 2".to_string()); + node2.add_tag("rust".to_string()); + node2.add_tag("testing".to_string()); + let node3 = Node::new(NodeType::Note, "Node 3".to_string()); + + graph.add_node(node1).unwrap(); + graph.add_node(node2).unwrap(); + graph.add_node(node3).unwrap(); + + let rust_nodes = graph.nodes_by_tag("rust"); + assert_eq!(rust_nodes.len(), 2); + + let testing_nodes = graph.nodes_by_tag("testing"); + assert_eq!(testing_nodes.len(), 1); + } + + #[test] + fn test_graph_nodes_by_status() { + let mut graph = Graph::new("test".to_string()); + let mut node1 = Node::new(NodeType::Note, "Node 1".to_string()); + node1.status = NodeStatus::Active; + let node2 = Node::new(NodeType::Note, "Node 2".to_string()); // Draft by default + + graph.add_node(node1).unwrap(); + graph.add_node(node2).unwrap(); + + let active = graph.nodes_by_status(NodeStatus::Active); + assert_eq!(active.len(), 1); + + let draft = graph.nodes_by_status(NodeStatus::Draft); + assert_eq!(draft.len(), 1); + } + + #[test] + fn test_node_type_display() { + assert_eq!(NodeType::Note.to_string(), "note"); + assert_eq!(NodeType::Decision.to_string(), "decision"); + assert_eq!(NodeType::Guideline.to_string(), "guideline"); + } + + #[test] + fn test_node_status_display() { + assert_eq!(NodeStatus::Draft.to_string(), "draft"); + assert_eq!(NodeStatus::Active.to_string(), "active"); + assert_eq!(NodeStatus::Superseded.to_string(), "superseded"); + assert_eq!(NodeStatus::Archived.to_string(), "archived"); + } + + #[test] + fn test_edge_type_display() { + assert_eq!(EdgeType::RelatesTo.to_string(), "relates_to"); + assert_eq!(EdgeType::DependsOn.to_string(), "depends_on"); + assert_eq!(EdgeType::Implements.to_string(), "implements"); + } + + #[test] + fn test_node_get_blocks() { + let mut node = Node::new(NodeType::Note, "Test".to_string()); + node.content = r#"- First block +- TODO Second block #tag + - Nested block"# + .to_string(); + + let blocks = node.get_blocks(); + assert_eq!(blocks.len(), 2); + assert_eq!(blocks[0].content, "First block"); + assert_eq!(blocks[1].properties.status, Some(TaskStatus::Todo)); + } + + #[test] + fn test_node_find_blocks_by_tag() { + let mut node = Node::new(NodeType::Note, "Test".to_string()); + node.content = r#"- Block 1 #important +- Block 2 +- Block 3 #important #urgent"# + .to_string(); + + let blocks = node.find_blocks_by_tag("important"); + assert_eq!(blocks.len(), 2); + } + + #[test] + fn test_node_find_all_todos() { + let mut node = Node::new(NodeType::Note, "Test".to_string()); + node.content = r#"- Regular block +- TODO Task 1 +- DONE Task 2 +- TODO Task 3"# + .to_string(); + + let todos = node.find_all_todos(); + assert_eq!(todos.len(), 3); + } + + #[test] + fn test_node_find_blocks_by_status() { + let mut node = Node::new(NodeType::Note, "Test".to_string()); + node.content = r#"- TODO Task 1 +- DONE Task 2 +- TODO Task 3"# + .to_string(); + + let todo_blocks = node.find_blocks_by_status(TaskStatus::Todo); + assert_eq!(todo_blocks.len(), 2); + + let done_blocks = node.find_blocks_by_status(TaskStatus::Done); + assert_eq!(done_blocks.len(), 1); + } + + #[test] + fn test_graph_find_blocks_by_tag() { + let mut graph = Graph::new("test".to_string()); + + let mut node1 = Node::new(NodeType::Note, "Node 1".to_string()); + node1.content = "- Block #card\n- Block #important".to_string(); + let id1 = node1.id.clone(); + + let mut node2 = Node::new(NodeType::Note, "Node 2".to_string()); + node2.content = "- Another #card".to_string(); + let id2 = node2.id.clone(); + + graph.add_node(node1).unwrap(); + graph.add_node(node2).unwrap(); + + let results = graph.find_blocks_by_tag("card"); + assert_eq!(results.len(), 2); + assert!(results.iter().any(|(id, _)| id == &id1)); + assert!(results.iter().any(|(id, _)| id == &id2)); + } + + #[test] + fn test_graph_find_all_todos() { + let mut graph = Graph::new("test".to_string()); + + let mut node1 = Node::new(NodeType::Note, "Node 1".to_string()); + node1.content = "- TODO Task in node 1".to_string(); + + let mut node2 = Node::new(NodeType::Note, "Node 2".to_string()); + node2.content = "- TODO Task in node 2\n- DONE Completed".to_string(); + + graph.add_node(node1).unwrap(); + graph.add_node(node2).unwrap(); + + let todos = graph.find_all_todos(); + assert_eq!(todos.len(), 2); + } +} diff --git a/crates/kogral-core/src/parser.rs b/crates/kogral-core/src/parser.rs new file mode 100644 index 0000000..2a64dad --- /dev/null +++ b/crates/kogral-core/src/parser.rs @@ -0,0 +1,248 @@ +//! Markdown parser with Logseq compatibility +//! +//! This module parses KOGRAL documents: +//! - YAML frontmatter extraction +//! - Markdown body parsing +//! - Wikilink resolution ([[links]]) +//! - Code reference parsing (@path/to/file.rs:42) + +use std::collections::HashMap; +use std::fmt::Write; + +use chrono::{DateTime, Utc}; +use serde_yaml; + +use crate::error::{KbError, Result}; +use crate::models::{Node, NodeStatus, NodeType}; +use crate::regex_patterns::{WIKILINK_PATTERN, CODE_REF_PATTERN}; + +/// Extract frontmatter as a map of key-value strings +/// +/// Parses YAML frontmatter and returns it as a `HashMap`. +/// This is useful for guideline inheritance and other metadata extraction. +/// +/// # Errors +/// +/// Returns an error if YAML frontmatter is malformed +pub fn parse_frontmatter_map(content: &str) -> Result> { + let (frontmatter_str, _body) = extract_frontmatter(content)?; + + let metadata = serde_yaml::from_str::(&frontmatter_str) + .map_err(|e| KbError::Parser(format!("Invalid YAML: {e}")))?; + + let mut result = HashMap::new(); + + if let serde_yaml::Value::Mapping(map) = metadata { + for (key, value) in map { + if let (serde_yaml::Value::String(k), serde_yaml::Value::String(v)) = (key, value) { + result.insert(k, v); + } + } + } + + Ok(result) +} + +/// Parse a markdown document with YAML frontmatter +/// +/// Format: +/// ```markdown +/// --- +/// id: node-123 +/// type: note +/// title: Example +/// --- +/// +/// # Content here +/// ``` +/// +/// # Errors +/// +/// Returns an error if: +/// - Document is missing frontmatter delimiters (`---`) +/// - YAML frontmatter is malformed or contains invalid syntax +/// - Required fields (`id`, `type`, `title`) are missing or invalid +pub fn parse_document(content: &str) -> Result { + let (frontmatter, body) = extract_frontmatter(content)?; + let metadata = parse_frontmatter(&frontmatter)?; + + // Build node from metadata + let mut node = Node::new(metadata.node_type, metadata.title.clone()); + + node.id = metadata.id; + node.created = metadata.created; + node.modified = metadata.modified; + node.status = metadata.status; + node.tags = metadata.tags; + node.content.clone_from(&body); + node.relates_to = metadata.relates_to; + node.depends_on = metadata.depends_on; + node.implements = metadata.implements; + node.extends = metadata.extends; + node.project = metadata.project; + node.metadata = metadata.extra; + + Ok(node) +} + +/// Frontmatter metadata +#[derive(Debug, serde::Deserialize)] +struct FrontmatterMetadata { + id: String, + #[serde(rename = "type")] + node_type: NodeType, + title: String, + created: DateTime, + modified: DateTime, + #[serde(default)] + tags: Vec, + #[serde(default)] + status: NodeStatus, + #[serde(default)] + relates_to: Vec, + #[serde(default)] + depends_on: Vec, + #[serde(default)] + implements: Vec, + #[serde(default)] + extends: Vec, + #[serde(default)] + project: Option, + #[serde(flatten)] + extra: HashMap, +} + +/// Extract YAML frontmatter from markdown content +fn extract_frontmatter(content: &str) -> Result<(String, String)> { + let lines: Vec<&str> = content.lines().collect(); + + if lines.is_empty() || !lines[0].trim().starts_with("---") { + return Err(KbError::Frontmatter("No frontmatter found".to_string())); + } + + // Find end of frontmatter + let end = lines[1..] + .iter() + .position(|line| line.trim() == "---") + .ok_or_else(|| KbError::Frontmatter("Unterminated frontmatter".to_string()))?; + + let frontmatter = lines[1..=end].join("\n"); + let body = lines[end + 2..].join("\n"); + + Ok((frontmatter, body)) +} + +/// Parse YAML frontmatter +fn parse_frontmatter(yaml: &str) -> Result { + serde_yaml::from_str(yaml) + .map_err(|e| KbError::Frontmatter(format!("Invalid frontmatter YAML: {e}"))) +} + +/// Extract wikilinks from markdown content +/// +/// Matches: [[link]], [[link|display text]] +/// +/// # Panics +/// +/// Panics if the hardcoded regex pattern is invalid (should never happen). +#[must_use] +pub fn extract_wikilinks(content: &str) -> Vec { + WIKILINK_PATTERN + .captures_iter(content) + .map(|cap| cap[1].to_string()) + .collect() +} + +/// Extract code references from markdown +/// +/// Matches: @path/to/file.rs:42 +#[must_use] +pub fn extract_code_refs(content: &str) -> Vec { + CODE_REF_PATTERN + .captures_iter(content) + .map(|cap| cap[1].to_string()) + .collect() +} + +/// Generate markdown from a node +#[must_use] +pub fn generate_markdown(node: &Node) -> String { + let mut output = String::new(); + + // Frontmatter + output.push_str("---\n"); + let _ = writeln!(output, "id: {}", node.id); + let _ = writeln!(output, "type: {}", node.node_type); + let _ = writeln!(output, "title: {}", node.title); + let _ = writeln!(output, "created: {}", node.created.to_rfc3339()); + let _ = writeln!(output, "modified: {}", node.modified.to_rfc3339()); + + if !node.tags.is_empty() { + let tags_str = node + .tags + .iter() + .map(|t| format!("\"{t}\"")) + .collect::>() + .join(", "); + let _ = writeln!(output, "tags: [{tags_str}]"); + } + + let _ = writeln!(output, "status: {}", node.status); + + if !node.relates_to.is_empty() { + output.push_str("relates_to:\n"); + for rel in &node.relates_to { + let _ = writeln!(output, " - {rel}"); + } + } + + if !node.depends_on.is_empty() { + output.push_str("depends_on:\n"); + for dep in &node.depends_on { + let _ = writeln!(output, " - {dep}"); + } + } + + if let Some(ref project) = node.project { + let _ = writeln!(output, "project: {project}"); + } + + output.push_str("---\n\n"); + + // Body + output.push_str(&node.content); + + output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_frontmatter() { + let content = r#"--- +id: test-123 +type: note +--- +Content here"#; + + let (fm, body) = extract_frontmatter(content).unwrap(); + assert!(fm.contains("id: test-123")); + assert_eq!(body.trim(), "Content here"); + } + + #[test] + fn test_extract_wikilinks() { + let content = "This is [[link1]] and [[link2|Display Text]]."; + let links = extract_wikilinks(content); + assert_eq!(links, vec!["link1", "link2"]); + } + + #[test] + fn test_extract_code_refs() { + let content = "See @src/main.rs:42 and @lib/util.rs:100"; + let refs = extract_code_refs(content); + assert_eq!(refs, vec!["src/main.rs:42", "lib/util.rs:100"]); + } +} diff --git a/crates/kogral-core/src/query.rs b/crates/kogral-core/src/query.rs new file mode 100644 index 0000000..ca435ca --- /dev/null +++ b/crates/kogral-core/src/query.rs @@ -0,0 +1,173 @@ +//! Query engine for text and semantic search + +use regex::Regex; + +use crate::error::Result; +use crate::models::{Graph, Node}; + +/// Query engine for searching nodes in a graph +/// +/// Provides text and semantic search capabilities across nodes. +/// Supports: +/// - Full-text search (title, content, tags) +/// - Tag-based filtering +/// - Type-based filtering +pub struct QueryEngine { + graph: Graph, +} + +impl QueryEngine { + /// Create a new query engine for the given graph + #[must_use] + pub fn new(graph: Graph) -> Self { + Self { graph } + } + + /// Search for nodes matching the query + /// + /// Performs full-text search across: + /// - Node titles (case-insensitive) + /// - Node content (case-insensitive) + /// - Node tags (exact match) + /// + /// # Arguments + /// + /// * `query` - Search query string + /// + /// # Returns + /// + /// Vector of nodes matching the query, sorted by relevance + /// + /// # Errors + /// + /// This function does not fail; it always returns Ok with results. + pub fn search(&self, query: &str) -> Result> { + if query.is_empty() { + return Ok(Vec::new()); + } + + let query_lower = query.to_lowercase(); + let mut results = Vec::new(); + + for node in self.graph.nodes.values() { + let mut score = 0; + + // Title match (highest score) + if node.title.to_lowercase().contains(&query_lower) { + score += 10; + } + + // Tag match (high score) + if node + .tags + .iter() + .any(|tag| tag.to_lowercase().contains(&query_lower)) + { + score += 5; + } + + // Content match (medium score) + if node.content.to_lowercase().contains(&query_lower) { + score += 2; + } + + if score > 0 { + results.push((score, node.clone())); + } + } + + // Sort by score (descending) + results.sort_by(|a, b| b.0.cmp(&a.0)); + + Ok(results.into_iter().map(|(_, node)| node).collect()) + } + + /// Search nodes by tag + /// + /// Returns all nodes that have the specified tag. + /// + /// # Errors + /// + /// This function does not fail; it always returns Ok with results. + pub fn search_by_tag(&self, tag: &str) -> Result> { + Ok(self + .graph + .nodes + .values() + .filter(|node| node.tags.iter().any(|t| t.eq_ignore_ascii_case(tag))) + .cloned() + .collect()) + } + + /// Search nodes by regex pattern + /// + /// Searches content and title using regex. + /// + /// # Errors + /// + /// Returns error if regex pattern is invalid. + pub fn search_regex(&self, pattern: &str) -> Result> { + let re = Regex::new(pattern) + .map_err(|e| crate::error::KbError::Query(format!("Invalid regex pattern: {e}")))?; + + Ok(self + .graph + .nodes + .values() + .filter(|node| re.is_match(&node.title) || re.is_match(&node.content)) + .cloned() + .collect()) + } + + /// Search nodes by type + /// + /// Returns all nodes of the specified type. + /// + /// # Errors + /// + /// This function does not fail; it always returns Ok with results. + pub fn search_by_type(&self, node_type: crate::models::NodeType) -> Result> { + Ok(self + .graph + .nodes + .values() + .filter(|node| node.node_type == node_type) + .cloned() + .collect()) + } + + /// Get all nodes related to the given node + /// + /// Returns nodes that have incoming or outgoing edges. + /// + /// # Errors + /// + /// This function does not fail; it always returns Ok with results. + pub fn get_related_nodes(&self, node_id: &str) -> Result> { + let mut related_ids = std::collections::HashSet::new(); + + // Add nodes that this node relates to + if let Some(node) = self.graph.nodes.get(node_id) { + related_ids.extend(node.relates_to.iter().cloned()); + related_ids.extend(node.depends_on.iter().cloned()); + related_ids.extend(node.implements.iter().cloned()); + related_ids.extend(node.extends.iter().cloned()); + } + + // Add nodes that relate to this node + for node in self.graph.nodes.values() { + if node.relates_to.contains(&node_id.to_string()) + || node.depends_on.contains(&node_id.to_string()) + || node.implements.contains(&node_id.to_string()) + || node.extends.contains(&node_id.to_string()) + { + related_ids.insert(node.id.clone()); + } + } + + Ok(related_ids + .into_iter() + .filter_map(|id| self.graph.nodes.get(&id).cloned()) + .collect()) + } +} diff --git a/crates/kogral-core/src/regex_patterns.rs b/crates/kogral-core/src/regex_patterns.rs new file mode 100644 index 0000000..60c8b21 --- /dev/null +++ b/crates/kogral-core/src/regex_patterns.rs @@ -0,0 +1,97 @@ +//! Lazy-compiled regex patterns for the KOGRAL knowledge base engine +//! +//! All regex patterns are compiled once at startup using `once_cell::sync::Lazy` +//! to avoid recompilation overhead during normal operation. + +use once_cell::sync::Lazy; +use regex::Regex; + +/// Wikilink pattern: `[[target]]` or `[[target|display]]` +pub static WIKILINK_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]").unwrap()); + +/// Code reference pattern: `@path/to/file.rs:42` +pub static CODE_REF_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"@([\w/.-]+:\d+)").unwrap()); + +/// Tag pattern: `#tagname` +pub static TAG_PATTERN: Lazy = Lazy::new(|| Regex::new(r"#(\w+)").unwrap()); + +/// Property pattern at start of line: `property:: value` +pub static PROPERTY_START_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"^(\w+)::\s*(\S+)$").unwrap()); + +/// Property pattern inline: `property:: value` anywhere in text +pub static PROPERTY_INLINE_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"(\w+)::\s*(\S+)").unwrap()); + +/// UUID pattern in double parentheses: `((uuid))` +pub static UUID_REF_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"\(\(([a-f0-9-]{36})\)\)").unwrap()); + +/// Logseq property pattern with dashes: `property-name:: value` +pub static LOGSEQ_PROPERTY_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"^(\w[\w-]*)::\s*(.+)$").unwrap()); + +/// Logseq wikilink pattern +pub static LOGSEQ_WIKILINK_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"\[\[([^\]]+)\]\]").unwrap()); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_wikilink_pattern() { + let re = &WIKILINK_PATTERN; + assert!(re.is_match("[[target]]")); + assert!(re.is_match("[[target|display]]")); + assert!(!re.is_match("[[incomplete")); + } + + #[test] + fn test_code_ref_pattern() { + let re = &CODE_REF_PATTERN; + assert!(re.is_match("@src/main.rs:42")); + assert!(re.is_match("@crates/core/lib.rs:100")); + assert!(!re.is_match("@no-colon.rs")); + } + + #[test] + fn test_tag_pattern() { + let re = &TAG_PATTERN; + assert!(re.is_match("#rust")); + assert!(re.is_match("#api-design")); + assert!(!re.is_match("#")); + } + + #[test] + fn test_property_start_pattern() { + let re = &PROPERTY_START_PATTERN; + assert!(re.is_match("author:: john")); + assert!(re.is_match("status:: active")); + assert!(!re.is_match("inline author:: john")); + } + + #[test] + fn test_property_inline_pattern() { + let re = &PROPERTY_INLINE_PATTERN; + assert!(re.is_match("author:: John")); + assert!(re.is_match("Contains author:: John in text")); + } + + #[test] + fn test_uuid_ref_pattern() { + let re = &UUID_REF_PATTERN; + assert!(re.is_match("((550e8400-e29b-41d4-a716-446655440000))")); + assert!(!re.is_match("((invalid))")); + } + + #[test] + fn test_logseq_property_pattern() { + let re = &LOGSEQ_PROPERTY_PATTERN; + assert!(re.is_match("author:: John Doe")); + assert!(re.is_match("created-at:: 2024-01-01")); + assert!(!re.is_match("::missing-prefix")); + } +} diff --git a/crates/kogral-core/src/storage/filesystem.rs b/crates/kogral-core/src/storage/filesystem.rs new file mode 100644 index 0000000..6234506 --- /dev/null +++ b/crates/kogral-core/src/storage/filesystem.rs @@ -0,0 +1,204 @@ +//! Filesystem storage backend +//! +//! Stores nodes as markdown files with YAML frontmatter + +use std::fs; + +use async_trait::async_trait; + +use crate::error::{KbError, Result}; +use crate::models::{Graph, Node}; +use crate::storage::Storage; + +/// Filesystem-based storage backend +/// +/// Stores graphs as directory structures with markdown files. +/// Each node becomes a `.md` file with YAML frontmatter. +pub struct FilesystemStorage { + base_path: std::path::PathBuf, +} + +impl FilesystemStorage { + /// Create a new filesystem storage at the given path + #[must_use] + pub fn new(base_path: std::path::PathBuf) -> Self { + Self { base_path } + } + + /// Get the path to a graph directory (with path traversal protection) + fn graph_path(&self, graph_name: &str) -> std::path::PathBuf { + // Prevent path traversal attacks - reject names with .. or slashes + let safe_name = graph_name.replace("..", "_").replace(['/', '\\'], "_"); + self.base_path.join(safe_name) + } + + /// Get the path to a node file (with path traversal protection) + fn node_path(&self, graph_name: &str, node_id: &str) -> std::path::PathBuf { + // Prevent path traversal - reject IDs with .. or slashes + let safe_id = node_id.replace("..", "_").replace(['/', '\\'], "_"); + self.graph_path(graph_name).join(format!("{safe_id}.md")) + } + + /// Serialize a node to markdown with YAML frontmatter + fn serialize_node(node: &Node) -> Result { + let frontmatter = + serde_yaml::to_string(node).map_err(|e| KbError::Serialization(e.to_string()))?; + Ok(format!("---\n{frontmatter}---\n\n{}", node.content)) + } + + /// Parse markdown with YAML frontmatter into a Node + fn deserialize_node(content: &str) -> Result { + if !content.starts_with("---\n") { + return Err(KbError::Frontmatter("Missing frontmatter".to_string())); + } + + let parts: Vec<&str> = content.splitn(3, "---\n").collect(); + if parts.len() < 3 { + return Err(KbError::Frontmatter( + "Invalid frontmatter format".to_string(), + )); + } + + let yaml = parts[1]; + let body = parts[2]; + + let mut node: Node = + serde_yaml::from_str(yaml).map_err(|e| KbError::Frontmatter(e.to_string()))?; + node.content = body.to_string(); + + Ok(node) + } +} + +#[async_trait] +impl Storage for FilesystemStorage { + async fn save_graph(&mut self, graph: &Graph) -> Result<()> { + let graph_path = self.graph_path(&graph.name); + fs::create_dir_all(&graph_path) + .map_err(|e| KbError::Storage(format!("Failed to create graph directory: {e}")))?; + + for node in graph.nodes.values() { + let node_path = self.node_path(&graph.name, &node.id); + let content = Self::serialize_node(node)?; + fs::write(&node_path, content) + .map_err(|e| KbError::Storage(format!("Failed to write node file: {e}")))?; + } + + Ok(()) + } + + async fn load_graph(&self, name: &str) -> Result { + let graph_path = self.graph_path(name); + if !graph_path.exists() { + return Err(KbError::Graph(format!("Graph not found: {name}"))); + } + + let mut graph = Graph::new(name.to_string()); + let entries = fs::read_dir(&graph_path) + .map_err(|e| KbError::Storage(format!("Failed to read graph directory: {e}")))?; + + for entry in entries { + let entry = entry.map_err(|e| KbError::Storage(e.to_string()))?; + let path = entry.path(); + + if path.extension().is_some_and(|ext| ext == "md") { + let content = fs::read_to_string(&path) + .map_err(|e| KbError::Storage(format!("Failed to read node file: {e}")))?; + let node = Self::deserialize_node(&content)?; + graph.nodes.insert(node.id.clone(), node); + } + } + + Ok(graph) + } + + async fn save_node(&mut self, node: &Node) -> Result<()> { + let graph_name = node + .project + .clone() + .unwrap_or_else(|| "default".to_string()); + let graph_path = self.graph_path(&graph_name); + fs::create_dir_all(&graph_path) + .map_err(|e| KbError::Storage(format!("Failed to create graph directory: {e}")))?; + + let node_path = self.node_path(&graph_name, &node.id); + let content = Self::serialize_node(node)?; + fs::write(&node_path, content) + .map_err(|e| KbError::Storage(format!("Failed to write node file: {e}"))) + } + + async fn load_node(&self, graph_name: &str, node_id: &str) -> Result { + let node_path = self.node_path(graph_name, node_id); + if !node_path.exists() { + return Err(KbError::NodeNotFound(format!("{graph_name}/{node_id}"))); + } + + let content = fs::read_to_string(&node_path) + .map_err(|e| KbError::Storage(format!("Failed to read node file: {e}")))?; + Self::deserialize_node(&content) + } + + async fn delete_node(&mut self, graph_name: &str, node_id: &str) -> Result<()> { + let node_path = self.node_path(graph_name, node_id); + if !node_path.exists() { + return Err(KbError::NodeNotFound(format!("{graph_name}/{node_id}"))); + } + + fs::remove_file(&node_path) + .map_err(|e| KbError::Storage(format!("Failed to delete node file: {e}"))) + } + + async fn list_graphs(&self) -> Result> { + if !self.base_path.exists() { + return Ok(Vec::new()); + } + + let mut graphs = Vec::new(); + let entries = fs::read_dir(&self.base_path) + .map_err(|e| KbError::Storage(format!("Failed to read base path: {e}")))?; + + for entry in entries { + let entry = entry.map_err(|e| KbError::Storage(e.to_string()))?; + let path = entry.path(); + if path.is_dir() { + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + graphs.push(name.to_string()); + } + } + } + + Ok(graphs) + } + + async fn list_nodes(&self, graph_name: &str, node_type: Option<&str>) -> Result> { + let graph_path = self.graph_path(graph_name); + if !graph_path.exists() { + return Err(KbError::Graph(format!("Graph not found: {graph_name}"))); + } + + let mut nodes = Vec::new(); + let entries = fs::read_dir(&graph_path) + .map_err(|e| KbError::Storage(format!("Failed to read graph directory: {e}")))?; + + for entry in entries { + let entry = entry.map_err(|e| KbError::Storage(e.to_string()))?; + let path = entry.path(); + + if path.extension().is_some_and(|ext| ext == "md") { + let content = fs::read_to_string(&path) + .map_err(|e| KbError::Storage(format!("Failed to read node file: {e}")))?; + let node = Self::deserialize_node(&content)?; + + if let Some(filter_type) = node_type { + if node.node_type.to_string() == filter_type { + nodes.push(node); + } + } else { + nodes.push(node); + } + } + } + + Ok(nodes) + } +} diff --git a/crates/kogral-core/src/storage/memory.rs b/crates/kogral-core/src/storage/memory.rs new file mode 100644 index 0000000..eb8bee3 --- /dev/null +++ b/crates/kogral-core/src/storage/memory.rs @@ -0,0 +1,103 @@ +//! In-memory storage backend + +use std::sync::Arc; + +use async_trait::async_trait; +use dashmap::DashMap; + +use crate::error::{KbError, Result}; +use crate::models::{Graph, Node}; +use crate::storage::Storage; + +/// In-memory storage backend using `DashMap` +/// +/// Stores graphs in a concurrent hash map for fast access. +/// Thread-safe and ideal for testing or caching scenarios. +pub struct MemoryStorage { + graphs: Arc>, +} + +impl MemoryStorage { + /// Create a new empty in-memory storage + #[must_use] + pub fn new() -> Self { + Self { + graphs: Arc::new(DashMap::new()), + } + } +} + +impl Default for MemoryStorage { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl Storage for MemoryStorage { + async fn save_graph(&mut self, graph: &Graph) -> Result<()> { + self.graphs.insert(graph.name.clone(), graph.clone()); + Ok(()) + } + + async fn load_graph(&self, name: &str) -> Result { + self.graphs + .get(name) + .map(|g| g.clone()) + .ok_or_else(|| KbError::Graph(format!("Graph not found: {name}"))) + } + + async fn save_node(&mut self, node: &Node) -> Result<()> { + let graph_name = node + .project + .clone() + .unwrap_or_else(|| "default".to_string()); + + let mut graph = self + .load_graph(&graph_name) + .await + .unwrap_or_else(|_| Graph::new(graph_name.clone())); + + graph.nodes.insert(node.id.clone(), node.clone()); + self.save_graph(&graph).await + } + + async fn load_node(&self, graph_name: &str, node_id: &str) -> Result { + self.graphs + .get(graph_name) + .and_then(|graph| graph.nodes.get(node_id).cloned()) + .ok_or_else(|| KbError::NodeNotFound(format!("{graph_name}/{node_id}"))) + } + + async fn delete_node(&mut self, graph_name: &str, node_id: &str) -> Result<()> { + if let Some(mut graph) = self.graphs.get_mut(graph_name) { + if graph.nodes.remove(node_id).is_some() { + return Ok(()); + } + } + Err(KbError::NodeNotFound(format!("{graph_name}/{node_id}"))) + } + + async fn list_graphs(&self) -> Result> { + Ok(self + .graphs + .iter() + .map(|entry| entry.key().clone()) + .collect()) + } + + async fn list_nodes(&self, graph_name: &str, node_type: Option<&str>) -> Result> { + let graph = self + .graphs + .get(graph_name) + .ok_or_else(|| KbError::Graph(format!("Graph not found: {graph_name}")))?; + + let mut nodes: Vec = graph.nodes.values().cloned().collect(); + + if let Some(filter_type) = node_type { + nodes.retain(|n| n.node_type.to_string() == filter_type); + } + + Ok(nodes) + } +} diff --git a/crates/kogral-core/src/storage/mod.rs b/crates/kogral-core/src/storage/mod.rs new file mode 100644 index 0000000..c73bae6 --- /dev/null +++ b/crates/kogral-core/src/storage/mod.rs @@ -0,0 +1,60 @@ +//! Storage backend abstraction and implementations +//! +//! Provides multiple storage backends for KOGRAL: +//! - Filesystem: Git-friendly markdown files +//! - Memory: In-memory graph for dev/cache +//! - `SurrealDB`: Scalable database backend (optional) + +use async_trait::async_trait; + +use crate::error::Result; +use crate::models::{Graph, Node}; + +/// Storage backend trait +/// +/// Provides abstraction over different storage implementations (filesystem, +/// `SurrealDB`, memory). All operations are async and return Result for +/// error handling. +#[async_trait] +pub trait Storage: Send + Sync { + /// Save a complete graph to storage + /// + /// Persists the entire graph including all nodes and edges. + async fn save_graph(&mut self, graph: &Graph) -> Result<()>; + + /// Load a graph from storage + /// + /// Returns a fully reconstructed graph with all nodes and relationships. + async fn load_graph(&self, name: &str) -> Result; + + /// Save a single node to storage + /// + /// Persists a node, updating if it already exists. + async fn save_node(&mut self, node: &Node) -> Result<()>; + + /// Load a node by ID from storage + /// + /// Searches across all node types (notes, decisions, guidelines, patterns, + /// journal, execution). + async fn load_node(&self, graph_name: &str, node_id: &str) -> Result; + + /// Delete a node from storage + /// + /// Removes the node and cleans up relationships. + async fn delete_node(&mut self, graph_name: &str, node_id: &str) -> Result<()>; + + /// List all graphs in storage + async fn list_graphs(&self) -> Result>; + + /// List nodes in a graph, optionally filtered by type + /// + /// Returns all nodes of a specific type if `node_type` is provided, + /// otherwise returns all nodes in the graph. + async fn list_nodes(&self, graph_name: &str, node_type: Option<&str>) -> Result>; +} + +pub mod filesystem; +pub mod memory; + +#[cfg(feature = "surrealdb-backend")] +pub mod surrealdb; diff --git a/crates/kogral-core/src/storage/surrealdb.rs b/crates/kogral-core/src/storage/surrealdb.rs new file mode 100644 index 0000000..6a19ebc --- /dev/null +++ b/crates/kogral-core/src/storage/surrealdb.rs @@ -0,0 +1,271 @@ +//! SurrealDB storage backend +//! +//! Provides scalable, graph-native storage using SurrealDB. +//! Supports distributed deployments and complex queries. + +use std::sync::Arc; + +use async_trait::async_trait; +use surrealdb::engine::any::Any; +use surrealdb::Surreal; +use tokio::sync::RwLock; + +use crate::error::{KbError, Result}; +use crate::models::{Graph, Node}; +use crate::storage::Storage; + +/// SurrealDB storage backend +/// +/// Stores graphs and nodes in SurrealDB with full ACID transactions. +/// Connection is wrapped in Arc> for thread-safe concurrent access. +pub struct SurrealDbStorage { + db: Arc>>, + namespace: String, + database: String, +} + +impl SurrealDbStorage { + /// Create a new SurrealDB storage instance + /// + /// # Arguments + /// * `db` - Connected SurrealDB instance + /// * `namespace` - SurrealDB namespace (default: "kogral") + /// * `database` - SurrealDB database (default: "kb") + pub fn new(db: Surreal, namespace: String, database: String) -> Self { + Self { + db: Arc::new(RwLock::new(db)), + namespace, + database, + } + } + + /// Create a new storage with default namespace and database names + pub fn with_defaults(db: Surreal) -> Self { + Self::new(db, "kogral".to_string(), "kb".to_string()) + } +} + +impl Clone for SurrealDbStorage { + fn clone(&self) -> Self { + Self { + db: Arc::clone(&self.db), + namespace: self.namespace.clone(), + database: self.database.clone(), + } + } +} + +#[async_trait] +impl Storage for SurrealDbStorage { + async fn save_graph(&mut self, graph: &Graph) -> Result<()> { + let db = self.db.write().await; + let _ = db + .use_ns(&self.namespace) + .use_db(&self.database) + .await + .map_err(|e| KbError::Database(e))?; + + // Serialize graph and all nodes + let graph_json = serde_json::to_value(graph) + .map_err(|e| KbError::Serialization(format!("Graph serialization error: {}", e)))?; + + // Use raw SurrealQL query for upserting + let query = "UPSERT graphs:$graph_id SET * = $content;"; + let graph_id = graph.name.clone(); + + let _: Vec = db + .query(query) + .bind(("graph_id", graph_id)) + .bind(("content", graph_json)) + .await + .map_err(|e| KbError::Database(e))? + .take(0) + .map_err(|e| KbError::Database(e))?; + + // Upsert all nodes + for node in graph.nodes.values() { + let node_json = serde_json::to_value(node) + .map_err(|e| KbError::Serialization(format!("Node serialization error: {}", e)))?; + let node_key = format!("{}_{}", graph.name, node.id); + + let query = "UPSERT nodes:$node_id SET * = $content;"; + let _: Vec = db + .query(query) + .bind(("node_id", node_key)) + .bind(("content", node_json)) + .await + .map_err(|e| KbError::Database(e))? + .take(0) + .map_err(|e| KbError::Database(e))?; + } + + Ok(()) + } + + async fn load_graph(&self, name: &str) -> Result { + let db = self.db.read().await; + let _ = db + .use_ns(&self.namespace) + .use_db(&self.database) + .await + .map_err(|e| KbError::Database(e))?; + + // Load graph metadata using raw query + let query = "SELECT * FROM graphs:$graph_id;"; + let graph_id = name.to_string(); + let result: Vec> = db + .query(query) + .bind(("graph_id", graph_id)) + .await + .map_err(|e| KbError::Database(e))? + .take(0) + .map_err(|e| KbError::Database(e))?; + + let mut graph = result + .into_iter() + .next() + .flatten() + .ok_or_else(|| KbError::Graph(format!("Graph not found: {}", name)))?; + + // Load all nodes for this graph + let query = "SELECT * FROM nodes WHERE id LIKE $pattern;"; + let pattern = format!("{}_%", name); + let nodes: Vec = db + .query(query) + .bind(("pattern", pattern)) + .await + .map_err(|e| KbError::Database(e))? + .take(0) + .map_err(|e| KbError::Database(e))?; + + for node in nodes { + graph.nodes.insert(node.id.clone(), node); + } + + Ok(graph) + } + + async fn save_node(&mut self, node: &Node) -> Result<()> { + let db = self.db.write().await; + let _ = db + .use_ns(&self.namespace) + .use_db(&self.database) + .await + .map_err(|e| KbError::Database(e))?; + + let graph_name = node + .project + .clone() + .unwrap_or_else(|| "default".to_string()); + let node_id = format!("{}_{}", graph_name, node.id); + let node_json = serde_json::to_value(node) + .map_err(|e| KbError::Serialization(format!("Node serialization error: {}", e)))?; + + let query = "UPSERT nodes:$node_id SET * = $content;"; + let _: Vec = db + .query(query) + .bind(("node_id", node_id)) + .bind(("content", node_json)) + .await + .map_err(|e| KbError::Database(e))? + .take(0) + .map_err(|e| KbError::Database(e))?; + + Ok(()) + } + + async fn load_node(&self, graph_name: &str, node_id: &str) -> Result { + let db = self.db.read().await; + let _ = db + .use_ns(&self.namespace) + .use_db(&self.database) + .await + .map_err(|e| KbError::Database(e))?; + + let combined_id = format!("{}_{}", graph_name, node_id); + let query = "SELECT * FROM nodes:$node_id;"; + let result: Vec> = db + .query(query) + .bind(("node_id", combined_id)) + .await + .map_err(|e| KbError::Database(e))? + .take(0) + .map_err(|e| KbError::Database(e))?; + + result + .into_iter() + .next() + .flatten() + .ok_or_else(|| KbError::NodeNotFound(format!("{}/{}", graph_name, node_id))) + } + + async fn delete_node(&mut self, graph_name: &str, node_id: &str) -> Result<()> { + let db = self.db.write().await; + let _ = db + .use_ns(&self.namespace) + .use_db(&self.database) + .await + .map_err(|e| KbError::Database(e))?; + + let combined_id = format!("{}_{}", graph_name, node_id); + let query = "DELETE nodes:$node_id RETURN BEFORE;"; + let deleted: Vec> = db + .query(query) + .bind(("node_id", combined_id)) + .await + .map_err(|e| KbError::Database(e))? + .take(0) + .map_err(|e| KbError::Database(e))?; + + deleted + .into_iter() + .next() + .flatten() + .ok_or_else(|| KbError::NodeNotFound(format!("{}/{}", graph_name, node_id))) + .map(|_| ()) + } + + async fn list_graphs(&self) -> Result> { + let db = self.db.read().await; + let _ = db + .use_ns(&self.namespace) + .use_db(&self.database) + .await + .map_err(|e| KbError::Database(e))?; + + let graphs: Vec = db + .select("graphs") + .await + .map_err(|e| KbError::Database(e))?; + + Ok(graphs.into_iter().map(|g| g.name).collect()) + } + + async fn list_nodes(&self, graph_name: &str, node_type: Option<&str>) -> Result> { + let db = self.db.read().await; + let _ = db + .use_ns(&self.namespace) + .use_db(&self.database) + .await + .map_err(|e| KbError::Database(e))?; + + let mut query_builder = db.query(if let Some(_) = node_type { + "SELECT * FROM nodes WHERE id LIKE $id_pattern AND type = $type_filter" + } else { + "SELECT * FROM nodes WHERE id LIKE $id_pattern" + }); + + let id_pattern = format!("{}_%", graph_name); + query_builder = query_builder.bind(("id_pattern", id_pattern)); + + if let Some(filter_type) = node_type { + query_builder = query_builder.bind(("type_filter", filter_type)); + } + + query_builder + .await + .map_err(|e| KbError::Database(e))? + .take(0) + .map_err(|e| KbError::Database(e)) + } +} diff --git a/crates/kogral-core/src/sync.rs b/crates/kogral-core/src/sync.rs new file mode 100644 index 0000000..ac0b1f1 --- /dev/null +++ b/crates/kogral-core/src/sync.rs @@ -0,0 +1,548 @@ +//! Bidirectional synchronization between storage backends +//! +//! Provides synchronization between two storage backends (typically filesystem +//! ↔ `SurrealDB`). Supports multiple sync strategies: +//! - `sync_to_target()`: One-way sync from source to target +//! - `sync_from_target()`: One-way sync from target to source +//! - `sync_bidirectional()`: Two-way sync with conflict resolution +//! +//! # Conflict Resolution +//! +//! When the same node exists in both storages with different content: +//! - **`LastWriteWins`**: Use the node with the most recent `modified` +//! timestamp +//! - **`SourceWins`**: Always prefer the source node +//! - **`TargetWins`**: Always prefer the target node +//! +//! # Example +//! +//! ```no_run +//! use kogral_core::sync::{SyncManager, ConflictStrategy}; +//! use kogral_core::storage::memory::MemoryStorage; +//! use kogral_core::config::SyncConfig; +//! use std::sync::Arc; +//! use tokio::sync::RwLock; +//! +//! # async fn example() -> kogral_core::error::Result<()> { +//! let source = Arc::new(RwLock::new(MemoryStorage::new())); +//! let target = Arc::new(RwLock::new(MemoryStorage::new())); +//! let config = SyncConfig::default(); +//! +//! let manager = SyncManager::new(source, target, config); +//! manager.sync_to_target().await?; +//! # Ok(()) +//! # } +//! ``` + +use std::sync::Arc; + +use tokio::sync::RwLock; +use tracing::{debug, info, warn}; + +use crate::config::SyncConfig; +use crate::error::{KbError, Result}; +use crate::models::Node; +use crate::storage::Storage; + +/// Conflict resolution strategy when the same node differs between storages +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ConflictStrategy { + /// Use the node with the most recent `modified` timestamp + #[default] + LastWriteWins, + /// Always prefer the source node + SourceWins, + /// Always prefer the target node + TargetWins, +} + +/// Bidirectional synchronization manager between two storage backends +/// +/// Manages synchronization between a source storage (typically filesystem) +/// and a target storage (typically `SurrealDB` or another backend). +/// +/// Both storages are wrapped in `Arc>` to support concurrent access +/// and async operations. +pub struct SyncManager { + /// Source storage (typically filesystem) + source: Arc>, + /// Target storage (typically `SurrealDB`) + target: Arc>, + /// Sync configuration + config: SyncConfig, + /// Conflict resolution strategy + conflict_strategy: ConflictStrategy, +} + +impl SyncManager { + /// Create a new sync manager + /// + /// # Arguments + /// + /// * `source` - Source storage backend (typically filesystem) + /// * `target` - Target storage backend (typically `SurrealDB`) + /// * `config` - Sync configuration + /// + /// # Example + /// + /// ```no_run + /// use kogral_core::sync::SyncManager; + /// use kogral_core::storage::memory::MemoryStorage; + /// use kogral_core::config::SyncConfig; + /// use std::sync::Arc; + /// use tokio::sync::RwLock; + /// + /// let source = Arc::new(RwLock::new(MemoryStorage::new())); + /// let target = Arc::new(RwLock::new(MemoryStorage::new())); + /// let config = SyncConfig::default(); + /// + /// let manager = SyncManager::new(source, target, config); + /// ``` + pub fn new( + source: Arc>, + target: Arc>, + config: SyncConfig, + ) -> Self { + Self { + source, + target, + config, + conflict_strategy: ConflictStrategy::default(), + } + } + + /// Create a sync manager with custom conflict resolution strategy + pub fn with_conflict_strategy( + source: Arc>, + target: Arc>, + config: SyncConfig, + conflict_strategy: ConflictStrategy, + ) -> Self { + Self { + source, + target, + config, + conflict_strategy, + } + } + + /// Synchronize from source to target (one-way) + /// + /// Copies all graphs and nodes from source storage to target storage. + /// Does not modify source storage. + /// + /// # Errors + /// + /// Returns error if: + /// - Failed to list graphs from source + /// - Failed to load graph from source + /// - Failed to save graph to target + /// + /// # Example + /// + /// ```no_run + /// # use kogral_core::sync::SyncManager; + /// # use kogral_core::storage::memory::MemoryStorage; + /// # use kogral_core::config::SyncConfig; + /// # use std::sync::Arc; + /// # use tokio::sync::RwLock; + /// # async fn example() -> kogral_core::error::Result<()> { + /// # let source = Arc::new(RwLock::new(MemoryStorage::new())); + /// # let target = Arc::new(RwLock::new(MemoryStorage::new())); + /// # let manager = SyncManager::new(source, target, SyncConfig::default()); + /// manager.sync_to_target().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn sync_to_target(&self) -> Result<()> { + info!("Starting sync from source to target"); + + let source = self.source.read().await; + let graphs = source.list_graphs().await?; + debug!("Found {} graphs in source", graphs.len()); + + let mut target = self.target.write().await; + + for graph_name in graphs { + let graph = source.load_graph(&graph_name).await?; + info!( + "Syncing graph '{}' to target ({} nodes)", + graph_name, + graph.nodes.len() + ); + target.save_graph(&graph).await?; + } + + info!("Sync to target completed successfully"); + Ok(()) + } + + /// Synchronize from target to source (one-way) + /// + /// Copies all graphs and nodes from target storage to source storage. + /// Does not modify target storage. + /// + /// # Errors + /// + /// Returns error if: + /// - Failed to list graphs from target + /// - Failed to load graph from target + /// - Failed to save graph to source + pub async fn sync_from_target(&self) -> Result<()> { + info!("Starting sync from target to source"); + + let target = self.target.read().await; + let graphs = target.list_graphs().await?; + debug!("Found {} graphs in target", graphs.len()); + + let mut source = self.source.write().await; + + for graph_name in graphs { + let graph = target.load_graph(&graph_name).await?; + info!( + "Syncing graph '{}' to source ({} nodes)", + graph_name, + graph.nodes.len() + ); + source.save_graph(&graph).await?; + } + + info!("Sync from target completed successfully"); + Ok(()) + } + + /// Bidirectional synchronization with conflict resolution + /// + /// Syncs nodes in both directions, resolving conflicts based on the + /// configured conflict strategy. + /// + /// # Algorithm + /// + /// 1. Load all graph names from both storages + /// 2. For each unique graph: + /// - Load from both storages (if exists) + /// - Compare node IDs + /// - For nodes only in source: copy to target + /// - For nodes only in target: copy to source + /// - For nodes in both: resolve conflict + /// + /// # Conflict Resolution + /// + /// - `LastWriteWins`: Compare `modified` timestamps + /// - `SourceWins`: Always use source node + /// - `TargetWins`: Always use target node + /// + /// # Errors + /// + /// Returns error if storage operations fail + pub async fn sync_bidirectional(&self) -> Result<()> { + info!( + "Starting bidirectional sync with strategy: {:?}", + self.conflict_strategy + ); + + let source = self.source.read().await; + let target = self.target.read().await; + + let source_graphs = source.list_graphs().await?; + let target_graphs = target.list_graphs().await?; + + // Combine unique graph names + let mut all_graphs: Vec = source_graphs.clone(); + for graph in target_graphs { + if !all_graphs.contains(&graph) { + all_graphs.push(graph); + } + } + + debug!( + "Found {} unique graphs across both storages", + all_graphs.len() + ); + + // Drop read locks before acquiring write locks + drop(source); + drop(target); + + for graph_name in all_graphs { + self.sync_graph_bidirectional(&graph_name).await?; + } + + info!("Bidirectional sync completed successfully"); + Ok(()) + } + + /// Sync a single graph bidirectionally + async fn sync_graph_bidirectional(&self, graph_name: &str) -> Result<()> { + let source = self.source.read().await; + let target = self.target.read().await; + + let source_graph = source.load_graph(graph_name).await; + let target_graph = target.load_graph(graph_name).await; + + match (source_graph, target_graph) { + (Ok(mut src), Ok(mut tgt)) => { + debug!("Graph '{}' exists in both storages", graph_name); + + // Find nodes only in source + let source_only: Vec = src + .nodes + .keys() + .filter(|id| !tgt.nodes.contains_key(*id)) + .cloned() + .collect(); + + // Find nodes only in target + let target_only: Vec = tgt + .nodes + .keys() + .filter(|id| !src.nodes.contains_key(*id)) + .cloned() + .collect(); + + // Find nodes in both (potential conflicts) + let in_both: Vec = src + .nodes + .keys() + .filter(|id| tgt.nodes.contains_key(*id)) + .cloned() + .collect(); + + debug!( + "Graph '{}': {} source-only, {} target-only, {} in both", + graph_name, + source_only.len(), + target_only.len(), + in_both.len() + ); + + // Copy source-only nodes to target + for node_id in source_only { + if let Some(node) = src.nodes.get(&node_id) { + tgt.nodes.insert(node_id.clone(), node.clone()); + } + } + + // Copy target-only nodes to source + for node_id in target_only { + if let Some(node) = tgt.nodes.get(&node_id) { + src.nodes.insert(node_id.clone(), node.clone()); + } + } + + // Resolve conflicts for nodes in both + for node_id in in_both { + if let (Some(src_node), Some(tgt_node)) = + (src.nodes.get(&node_id), tgt.nodes.get(&node_id)) + { + let winning_node = self.resolve_conflict(src_node, tgt_node); + src.nodes.insert(node_id.clone(), winning_node.clone()); + tgt.nodes.insert(node_id.clone(), winning_node); + } + } + + // Drop read locks + drop(source); + drop(target); + + // Save updated graphs + let mut source_write = self.source.write().await; + let mut target_write = self.target.write().await; + + source_write.save_graph(&src).await?; + target_write.save_graph(&tgt).await?; + } + (Ok(graph), Err(_)) => { + debug!("Graph '{}' only in source, copying to target", graph_name); + drop(source); + drop(target); + let mut target_write = self.target.write().await; + target_write.save_graph(&graph).await?; + } + (Err(_), Ok(graph)) => { + debug!("Graph '{}' only in target, copying to source", graph_name); + drop(source); + drop(target); + let mut source_write = self.source.write().await; + source_write.save_graph(&graph).await?; + } + (Err(e1), Err(e2)) => { + warn!( + "Graph '{}' not found in either storage: source={:?}, target={:?}", + graph_name, e1, e2 + ); + return Err(KbError::Graph(format!( + "Graph '{graph_name}' not found in either storage" + ))); + } + } + + Ok(()) + } + + /// Resolve conflict between two versions of the same node + fn resolve_conflict(&self, source_node: &Node, target_node: &Node) -> Node { + match self.conflict_strategy { + ConflictStrategy::LastWriteWins => { + if source_node.modified > target_node.modified { + debug!("Conflict resolved: source wins (newer modified time)"); + source_node.clone() + } else { + debug!("Conflict resolved: target wins (newer modified time)"); + target_node.clone() + } + } + ConflictStrategy::SourceWins => { + debug!("Conflict resolved: source wins (strategy)"); + source_node.clone() + } + ConflictStrategy::TargetWins => { + debug!("Conflict resolved: target wins (strategy)"); + target_node.clone() + } + } + } + + /// Get the sync configuration + #[must_use] + pub fn config(&self) -> &SyncConfig { + &self.config + } + + /// Get the current conflict resolution strategy + #[must_use] + pub fn conflict_strategy(&self) -> ConflictStrategy { + self.conflict_strategy + } +} + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::models::{Graph, Node, NodeType}; + use crate::storage::memory::MemoryStorage; + + fn create_test_node(id: &str, title: &str) -> Node { + let mut node = Node::new(NodeType::Note, title.to_string()); + node.id = id.to_string(); // Override UUID with test ID + node + } + + #[tokio::test] + async fn test_sync_to_target() { + let mut source_storage = MemoryStorage::new(); + let target_storage = MemoryStorage::new(); + + // Add graph to source + let mut graph = Graph::new("test".to_string()); + let node = create_test_node("node-1", "Test Node"); + graph.nodes.insert("node-1".to_string(), node); + + source_storage.save_graph(&graph).await.unwrap(); + + let source = Arc::new(RwLock::new(source_storage)); + let target = Arc::new(RwLock::new(target_storage)); + + let manager = SyncManager::new(source.clone(), target.clone(), SyncConfig::default()); + manager.sync_to_target().await.unwrap(); + + // Verify node exists in target + let target_read = target.read().await; + let loaded_node = target_read.load_node("test", "node-1").await.unwrap(); + assert_eq!(loaded_node.title, "Test Node"); + } + + #[tokio::test] + async fn test_sync_from_target() { + let source_storage = MemoryStorage::new(); + let mut target_storage = MemoryStorage::new(); + + // Add graph to target + let mut graph = Graph::new("test".to_string()); + let node = create_test_node("node-2", "Target Node"); + graph.nodes.insert("node-2".to_string(), node); + + target_storage.save_graph(&graph).await.unwrap(); + + let source = Arc::new(RwLock::new(source_storage)); + let target = Arc::new(RwLock::new(target_storage)); + + let manager = SyncManager::new(source.clone(), target.clone(), SyncConfig::default()); + manager.sync_from_target().await.unwrap(); + + // Verify node exists in source + let source_read = source.read().await; + let loaded_node = source_read.load_node("test", "node-2").await.unwrap(); + assert_eq!(loaded_node.title, "Target Node"); + } + + #[tokio::test] + async fn test_bidirectional_sync_source_only() { + let mut source_storage = MemoryStorage::new(); + let target_storage = MemoryStorage::new(); + + let mut graph = Graph::new("test".to_string()); + let node = create_test_node("node-src", "Source Only"); + graph.nodes.insert("node-src".to_string(), node); + source_storage.save_graph(&graph).await.unwrap(); + + let source = Arc::new(RwLock::new(source_storage)); + let target = Arc::new(RwLock::new(target_storage)); + + let manager = SyncManager::new(source.clone(), target.clone(), SyncConfig::default()); + manager.sync_bidirectional().await.unwrap(); + + // Node should exist in both + let source_read = source.read().await; + let target_read = target.read().await; + + assert!(source_read.load_node("test", "node-src").await.is_ok()); + assert!(target_read.load_node("test", "node-src").await.is_ok()); + } + + #[tokio::test] + async fn test_conflict_resolution_last_write_wins() { + let mut source_storage = MemoryStorage::new(); + let mut target_storage = MemoryStorage::new(); + + // Create conflicting nodes + let mut old_node = create_test_node("conflict", "Old Version"); + old_node.modified = Utc::now() - chrono::Duration::hours(1); + + let new_node = create_test_node("conflict", "New Version"); + + let mut source_graph = Graph::new("test".to_string()); + source_graph + .nodes + .insert("conflict".to_string(), new_node.clone()); + source_storage.save_graph(&source_graph).await.unwrap(); + + let mut target_graph = Graph::new("test".to_string()); + target_graph.nodes.insert("conflict".to_string(), old_node); + target_storage.save_graph(&target_graph).await.unwrap(); + + let source = Arc::new(RwLock::new(source_storage)); + let target = Arc::new(RwLock::new(target_storage)); + + let manager = SyncManager::with_conflict_strategy( + source.clone(), + target.clone(), + SyncConfig::default(), + ConflictStrategy::LastWriteWins, + ); + + manager.sync_bidirectional().await.unwrap(); + + // Both should have the newer version + let source_read = source.read().await; + let target_read = target.read().await; + + let source_node = source_read.load_node("test", "conflict").await.unwrap(); + let target_node = target_read.load_node("test", "conflict").await.unwrap(); + + assert_eq!(source_node.title, "New Version"); + assert_eq!(target_node.title, "New Version"); + } +} diff --git a/crates/kogral-core/tests/nickel_integration_test.rs b/crates/kogral-core/tests/nickel_integration_test.rs new file mode 100644 index 0000000..caaf4b1 --- /dev/null +++ b/crates/kogral-core/tests/nickel_integration_test.rs @@ -0,0 +1,83 @@ +//! Integration tests for Nickel configuration loading + +use std::path::{Path, PathBuf}; + +use kogral_core::config::nickel; +use kogral_core::config::schema::{EmbeddingProvider, KbConfig, StorageType}; + +fn workspace_dir() -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.pop(); // kb-core + path.pop(); // crates + path +} + +#[test] +#[ignore] // Requires Nickel CLI to be installed +fn test_load_minimal_config() { + if !nickel::is_nickel_available() { + eprintln!("Skipping: Nickel CLI not available"); + return; + } + + let config_path = workspace_dir().join("config/minimal.ncl"); + let config: KbConfig = nickel::load_nickel_config(&config_path).expect("Failed to load config"); + + assert_eq!(config.graph.name, "minimal-kb"); + assert_eq!(config.graph.version, "1.0.0"); + assert_eq!(config.storage.primary, StorageType::Filesystem); + assert_eq!(config.embeddings.provider, EmbeddingProvider::Fastembed); +} + +#[test] +#[ignore] // Requires Nickel CLI to be installed +fn test_load_defaults_config() { + if !nickel::is_nickel_available() { + eprintln!("Skipping: Nickel CLI not available"); + return; + } + + let config_path = workspace_dir().join("config/defaults.ncl"); + let config: KbConfig = nickel::load_nickel_config(&config_path).expect("Failed to load config"); + + assert_eq!(config.graph.name, "my-project-kb"); + assert_eq!(config.graph.description, "Project KOGRAL"); + assert_eq!(config.embeddings.dimensions, 384); + assert_eq!(config.query.max_results, 10); + assert_eq!(config.query.similarity_threshold, 0.4); + assert!(config.mcp.tools.search); + assert!(config.sync.auto_index); + assert_eq!(config.sync.debounce_ms, 500); +} + +#[test] +#[ignore] // Requires Nickel CLI to be installed +fn test_load_production_config() { + if !nickel::is_nickel_available() { + eprintln!("Skipping: Nickel CLI not available"); + return; + } + + let config_path = workspace_dir().join("config/production.ncl"); + let config: KbConfig = nickel::load_nickel_config(&config_path).expect("Failed to load config"); + + assert_eq!(config.graph.name, "tools-ecosystem-kb"); + assert!(config.storage.secondary.enabled); + assert_eq!(config.storage.secondary.namespace, "tools_kb"); + assert_eq!(config.storage.secondary.database, "production"); + assert_eq!(config.embeddings.provider, EmbeddingProvider::Openai); + assert_eq!(config.embeddings.model, "text-embedding-3-small"); + assert_eq!(config.query.similarity_threshold, 0.5); + assert!(!config.query.cross_graph); +} + +#[test] +fn test_nickel_availability() { + let available = nickel::is_nickel_available(); + println!("Nickel CLI available: {}", available); + + if available { + let version = nickel::nickel_version().expect("Failed to get version"); + println!("Nickel version: {}", version); + } +} diff --git a/crates/kogral-mcp/Cargo.toml b/crates/kogral-mcp/Cargo.toml new file mode 100644 index 0000000..b10c944 --- /dev/null +++ b/crates/kogral-mcp/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "kogral-mcp" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "KOGRAL MCP Server - Model Context Protocol integration for Claude Code" + +[[bin]] +name = "kogral-mcp" +path = "src/main.rs" + +[dependencies] +kogral-core = { path = "../kogral-core" } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["full"] } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +async-trait = { workspace = true } +chrono = { workspace = true } diff --git a/crates/kogral-mcp/src/auth.rs b/crates/kogral-mcp/src/auth.rs new file mode 100644 index 0000000..d5439fe --- /dev/null +++ b/crates/kogral-mcp/src/auth.rs @@ -0,0 +1,164 @@ +//! Authentication and authorization for MCP server +//! +//! Provides token-based authentication mechanism for MCP requests. +//! Tokens can be provided via: +//! 1. Environment variable: `KOGRAL_MCP_TOKEN` +//! 2. JSON-RPC request parameter: `token` field in params + +use anyhow::{anyhow, Result}; +use serde_json::Value; +use tracing::{debug, warn}; + +/// Authentication configuration +pub struct AuthConfig { + /// Expected token for authentication (from environment or config) + token: Option, + /// Whether authentication is required (true if token is set) + required: bool, +} + +impl AuthConfig { + /// Create authentication config from environment and settings + pub fn from_env() -> Self { + let token = std::env::var("KOGRAL_MCP_TOKEN").ok(); + let required = token.is_some(); + + Self { token, required } + } + + /// Verify a request token against the configured token + /// + /// # Arguments + /// * `request_token` - Token provided in the request (can be from params or header) + /// + /// # Returns + /// * Ok(()) if authentication succeeds + /// * Err if authentication fails + pub fn verify(&self, request_token: Option<&str>) -> Result<()> { + if !self.required { + // Authentication not required + return Ok(()); + } + + let configured_token = match &self.token { + Some(t) => t, + None => return Err(anyhow!("Authentication required but no token configured")), + }; + + let provided_token = match request_token { + Some(t) => t, + None => { + warn!("Authentication required but no token provided"); + return Err(anyhow!("Missing authentication token")); + } + }; + + // Constant-time comparison to prevent timing attacks + if constant_time_compare(provided_token, configured_token) { + debug!("Authentication successful"); + Ok(()) + } else { + warn!("Authentication failed: invalid token"); + Err(anyhow!("Invalid authentication token")) + } + } + + /// Whether authentication is required for this server + pub fn is_required(&self) -> bool { + self.required + } +} + +/// Extract token from JSON-RPC request parameters +/// +/// Supports multiple formats: +/// 1. `{"token": "..."}` - Direct token field +/// 2. `{"params": {"token": "..."}...}` - Token in params object +pub fn extract_token_from_params(params: &Value) -> Option<&str> { + // Try direct token field + if let Some(token) = params.get("token").and_then(|v| v.as_str()) { + return Some(token); + } + + // Try nested in object + if let Some(obj) = params.as_object() { + if let Some(token_value) = obj.get("token") { + return token_value.as_str(); + } + } + + None +} + +/// Constant-time string comparison to prevent timing attacks +fn constant_time_compare(a: &str, b: &str) -> bool { + if a.len() != b.len() { + return false; + } + + let mut result = 0u8; + for (a_byte, b_byte) in a.bytes().zip(b.bytes()) { + result |= a_byte ^ b_byte; + } + + result == 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_auth_config_no_env_token() { + // When no token is set in environment + std::env::remove_var("KOGRAL_MCP_TOKEN"); + let config = AuthConfig::from_env(); + assert!(!config.is_required()); + assert!(config.verify(None).is_ok()); + assert!(config.verify(Some("any-token")).is_ok()); + } + + #[test] + fn test_auth_config_with_env_token() { + // When token is set in environment + std::env::set_var("KOGRAL_MCP_TOKEN", "secret-token"); + let config = AuthConfig::from_env(); + assert!(config.is_required()); + + // Missing token should fail + assert!(config.verify(None).is_err()); + + // Wrong token should fail + assert!(config.verify(Some("wrong-token")).is_err()); + + // Correct token should succeed + assert!(config.verify(Some("secret-token")).is_ok()); + } + + #[test] + fn test_extract_token_from_direct_field() { + let params = serde_json::json!({"token": "my-token", "other": "field"}); + assert_eq!(extract_token_from_params(¶ms), Some("my-token")); + } + + #[test] + fn test_extract_token_missing() { + let params = serde_json::json!({"other": "field"}); + assert_eq!(extract_token_from_params(¶ms), None); + } + + #[test] + fn test_constant_time_compare_equal() { + assert!(constant_time_compare("secret", "secret")); + } + + #[test] + fn test_constant_time_compare_different() { + assert!(!constant_time_compare("secret", "wrong")); + } + + #[test] + fn test_constant_time_compare_different_length() { + assert!(!constant_time_compare("short", "much-longer-string")); + } +} diff --git a/crates/kogral-mcp/src/lib.rs b/crates/kogral-mcp/src/lib.rs new file mode 100644 index 0000000..4b9ba4e --- /dev/null +++ b/crates/kogral-mcp/src/lib.rs @@ -0,0 +1,15 @@ +//! MCP server for KOGRAL integration +//! +//! Provides Model Context Protocol server for Claude Code integration. + +#![forbid(unsafe_code)] + +pub mod auth; +pub mod prompts; +pub mod resources; +pub mod server; +pub mod tools; +pub mod types; +pub mod validation; + +pub use server::McpServer; diff --git a/crates/kogral-mcp/src/main.rs b/crates/kogral-mcp/src/main.rs new file mode 100644 index 0000000..1fb6741 --- /dev/null +++ b/crates/kogral-mcp/src/main.rs @@ -0,0 +1,25 @@ +//! MCP server binary + +use anyhow::Result; +use kogral_mcp::McpServer; + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .with_writer(std::io::stderr) // Write logs to stderr to not interfere with stdio transport + .init(); + + // Create and run server + let server = McpServer::new( + "kogral-mcp".to_string(), + env!("CARGO_PKG_VERSION").to_string(), + ); + server.run_stdio().await?; + + Ok(()) +} diff --git a/crates/kogral-mcp/src/prompts.rs b/crates/kogral-mcp/src/prompts.rs new file mode 100644 index 0000000..e0455ad --- /dev/null +++ b/crates/kogral-mcp/src/prompts.rs @@ -0,0 +1,296 @@ +//! MCP prompt handlers + +use anyhow::Result; +use kogral_core::{ + models::{Edge, Graph, NodeType}, + storage::{filesystem::FilesystemStorage, Storage}, +}; +use serde_json::Value; +use tracing::info; + +use crate::types::{Prompt, PromptArgument, PromptContent, PromptMessage}; + +/// List available prompts +pub fn list_prompts() -> Vec { + vec![ + Prompt { + name: "kogral/summarize_project".to_string(), + description: "Generate a comprehensive summary of the project's KOGRAL".to_string(), + arguments: Some(vec![PromptArgument { + name: "project".to_string(), + description: "Project name to summarize".to_string(), + required: false, + }]), + }, + Prompt { + name: "kogral/find_related".to_string(), + description: "Find nodes related to a given topic or node".to_string(), + arguments: Some(vec![ + PromptArgument { + name: "topic".to_string(), + description: "Topic or node ID to find related items for".to_string(), + required: true, + }, + PromptArgument { + name: "depth".to_string(), + description: "How many levels deep to traverse relationships (default: 1)" + .to_string(), + required: false, + }, + ]), + }, + ] +} + +/// Execute a prompt +pub async fn execute_prompt(name: &str, arguments: Value) -> Result> { + info!("Executing prompt: {} with args: {}", name, arguments); + + match name { + "kogral/summarize_project" => prompt_summarize_project(arguments).await, + "kogral/find_related" => prompt_find_related(arguments).await, + _ => Err(anyhow::anyhow!("Unknown prompt: {}", name)), + } +} + +/// Generate project summary prompt +async fn prompt_summarize_project(args: Value) -> Result> { + let project = args["project"].as_str().unwrap_or("default"); + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let graph = storage.load_graph(project).await?; + + let mut summary = String::from("# Project Knowledge Base Summary\n\n"); + + // Overview statistics + summary.push_str(&format!("**Project:** {}\n", graph.name)); + summary.push_str(&format!("**Total Nodes:** {}\n", graph.nodes.len())); + summary.push_str(&format!( + "**Total Relationships:** {}\n\n", + graph.edges.len() + )); + + // Count nodes by type + let mut type_counts = std::collections::HashMap::new(); + for node in graph.nodes.values() { + *type_counts + .entry(format!("{:?}", node.node_type)) + .or_insert(0) += 1; + } + + summary.push_str("## Nodes by Type\n\n"); + for (node_type, count) in &type_counts { + summary.push_str(&format!("- **{}:** {}\n", node_type, count)); + } + summary.push('\n'); + + // Recent decisions + let mut decisions: Vec<_> = graph + .nodes + .values() + .filter(|n| n.node_type == NodeType::Decision) + .collect(); + decisions.sort_by(|a, b| b.created.cmp(&a.created)); + + if !decisions.is_empty() { + summary.push_str("## Recent Architectural Decisions\n\n"); + for decision in decisions.iter().take(5) { + summary.push_str(&format!("- **{}** ({})\n", decision.title, decision.id)); + if let Some(decision_text) = decision.metadata.get("decision") { + if let Some(text) = decision_text.as_str() { + let preview: String = text + .lines() + .next() + .unwrap_or("") + .chars() + .take(100) + .collect(); + summary.push_str(&format!(" {}\n", preview)); + } + } + } + summary.push('\n'); + } + + // Active guidelines + let guidelines: Vec<_> = graph + .nodes + .values() + .filter(|n| n.node_type == NodeType::Guideline) + .collect(); + + if !guidelines.is_empty() { + summary.push_str("## Active Guidelines\n\n"); + for guideline in guidelines.iter().take(10) { + summary.push_str(&format!("- **{}**\n", guideline.title)); + if let Some(lang) = guideline.metadata.get("language").and_then(|v| v.as_str()) { + summary.push_str(&format!(" Language: {}\n", lang)); + } + if let Some(cat) = guideline.metadata.get("category").and_then(|v| v.as_str()) { + summary.push_str(&format!(" Category: {}\n", cat)); + } + } + summary.push('\n'); + } + + // Key patterns + let patterns: Vec<_> = graph + .nodes + .values() + .filter(|n| n.node_type == NodeType::Pattern) + .collect(); + + if !patterns.is_empty() { + summary.push_str("## Design Patterns\n\n"); + for pattern in patterns { + summary.push_str(&format!("- **{}**\n", pattern.title)); + } + summary.push('\n'); + } + + // Top tags + let mut tag_counts = std::collections::HashMap::new(); + for node in graph.nodes.values() { + for tag in &node.tags { + *tag_counts.entry(tag.clone()).or_insert(0) += 1; + } + } + + if !tag_counts.is_empty() { + summary.push_str("## Most Common Tags\n\n"); + let mut tags: Vec<_> = tag_counts.iter().collect(); + tags.sort_by(|a, b| b.1.cmp(a.1)); + for (tag, count) in tags.iter().take(10) { + summary.push_str(&format!("- **{}**: {} nodes\n", tag, count)); + } + } + + Ok(vec![PromptMessage { + role: "user".to_string(), + content: PromptContent::Text { text: summary }, + }]) +} + +/// Helper to format related edges into text +fn format_edge_relationships(edges: &[&Edge], graph: &Graph, node_id: &str, output: &mut String) { + for edge in edges { + if edge.from == node_id { + if let Some(target) = graph.get_node(&edge.to) { + output.push_str(&format!( + "- **{:?}** → [{}]({})\n", + edge.edge_type, target.title, target.id + )); + } + } else if let Some(source) = graph.get_node(&edge.from) { + output.push_str(&format!( + "- [{}]({}) → **{:?}**\n", + source.title, source.id, edge.edge_type + )); + } + } +} + +/// Find related nodes prompt +async fn prompt_find_related(args: Value) -> Result> { + let topic = args["topic"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("Missing topic argument"))?; + let depth = args["depth"].as_u64().unwrap_or(1) as usize; + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let graph = storage.load_graph("default").await?; + + let mut related_text = format!("# Related Nodes for '{}'\n\n", topic); + + // Find the target node + let target_node = graph + .nodes + .values() + .find(|n| n.id == topic || n.title.to_lowercase().contains(&topic.to_lowercase())); + + if let Some(node) = target_node { + related_text.push_str(&format!("## Target Node: {}\n\n", node.title)); + related_text.push_str(&format!("**ID:** {}\n", node.id)); + related_text.push_str(&format!("**Type:** {:?}\n\n", node.node_type)); + + // Find direct relationships + related_text.push_str("## Direct Relationships\n\n"); + let related_edges: Vec<_> = graph + .edges + .iter() + .filter(|e| e.from == node.id || e.to == node.id) + .collect(); + + if related_edges.is_empty() { + related_text.push_str("No direct relationships found.\n\n"); + } else { + format_edge_relationships(&related_edges, &graph, &node.id, &mut related_text); + related_text.push('\n'); + } + + // If depth > 1, traverse further + if depth > 1 { + related_text.push_str("## Extended Relationships\n\n"); + related_text.push_str(&format!( + "(Traversing {} level(s) deep - feature coming soon)\n\n", + depth + )); + } + + // Find nodes with similar tags + if !node.tags.is_empty() { + related_text.push_str("## Nodes with Similar Tags\n\n"); + let similar: Vec<_> = graph + .nodes + .values() + .filter(|n| n.id != node.id && n.tags.iter().any(|tag| node.tags.contains(tag))) + .take(5) + .collect(); + + for similar_node in similar { + let common_tags: Vec = similar_node + .tags + .iter() + .filter(|tag| node.tags.contains(tag)) + .cloned() + .collect(); + related_text.push_str(&format!( + "- **{}** (common tags: {})\n", + similar_node.title, + common_tags.join(", ") + )); + } + } + } else { + // Topic search mode + related_text.push_str("## Search Results\n\n"); + let results: Vec<_> = graph + .nodes + .values() + .filter(|n| { + n.title.to_lowercase().contains(&topic.to_lowercase()) + || n.content.to_lowercase().contains(&topic.to_lowercase()) + || n.tags + .iter() + .any(|t| t.to_lowercase().contains(&topic.to_lowercase())) + }) + .take(10) + .collect(); + + if results.is_empty() { + related_text.push_str(&format!("No nodes found matching '{}'.\n", topic)); + } else { + for node in results { + related_text.push_str(&format!( + "- **{}** ({:?})\n ID: {}\n", + node.title, node.node_type, node.id + )); + } + } + } + + Ok(vec![PromptMessage { + role: "user".to_string(), + content: PromptContent::Text { text: related_text }, + }]) +} diff --git a/crates/kogral-mcp/src/resources.rs b/crates/kogral-mcp/src/resources.rs new file mode 100644 index 0000000..20a0952 --- /dev/null +++ b/crates/kogral-mcp/src/resources.rs @@ -0,0 +1,137 @@ +//! MCP resource providers + +use anyhow::Result; +use kogral_core::{ + models::NodeType, + storage::{filesystem::FilesystemStorage, Storage}, +}; +use tracing::info; + +use crate::types::{Resource, ResourceContents}; + +/// List available resources +pub async fn list_resources() -> Result> { + Ok(vec![ + Resource { + uri: "kogral://project/notes".to_string(), + name: "Project Notes".to_string(), + description: Some("All notes in the current project".to_string()), + mime_type: Some("text/markdown".to_string()), + }, + Resource { + uri: "kogral://project/decisions".to_string(), + name: "Project Decisions".to_string(), + description: Some("All architectural decisions in the current project".to_string()), + mime_type: Some("text/markdown".to_string()), + }, + Resource { + uri: "kogral://project/guidelines".to_string(), + name: "Project Guidelines".to_string(), + description: Some("All guidelines in the current project".to_string()), + mime_type: Some("text/markdown".to_string()), + }, + Resource { + uri: "kogral://project/patterns".to_string(), + name: "Project Patterns".to_string(), + description: Some("All design patterns in the current project".to_string()), + mime_type: Some("text/markdown".to_string()), + }, + Resource { + uri: "kogral://shared/guidelines".to_string(), + name: "Shared Guidelines".to_string(), + description: Some("Inherited base guidelines".to_string()), + mime_type: Some("text/markdown".to_string()), + }, + Resource { + uri: "kogral://shared/patterns".to_string(), + name: "Shared Patterns".to_string(), + description: Some("Inherited base patterns".to_string()), + mime_type: Some("text/markdown".to_string()), + }, + ]) +} + +/// Read resource contents +pub async fn read_resource(uri: &str) -> Result { + info!("Reading resource: {}", uri); + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let graph = storage.load_graph("default").await?; + + let (scope, node_type) = parse_uri(uri)?; + + let nodes: Vec<_> = graph + .nodes + .values() + .filter(|node| { + let type_match = match node_type.as_str() { + "notes" => node.node_type == NodeType::Note, + "decisions" => node.node_type == NodeType::Decision, + "guidelines" => node.node_type == NodeType::Guideline, + "patterns" => node.node_type == NodeType::Pattern, + _ => false, + }; + + let scope_match = match scope.as_str() { + "project" => node.project.is_some(), + "shared" => node.project.is_none(), + _ => true, + }; + + type_match && scope_match + }) + .collect(); + + let mut content = format!("# {}\n\n", get_title_for_uri(uri)); + content.push_str(&format!("Found {} node(s)\n\n", nodes.len())); + + for node in nodes { + content.push_str(&format!("## {}\n\n", node.title)); + content.push_str(&format!("**ID:** {}\n", node.id)); + content.push_str(&format!("**Type:** {:?}\n", node.node_type)); + content.push_str(&format!("**Status:** {:?}\n", node.status)); + content.push_str(&format!("**Created:** {}\n", node.created)); + + if !node.tags.is_empty() { + content.push_str(&format!("**Tags:** {}\n", node.tags.join(", "))); + } + + content.push_str(&format!("\n{}\n\n---\n\n", node.content)); + } + + Ok(ResourceContents { + uri: uri.to_string(), + mime_type: "text/markdown".to_string(), + text: content, + }) +} + +fn parse_uri(uri: &str) -> Result<(String, String)> { + if !uri.starts_with("kogral://") { + return Err(anyhow::anyhow!("Invalid URI scheme, expected kogral://")); + } + + let path = &uri[9..]; + let parts: Vec<&str> = path.split('/').collect(); + + if parts.len() != 2 { + return Err(anyhow::anyhow!( + "Invalid URI format, expected kogral:///" + )); + } + + Ok((parts[0].to_string(), parts[1].to_string())) +} + +fn get_title_for_uri(uri: &str) -> String { + match uri { + "kogral://project/notes" => "Project Notes", + "kogral://project/decisions" => "Project Architectural Decisions", + "kogral://project/guidelines" => "Project Guidelines", + "kogral://project/patterns" => "Project Design Patterns", + "kogral://shared/guidelines" => "Shared Guidelines", + "kogral://shared/patterns" => "Shared Design Patterns", + _ => "KOGRAL Resource", + } + .to_string() +} diff --git a/crates/kogral-mcp/src/server.rs b/crates/kogral-mcp/src/server.rs new file mode 100644 index 0000000..fb2cf50 --- /dev/null +++ b/crates/kogral-mcp/src/server.rs @@ -0,0 +1,224 @@ +//! MCP server implementation + +use std::io::{self, BufRead, BufReader, Write}; + +use anyhow::Result; +use serde_json::{json, Value}; +use tracing::{debug, error, info, warn}; + +use crate::{ + auth::{AuthConfig, extract_token_from_params}, + prompts, resources, tools, + types::{ + JsonRpcRequest, JsonRpcResponse, PromptsCapability, ResourcesCapability, + ServerCapabilities, ToolsCapability, + }, +}; + +/// MCP Server with authentication support +pub struct McpServer { + name: String, + version: String, + auth: AuthConfig, +} + +impl McpServer { + /// Create a new MCP server + pub fn new(name: String, version: String) -> Self { + let auth = AuthConfig::from_env(); + if auth.is_required() { + info!("MCP server authentication enabled via KOGRAL_MCP_TOKEN"); + } else { + info!("MCP server running without authentication"); + } + Self { name, version, auth } + } + + /// Run the MCP server with stdio transport + pub async fn run_stdio(&self) -> Result<()> { + info!("Starting MCP server: {} v{}", self.name, self.version); + info!("Transport: stdio"); + + let stdin = io::stdin(); + let mut reader = BufReader::new(stdin.lock()); + let mut stdout = io::stdout(); + + loop { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => { + // EOF + info!("Received EOF, shutting down"); + break; + } + Ok(_) => { + let line = line.trim(); + if line.is_empty() { + continue; + } + + debug!("Received request: {}", line); + + match self.handle_request(line).await { + Ok(response) => { + let response_json = serde_json::to_string(&response)?; + debug!("Sending response: {}", response_json); + writeln!(stdout, "{}", response_json)?; + stdout.flush()?; + } + Err(e) => { + error!("Error handling request: {}", e); + let error_response = JsonRpcResponse::error( + None, + -32603, + format!("Internal error: {}", e), + ); + let error_json = serde_json::to_string(&error_response)?; + writeln!(stdout, "{}", error_json)?; + stdout.flush()?; + } + } + } + Err(e) => { + error!("Error reading from stdin: {}", e); + break; + } + } + } + + Ok(()) + } + + /// Handle a JSON-RPC request + async fn handle_request(&self, request_json: &str) -> Result { + let request: JsonRpcRequest = serde_json::from_str(request_json)?; + + debug!("Handling method: {}", request.method); + + // Verify authentication for protected methods + // Initialize is allowed without auth for initial setup, ping can be used for health checks + if !matches!(request.method.as_str(), "initialize" | "ping") { + if let Err(e) = self.authenticate(&request.params) { + debug!("Authentication failed: {}", e); + return Ok(JsonRpcResponse::error( + request.id, + -32600, + format!("Unauthorized: {}", e), + )); + } + } + + let result = match request.method.as_str() { + "initialize" => self.handle_initialize(request.params).await, + "tools/list" => self.handle_tools_list().await, + "tools/call" => self.handle_tools_call(request.params).await, + "resources/list" => self.handle_resources_list().await, + "resources/read" => self.handle_resources_read(request.params).await, + "prompts/list" => self.handle_prompts_list().await, + "prompts/get" => self.handle_prompts_get(request.params).await, + "ping" => Ok(json!({"status": "ok"})), + _ => { + warn!("Unknown method: {}", request.method); + return Ok(JsonRpcResponse::error( + request.id, + -32601, + format!("Method not found: {}", request.method), + )); + } + }; + + match result { + Ok(value) => Ok(JsonRpcResponse::success(request.id, value)), + Err(e) => { + error!("Error executing method {}: {}", request.method, e); + Ok(JsonRpcResponse::error( + request.id, + -32603, + format!("Internal error: {}", e), + )) + } + } + } + + /// Authenticate a request by verifying its token + fn authenticate(&self, params: &Value) -> Result<()> { + let token = extract_token_from_params(params); + self.auth.verify(token)?; + Ok(()) + } + + /// Handle initialize request + async fn handle_initialize(&self, _params: Value) -> Result { + info!("Initializing MCP server"); + + Ok(json!({ + "protocolVersion": "2024-11-05", + "serverInfo": { + "name": self.name, + "version": self.version + }, + "capabilities": ServerCapabilities { + tools: Some(ToolsCapability { + list_changed: Some(false), + }), + resources: Some(ResourcesCapability { + subscribe: Some(false), + list_changed: Some(false), + }), + prompts: Some(PromptsCapability { + list_changed: Some(false), + }), + } + })) + } + + /// Handle tools/list request + async fn handle_tools_list(&self) -> Result { + let tools = tools::list_tools(); + Ok(json!({ "tools": tools })) + } + + /// Handle tools/call request + async fn handle_tools_call(&self, params: Value) -> Result { + let name = params["name"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("Missing tool name"))?; + let arguments = params["arguments"].clone(); + + let result = tools::execute_tool(name, arguments).await?; + Ok(serde_json::to_value(result)?) + } + + /// Handle resources/list request + async fn handle_resources_list(&self) -> Result { + let resources = resources::list_resources().await?; + Ok(json!({ "resources": resources })) + } + + /// Handle resources/read request + async fn handle_resources_read(&self, params: Value) -> Result { + let uri = params["uri"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("Missing resource URI"))?; + + let contents = resources::read_resource(uri).await?; + Ok(json!({ "contents": [contents] })) + } + + /// Handle prompts/list request + async fn handle_prompts_list(&self) -> Result { + let prompts = prompts::list_prompts(); + Ok(json!({ "prompts": prompts })) + } + + /// Handle prompts/get request + async fn handle_prompts_get(&self, params: Value) -> Result { + let name = params["name"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("Missing prompt name"))?; + let arguments = params["arguments"].clone(); + + let messages = prompts::execute_prompt(name, arguments).await?; + Ok(json!({ "messages": messages })) + } +} diff --git a/crates/kogral-mcp/src/tools.rs b/crates/kogral-mcp/src/tools.rs new file mode 100644 index 0000000..853755a --- /dev/null +++ b/crates/kogral-mcp/src/tools.rs @@ -0,0 +1,800 @@ +//! MCP tool handlers + +use anyhow::Result; +use kogral_core::{ + models::{Edge, EdgeType, Graph, Node, NodeType, TaskStatus}, + storage::{filesystem::FilesystemStorage, Storage}, +}; +use serde_json::{json, Value}; +use tracing::{error, info}; + +use crate::types::{Tool, ToolResult}; +use crate::validation::{ + validate_enum, validate_limit, validate_node_id, validate_optional_string, + validate_required_string, validate_strength, validate_string_array, MAX_ARRAY_ITEMS, + MAX_SHORT_LENGTH, MAX_STRING_LENGTH, +}; + +/// Get all available tools +pub fn list_tools() -> Vec { + vec![ + Tool { + name: "kogral/search".to_string(), + description: "Search KOGRAL using text and/or semantic similarity".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + }, + "type": { + "type": "string", + "enum": ["note", "decision", "guideline", "pattern", "journal", "execution", "all"], + "description": "Node type filter" + }, + "project": { + "type": "string", + "description": "Limit to project graph" + }, + "semantic": { + "type": "boolean", + "default": true, + "description": "Use semantic search (requires embeddings)" + }, + "limit": { + "type": "integer", + "default": 10, + "description": "Maximum number of results" + } + }, + "required": ["query"] + }), + }, + Tool { + name: "kogral/add_note".to_string(), + description: "Add a new note to KOGRAL".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Note title" + }, + "content": { + "type": "string", + "description": "Note content (markdown)" + }, + "tags": { + "type": "array", + "items": {"type": "string"}, + "description": "Tags" + }, + "relates_to": { + "type": "array", + "items": {"type": "string"}, + "description": "Related node IDs" + }, + "project": { + "type": "string", + "description": "Project identifier" + } + }, + "required": ["title", "content"] + }), + }, + Tool { + name: "kogral/add_decision".to_string(), + description: "Add an architectural decision record (ADR)".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Decision title" + }, + "context": { + "type": "string", + "description": "Problem context" + }, + "decision": { + "type": "string", + "description": "Decision made" + }, + "consequences": { + "type": "array", + "items": {"type": "string"}, + "description": "Consequences and impacts" + }, + "tags": { + "type": "array", + "items": {"type": "string"}, + "description": "Tags" + }, + "project": { + "type": "string", + "description": "Project identifier" + } + }, + "required": ["title", "decision"] + }), + }, + Tool { + name: "kogral/link".to_string(), + description: "Create a relationship between two nodes".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "Source node ID" + }, + "to": { + "type": "string", + "description": "Target node ID" + }, + "relation": { + "type": "string", + "enum": ["relates_to", "depends_on", "implements", "extends", "supersedes", "explains"], + "description": "Relationship type" + }, + "strength": { + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 1.0, + "description": "Relationship strength" + } + }, + "required": ["from", "to", "relation"] + }), + }, + Tool { + name: "kogral/get_guidelines".to_string(), + description: "Get guidelines for current project with inheritance resolution" + .to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "language": { + "type": "string", + "description": "Programming language (e.g., rust, nushell)" + }, + "category": { + "type": "string", + "description": "Category (e.g., error-handling, testing)" + }, + "include_base": { + "type": "boolean", + "default": true, + "description": "Include inherited base guidelines" + } + } + }), + }, + Tool { + name: "kogral/list_graphs".to_string(), + description: "List available knowledge graphs".to_string(), + input_schema: json!({ + "type": "object", + "properties": {} + }), + }, + Tool { + name: "kogral/export".to_string(), + description: "Export KOGRAL to external format".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "format": { + "type": "string", + "enum": ["logseq", "markdown", "json", "summary"], + "description": "Export format" + }, + "type": { + "type": "string", + "enum": ["note", "decision", "guideline", "pattern", "all"], + "description": "Node type filter" + } + }, + "required": ["format"] + }), + }, + Tool { + name: "kogral/find_blocks".to_string(), + description: "Find blocks by tag, task status, or custom property".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "tag": { + "type": "string", + "description": "Find blocks with this tag (e.g., 'card', 'important')" + }, + "status": { + "type": "string", + "enum": ["TODO", "DOING", "DONE", "LATER", "NOW", "WAITING", "CANCELLED"], + "description": "Find blocks with this task status" + }, + "property_key": { + "type": "string", + "description": "Custom property key" + }, + "property_value": { + "type": "string", + "description": "Custom property value" + }, + "limit": { + "type": "integer", + "default": 20, + "description": "Maximum number of blocks to return" + } + } + }), + }, + Tool { + name: "kogral/find_todos".to_string(), + description: "Find all TODO blocks across KOGRAL".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "description": "Maximum number of TODOs to return" + } + } + }), + }, + Tool { + name: "kogral/find_cards".to_string(), + description: "Find all flashcard blocks (#card tag) for spaced repetition".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "limit": { + "type": "integer", + "default": 30, + "description": "Maximum number of cards to return" + } + } + }), + }, + ] +} + +/// Execute a tool call +pub async fn execute_tool(name: &str, arguments: Value) -> Result { + info!("Executing tool: {} with args: {}", name, arguments); + + match name { + "kogral/search" => tool_search(arguments).await, + "kogral/add_note" => tool_add_note(arguments).await, + "kogral/add_decision" => tool_add_decision(arguments).await, + "kogral/link" => tool_link(arguments).await, + "kogral/get_guidelines" => tool_get_guidelines(arguments).await, + "kogral/list_graphs" => tool_list_graphs(arguments).await, + "kogral/export" => tool_export(arguments).await, + "kogral/find_blocks" => tool_find_blocks(arguments).await, + "kogral/find_todos" => tool_find_todos(arguments).await, + "kogral/find_cards" => tool_find_cards(arguments).await, + _ => { + error!("Unknown tool: {}", name); + Ok(ToolResult::error(format!("Unknown tool: {}", name))) + } + } +} + +/// Search knowledge base +async fn tool_search(args: Value) -> Result { + let query = validate_required_string(args["query"].as_str(), "query", MAX_STRING_LENGTH)?; + let limit = validate_limit(args["limit"].as_u64(), 10)?; + let node_type = validate_optional_string(args["type"].as_str(), "type", MAX_SHORT_LENGTH)?; + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let graph = storage.load_graph("default").await?; + + // Simple text search + let results: Vec<&Node> = graph + .nodes + .values() + .filter(|node| { + let matches_query = node.title.to_lowercase().contains(&query.to_lowercase()) + || node.content.to_lowercase().contains(&query.to_lowercase()) + || node + .tags + .iter() + .any(|t| t.to_lowercase().contains(&query.to_lowercase())); + + let matches_type = match node_type.as_deref() { + Some("note") => node.node_type == NodeType::Note, + Some("decision") => node.node_type == NodeType::Decision, + Some("guideline") => node.node_type == NodeType::Guideline, + Some("pattern") => node.node_type == NodeType::Pattern, + Some("journal") => node.node_type == NodeType::Journal, + Some("execution") => node.node_type == NodeType::Execution, + _ => true, + }; + + matches_query && matches_type + }) + .take(limit) + .collect(); + + let mut output = format!("Found {} result(s):\n\n", results.len()); + for (i, node) in results.iter().enumerate() { + output.push_str(&format!( + "{}. **{}** ({})\n Type: {:?} | Status: {:?}\n", + i + 1, + node.title, + node.id, + node.node_type, + node.status + )); + if !node.tags.is_empty() { + output.push_str(&format!(" Tags: {}\n", node.tags.join(", "))); + } + if !node.content.is_empty() { + let preview: String = node + .content + .lines() + .next() + .unwrap_or("") + .chars() + .take(100) + .collect(); + output.push_str(&format!(" {}\n", preview)); + } + output.push('\n'); + } + + Ok(ToolResult::text(output)) +} + +/// Add a note +async fn tool_add_note(args: Value) -> Result { + let title = validate_required_string(args["title"].as_str(), "title", MAX_STRING_LENGTH)?; + let content = validate_required_string(args["content"].as_str(), "content", MAX_STRING_LENGTH)?; + + let mut node = Node::new(NodeType::Note, title); + node.content = content; + + let tags = validate_string_array(args["tags"].as_array(), "tags", MAX_ARRAY_ITEMS, MAX_SHORT_LENGTH)?; + node.tags = tags; + + let relates_to = validate_string_array( + args["relates_to"].as_array(), + "relates_to", + MAX_ARRAY_ITEMS, + MAX_SHORT_LENGTH, + )?; + node.relates_to = relates_to; + + if let Some(project) = validate_optional_string(args["project"].as_str(), "project", MAX_SHORT_LENGTH)? { + node.project = Some(project); + } + + let mut storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let mut graph = storage + .load_graph("default") + .await + .unwrap_or_else(|_| Graph::new("default".to_string())); + + graph + .add_node(node.clone()) + .map_err(|e| anyhow::anyhow!(e))?; + storage.save_graph(&graph).await?; + + Ok(ToolResult::text(format!( + "✓ Note added successfully\n ID: {}\n Title: {}", + node.id, node.title + ))) +} + +/// Add an ADR +async fn tool_add_decision(args: Value) -> Result { + let title = validate_required_string(args["title"].as_str(), "title", MAX_STRING_LENGTH)?; + let decision = validate_required_string(args["decision"].as_str(), "decision", MAX_STRING_LENGTH)?; + + let mut node = Node::new(NodeType::Decision, title); + node.content = decision.clone(); + + // Store decision-specific data in metadata + let mut metadata = serde_json::Map::new(); + if let Some(context) = validate_optional_string(args["context"].as_str(), "context", MAX_STRING_LENGTH)? { + metadata.insert("context".to_string(), json!(context)); + } + metadata.insert("decision".to_string(), json!(decision)); + + let consequences = validate_string_array( + args["consequences"].as_array(), + "consequences", + MAX_ARRAY_ITEMS, + MAX_STRING_LENGTH, + )?; + if !consequences.is_empty() { + metadata.insert("consequences".to_string(), json!(consequences)); + } + + node.metadata = metadata.into_iter().collect(); + + let tags = validate_string_array(args["tags"].as_array(), "tags", MAX_ARRAY_ITEMS, MAX_SHORT_LENGTH)?; + node.tags = tags; + + if let Some(project) = validate_optional_string(args["project"].as_str(), "project", MAX_SHORT_LENGTH)? { + node.project = Some(project); + } + + let mut storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let mut graph = storage + .load_graph("default") + .await + .unwrap_or_else(|_| Graph::new("default".to_string())); + + graph + .add_node(node.clone()) + .map_err(|e| anyhow::anyhow!(e))?; + storage.save_graph(&graph).await?; + + Ok(ToolResult::text(format!( + "✓ Decision added successfully\n ID: {}\n Title: {}", + node.id, node.title + ))) +} + +/// Create a link between nodes +async fn tool_link(args: Value) -> Result { + let from = validate_node_id(args["from"].as_str(), "from")?; + let to = validate_node_id(args["to"].as_str(), "to")?; + let relation_str = validate_enum( + args["relation"].as_str(), + "relation", + &["relates_to", "depends_on", "implements", "extends", "supersedes", "explains"], + )?; + let strength = validate_strength(args["strength"].as_f64())?; + + let relation = match relation_str.as_str() { + "relates_to" => EdgeType::RelatesTo, + "depends_on" => EdgeType::DependsOn, + "implements" => EdgeType::Implements, + "extends" => EdgeType::Extends, + "supersedes" => EdgeType::Supersedes, + "explains" => EdgeType::Explains, + _ => unreachable!("validate_enum should have prevented invalid relation"), + }; + + let mut storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let mut graph = storage.load_graph("default").await?; + + let mut edge = Edge::new(from.clone(), to.clone(), relation); + edge.strength = strength; + + graph.add_edge(edge).map_err(|e| anyhow::anyhow!(e))?; + storage.save_graph(&graph).await?; + + Ok(ToolResult::text(format!( + "✓ Relationship created: {} --{:?}--> {}", + from, relation, to + ))) +} + +/// Get guidelines +async fn tool_get_guidelines(args: Value) -> Result { + let language = validate_optional_string(args["language"].as_str(), "language", MAX_SHORT_LENGTH)?; + let category = validate_optional_string(args["category"].as_str(), "category", MAX_SHORT_LENGTH)?; + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let graph = storage.load_graph("default").await?; + + let guidelines: Vec<&Node> = graph + .nodes + .values() + .filter(|node| { + if node.node_type != NodeType::Guideline { + return false; + } + + let lang_match = language + .as_ref() + .map(|l| { + node.metadata + .get("language") + .and_then(|v| v.as_str()) + .map(|lang| lang == l) + .unwrap_or(false) + }) + .unwrap_or(true); + + let cat_match = category + .as_ref() + .map(|c| { + node.metadata + .get("category") + .and_then(|v| v.as_str()) + .map(|cat| cat == c) + .unwrap_or(false) + }) + .unwrap_or(true); + + lang_match && cat_match + }) + .collect(); + + let mut output = format!("Found {} guideline(s):\n\n", guidelines.len()); + for guideline in guidelines { + output.push_str(&format!("## {}\n", guideline.title)); + if let Some(lang) = guideline.metadata.get("language").and_then(|v| v.as_str()) { + output.push_str(&format!("**Language:** {}\n", lang)); + } + if let Some(cat) = guideline.metadata.get("category").and_then(|v| v.as_str()) { + output.push_str(&format!("**Category:** {}\n", cat)); + } + output.push_str(&format!("\n{}\n\n---\n\n", guideline.content)); + } + + Ok(ToolResult::text(output)) +} + +/// List available graphs +async fn tool_list_graphs(_args: Value) -> Result { + // For now, return default graph + Ok(ToolResult::text("Available graphs:\n- default".to_string())) +} + +/// Export knowledge base +async fn tool_export(args: Value) -> Result { + let format = validate_enum( + args["format"].as_str(), + "format", + &["logseq", "markdown", "json", "summary"], + )?; + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let graph = storage.load_graph("default").await?; + + match format.as_str() { + "json" => { + let json = serde_json::to_string_pretty(&graph)?; + Ok(ToolResult::text(json)) + } + "summary" => { + let mut output = "# Knowledge Base Summary\n\n".to_string(); + output.push_str(&format!("**Graph:** {}\n", graph.name)); + output.push_str(&format!("**Nodes:** {}\n", graph.nodes.len())); + output.push_str(&format!("**Edges:** {}\n\n", graph.edges.len())); + + output.push_str("## Nodes by Type\n\n"); + let mut type_counts = std::collections::HashMap::new(); + for node in graph.nodes.values() { + *type_counts + .entry(format!("{:?}", node.node_type)) + .or_insert(0) += 1; + } + for (node_type, count) in type_counts { + output.push_str(&format!("- **{}:** {}\n", node_type, count)); + } + + Ok(ToolResult::text(output)) + } + _ => unreachable!("validate_enum should have prevented invalid format"), + } +} + +/// Find blocks by tag, status, or property +#[allow(clippy::excessive_nesting)] +async fn tool_find_blocks(args: Value) -> Result { + let tag = validate_optional_string(args["tag"].as_str(), "tag", MAX_SHORT_LENGTH)?; + let status = validate_optional_string(args["status"].as_str(), "status", MAX_SHORT_LENGTH)?; + let property_key = validate_optional_string(args["property_key"].as_str(), "property_key", MAX_SHORT_LENGTH)?; + let property_value = validate_optional_string(args["property_value"].as_str(), "property_value", MAX_STRING_LENGTH)?; + let limit = validate_limit(args["limit"].as_u64(), 20)?; + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let mut graph = storage.load_graph("default").await?; + + let mut output = String::new(); + let mut total_blocks = 0; + + // Find by tag + if let Some(tag_name) = &tag { + // First, extract all block info and node IDs + let block_results = graph.find_blocks_by_tag(tag_name); + let mut node_blocks: Vec<(String, Vec)> = Vec::new(); + + for (node_id, blocks) in block_results { + let contents: Vec = blocks.iter().map(|b| b.content.clone()).collect(); + node_blocks.push((node_id, contents)); + } + + // Now access graph to get node titles + output.push_str(&format!("## Blocks with tag #{}:\n\n", tag_name)); + for (node_id, contents) in node_blocks.into_iter().take(limit) { + if let Some(node) = graph.get_node(&node_id) { + output.push_str(&format!("**{}** ({})\n", node.title, node_id)); + for content in contents { + output.push_str(&format!(" - {}\n", content)); + total_blocks += 1; + } + output.push('\n'); + } + } + } + + // Find by status + if let Some(status_str) = &status { + let task_status = match status_str.as_str() { + "TODO" => Some(TaskStatus::Todo), + "DOING" => Some(TaskStatus::Doing), + "DONE" => Some(TaskStatus::Done), + "LATER" => Some(TaskStatus::Later), + "NOW" => Some(TaskStatus::Now), + "WAITING" => Some(TaskStatus::Waiting), + "CANCELLED" => Some(TaskStatus::Cancelled), + _ => None, + }; + + if let Some(task_status_value) = task_status { + let block_results = graph.find_blocks_by_status(task_status_value); + let mut node_blocks: Vec<(String, Vec)> = Vec::new(); + + for (node_id, blocks) in block_results { + let contents: Vec = blocks.iter().map(|b| b.content.clone()).collect(); + node_blocks.push((node_id, contents)); + } + + output.push_str(&format!("## Blocks with status {}:\n\n", status_str)); + for (node_id, contents) in node_blocks.into_iter().take(limit) { + if let Some(node) = graph.get_node(&node_id) { + output.push_str(&format!("**{}** ({})\n", node.title, node_id)); + for content in contents { + output.push_str(&format!(" - {} {}\n", status_str, content)); + total_blocks += 1; + } + output.push('\n'); + } + } + } + } + + // Find by custom property + if let (Some(key), Some(value)) = (&property_key, &property_value) { + let block_results = graph.find_blocks_by_property(key, value); + let mut node_blocks: Vec<(String, Vec)> = Vec::new(); + + for (node_id, blocks) in block_results { + let contents: Vec = blocks.iter().map(|b| b.content.clone()).collect(); + node_blocks.push((node_id, contents)); + } + + output.push_str(&format!("## Blocks with {}:: {}:\n\n", key, value)); + for (node_id, contents) in node_blocks.into_iter().take(limit) { + if let Some(node) = graph.get_node(&node_id) { + output.push_str(&format!("**{}** ({})\n", node.title, node_id)); + for content in contents { + output.push_str(&format!(" - {}\n", content)); + total_blocks += 1; + } + output.push('\n'); + } + } + } + + if total_blocks == 0 { + output = "No blocks found matching criteria.".to_string(); + } else { + output = format!("Found {} block(s)\n\n{}", total_blocks, output); + } + + Ok(ToolResult::text(output)) +} + +/// Find all TODO blocks +async fn tool_find_todos(args: Value) -> Result { + let limit = validate_limit(args["limit"].as_u64(), 50)?; + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let mut graph = storage.load_graph("default").await?; + + let todo_results = graph.find_all_todos(); + let mut node_blocks: Vec<(String, Vec<(String, String)>)> = Vec::new(); + + for (node_id, blocks) in todo_results { + let block_data: Vec<(String, String)> = blocks + .iter() + .map(|b| { + let status = b + .properties + .status + .map(|s| s.to_string()) + .unwrap_or_else(|| "TODO".to_string()); + (status, b.content.clone()) + }) + .collect(); + node_blocks.push((node_id, block_data)); + } + + let total_todos: usize = node_blocks.iter().map(|(_, blocks)| blocks.len()).sum(); + let mut output = format!("Found {} TODO block(s):\n\n", total_todos); + + let mut count = 0; + for (node_id, blocks) in node_blocks { + if count >= limit { + break; + } + + if let Some(node) = graph.get_node(&node_id) { + output.push_str(&format!("## {} ({})\n", node.title, node_id)); + + for (status, content) in blocks { + if count >= limit { + break; + } + output.push_str(&format!("- {} {}\n", status, content)); + count += 1; + } + output.push('\n'); + } + } + + if total_todos == 0 { + output = "No TODO blocks found.".to_string(); + } + + Ok(ToolResult::text(output)) +} + +/// Find all flashcard blocks (#card) +async fn tool_find_cards(args: Value) -> Result { + let limit = validate_limit(args["limit"].as_u64(), 30)?; + + let storage = FilesystemStorage::new(std::path::PathBuf::from(".kogral")); + let mut graph = storage.load_graph("default").await?; + + let card_results = graph.find_blocks_by_tag("card"); + let mut node_blocks: Vec<(String, Vec<(String, Vec)>)> = Vec::new(); + + for (node_id, blocks) in card_results { + let card_data: Vec<(String, Vec)> = blocks + .iter() + .map(|b| { + let children: Vec = b.children.iter().map(|c| c.content.clone()).collect(); + (b.content.clone(), children) + }) + .collect(); + node_blocks.push((node_id, card_data)); + } + + let total_cards: usize = node_blocks.iter().map(|(_, cards)| cards.len()).sum(); + let mut output = format!("Found {} flashcard(s):\n\n", total_cards); + + let mut count = 0; + for (node_id, cards) in node_blocks { + if count >= limit { + break; + } + + if let Some(node) = graph.get_node(&node_id) { + output.push_str(&format!("## {} ({})\n", node.title, node_id)); + + for (content, children) in cards { + if count >= limit { + break; + } + output.push_str(&format!("- {} #card\n", content)); + + // Show nested blocks (answer part of flashcard) + for child in children { + output.push_str(&format!(" - {}\n", child)); + } + + count += 1; + } + output.push('\n'); + } + } + + if total_cards == 0 { + output = "No flashcards (#card) found.".to_string(); + } + + Ok(ToolResult::text(output)) +} diff --git a/crates/kogral-mcp/src/types.rs b/crates/kogral-mcp/src/types.rs new file mode 100644 index 0000000..57b5466 --- /dev/null +++ b/crates/kogral-mcp/src/types.rs @@ -0,0 +1,191 @@ +//! MCP protocol types + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// MCP JSON-RPC request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcRequest { + pub jsonrpc: String, + pub id: Option, + pub method: String, + #[serde(default)] + pub params: Value, +} + +/// MCP JSON-RPC response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcResponse { + pub jsonrpc: String, + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// MCP JSON-RPC error +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JsonRpcError { + pub code: i32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +/// MCP tool definition +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Tool { + pub name: String, + pub description: String, + #[serde(rename = "inputSchema")] + pub input_schema: Value, +} + +/// MCP tool call request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolCall { + pub name: String, + #[serde(default)] + pub arguments: Value, +} + +/// MCP tool result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolResult { + pub content: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_error: Option, +} + +/// Content block in tool result +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum ContentBlock { + Text { text: String }, +} + +/// MCP resource definition +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Resource { + pub uri: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, +} + +/// MCP resource contents +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResourceContents { + pub uri: String, + pub mime_type: String, + pub text: String, +} + +/// MCP prompt definition +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Prompt { + pub name: String, + pub description: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub arguments: Option>, +} + +/// Prompt argument definition +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PromptArgument { + pub name: String, + pub description: String, + pub required: bool, +} + +/// Prompt message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PromptMessage { + pub role: String, + pub content: PromptContent, +} + +/// Prompt content +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum PromptContent { + Text { text: String }, +} + +/// Server capabilities +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerCapabilities { + #[serde(skip_serializing_if = "Option::is_none")] + pub tools: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub resources: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub prompts: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolsCapability { + #[serde(skip_serializing_if = "Option::is_none")] + pub list_changed: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResourcesCapability { + #[serde(skip_serializing_if = "Option::is_none")] + pub subscribe: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub list_changed: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PromptsCapability { + #[serde(skip_serializing_if = "Option::is_none")] + pub list_changed: Option, +} + +impl JsonRpcResponse { + /// Create success response + pub fn success(id: Option, result: Value) -> Self { + Self { + jsonrpc: "2.0".to_string(), + id, + result: Some(result), + error: None, + } + } + + /// Create error response + pub fn error(id: Option, code: i32, message: String) -> Self { + Self { + jsonrpc: "2.0".to_string(), + id, + result: None, + error: Some(JsonRpcError { + code, + message, + data: None, + }), + } + } +} + +impl ToolResult { + /// Create text result + pub fn text(text: String) -> Self { + Self { + content: vec![ContentBlock::Text { text }], + is_error: None, + } + } + + /// Create error result + pub fn error(message: String) -> Self { + Self { + content: vec![ContentBlock::Text { text: message }], + is_error: Some(true), + } + } +} diff --git a/crates/kogral-mcp/src/validation.rs b/crates/kogral-mcp/src/validation.rs new file mode 100644 index 0000000..43735f2 --- /dev/null +++ b/crates/kogral-mcp/src/validation.rs @@ -0,0 +1,219 @@ +//! Input validation for MCP tools +//! +//! Enforces safe limits on user inputs to prevent abuse and resource exhaustion. + +use anyhow::{anyhow, Result}; +use serde_json::Value; + +/// Maximum length for string fields (titles, content, etc.) +pub const MAX_STRING_LENGTH: usize = 10_000; + +/// Maximum length for short fields (IDs, names, etc.) +pub const MAX_SHORT_LENGTH: usize = 256; + +/// Maximum number of items in array fields (tags, relates_to, etc.) +pub const MAX_ARRAY_ITEMS: usize = 100; + +/// Maximum limit value for result sets +pub const MAX_LIMIT: usize = 1000; + +/// Validates and sanitizes a required string field +pub fn validate_required_string( + value: Option<&str>, + field_name: &str, + max_length: usize, +) -> Result { + let s = value.ok_or_else(|| anyhow!("Missing required field: {}", field_name))?; + + if s.is_empty() { + return Err(anyhow!("Field '{}' cannot be empty", field_name)); + } + + if s.len() > max_length { + return Err(anyhow!( + "Field '{}' exceeds maximum length of {} characters", + field_name, + max_length + )); + } + + Ok(s.trim().to_string()) +} + +/// Validates an optional string field +pub fn validate_optional_string(value: Option<&str>, field_name: &str, max_length: usize) -> Result> { + match value { + Some(s) => { + if s.is_empty() { + return Ok(None); + } + + if s.len() > max_length { + return Err(anyhow!( + "Field '{}' exceeds maximum length of {} characters", + field_name, + max_length + )); + } + + Ok(Some(s.trim().to_string())) + } + None => Ok(None), + } +} + +/// Validates a limit parameter (for pagination) +pub fn validate_limit(value: Option, default: usize) -> Result { + match value { + Some(limit) => { + if limit == 0 { + return Err(anyhow!("Limit must be greater than 0")); + } + if limit as usize > MAX_LIMIT { + return Err(anyhow!("Limit cannot exceed {}", MAX_LIMIT)); + } + Ok(limit as usize) + } + None => Ok(default), + } +} + +/// Validates a numeric strength value (0.0 to 1.0) +pub fn validate_strength(value: Option) -> Result { + let strength = value.unwrap_or(1.0); + if !(0.0..=1.0).contains(&strength) { + return Err(anyhow!("Strength must be between 0 and 1, got {}", strength)); + } + Ok(strength as f32) +} + +/// Validates an array of string items +pub fn validate_string_array( + value: Option<&Vec>, + field_name: &str, + max_items: usize, + max_item_length: usize, +) -> Result> { + match value { + Some(arr) => { + if arr.len() > max_items { + return Err(anyhow!( + "Field '{}' cannot have more than {} items", + field_name, + max_items + )); + } + + arr.iter() + .enumerate() + .map(|(i, v)| { + let s = v + .as_str() + .ok_or_else(|| anyhow!("Item {} in '{}' is not a string", i, field_name))?; + + if s.is_empty() { + return Err(anyhow!("Item {} in '{}' cannot be empty", i, field_name)); + } + + if s.len() > max_item_length { + return Err(anyhow!( + "Item {} in '{}' exceeds maximum length of {} characters", + i, + field_name, + max_item_length + )); + } + + Ok(s.trim().to_string()) + }) + .collect() + } + None => Ok(Vec::new()), + } +} + +/// Validates a node ID reference (must exist is checked at call site) +pub fn validate_node_id(value: Option<&str>, field_name: &str) -> Result { + validate_required_string(value, field_name, MAX_SHORT_LENGTH) +} + +/// Validates an enum-like field against allowed values +pub fn validate_enum(value: Option<&str>, field_name: &str, allowed: &[&str]) -> Result { + let s = validate_required_string(value, field_name, MAX_SHORT_LENGTH)?; + + if !allowed.contains(&s.as_str()) { + return Err(anyhow!( + "Invalid value for '{}': '{}'. Must be one of: {}", + field_name, + s, + allowed.join(", ") + )); + } + + Ok(s) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_required_string() { + let result = validate_required_string(Some("hello"), "test", 100); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "hello"); + + let result = validate_required_string(None, "test", 100); + assert!(result.is_err()); + + let result = validate_required_string(Some(""), "test", 100); + assert!(result.is_err()); + + let long_string = "a".repeat(101); + let result = validate_required_string(Some(&long_string), "test", 100); + assert!(result.is_err()); + } + + #[test] + fn test_validate_limit() { + let result = validate_limit(Some(10), 5); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 10); + + let result = validate_limit(None, 5); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 5); + + let result = validate_limit(Some(0), 5); + assert!(result.is_err()); + + let result = validate_limit(Some((MAX_LIMIT + 1) as u64), 5); + assert!(result.is_err()); + } + + #[test] + fn test_validate_strength() { + let result = validate_strength(Some(0.5)); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 0.5); + + let result = validate_strength(None); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1.0); + + let result = validate_strength(Some(1.5)); + assert!(result.is_err()); + + let result = validate_strength(Some(-0.1)); + assert!(result.is_err()); + } + + #[test] + fn test_validate_enum() { + let result = validate_enum(Some("note"), "type", &["note", "decision", "guideline"]); + assert!(result.is_ok()); + + let result = validate_enum(Some("invalid"), "type", &["note", "decision"]); + assert!(result.is_err()); + } +} diff --git a/crates/kogral-mcp/tests/integration_test.rs b/crates/kogral-mcp/tests/integration_test.rs new file mode 100644 index 0000000..d83a286 --- /dev/null +++ b/crates/kogral-mcp/tests/integration_test.rs @@ -0,0 +1,60 @@ +//! Integration tests for kb-mcp + +use kogral_mcp::{prompts, resources, tools}; +use serde_json::json; + +#[tokio::test] +async fn test_list_tools() { + let tools = tools::list_tools(); + assert!(!tools.is_empty()); + assert!(tools.iter().any(|t| t.name == "kogral/search")); + assert!(tools.iter().any(|t| t.name == "kogral/add_note")); + assert!(tools.iter().any(|t| t.name == "kogral/add_decision")); +} + +#[tokio::test] +async fn test_list_resources() { + let resources = resources::list_resources().await.unwrap(); + assert!(!resources.is_empty()); + assert!(resources.iter().any(|r| r.uri == "kogral://project/notes")); + assert!(resources + .iter() + .any(|r| r.uri == "kogral://shared/guidelines")); +} + +#[test] +fn test_list_prompts() { + let prompts = prompts::list_prompts(); + assert!(!prompts.is_empty()); + assert!(prompts.iter().any(|p| p.name == "kogral/summarize_project")); + assert!(prompts.iter().any(|p| p.name == "kogral/find_related")); +} + +#[tokio::test] +async fn test_execute_search_tool() { + let args = json!({ + "query": "test", + "limit": 5 + }); + + // This test expects the tool to execute even if no graph exists + // The tool should return an empty result or error gracefully + let result = tools::execute_tool("kogral/search", args).await; + + // Tool execution should succeed (even with no results) + // The actual search may fail if graph doesn't exist, but tool dispatch should + // work + match result { + Ok(_) => assert!(true), + Err(e) => { + // If it errors, it should be due to missing graph, not a tool execution error + assert!(e.to_string().contains("not found") || e.to_string().contains("Graph")); + } + } +} + +#[tokio::test] +async fn test_execute_list_graphs_tool() { + let result = tools::execute_tool("kogral/list_graphs", json!({})).await; + assert!(result.is_ok()); +}