commit f820a72b046e458eba18da6b7aa4b3a5e5758add Author: Harald Hoyer Date: Tue Mar 3 01:21:17 2026 +0100 Initial implementation of vault-os Complete implementation across all 13 phases: - vault-core: types, YAML frontmatter parsing, entity classification, filesystem ops, config, prompt composition, validation, search - vault-watch: filesystem watcher with daemon write filtering, event classification - vault-scheduler: cron engine, process executor, task runner with retry logic and concurrency limiting - vault-api: Axum REST API (15 route modules), WebSocket with broadcast, AI assistant proxy, validation, templates - Dashboard: React + TypeScript + Tailwind v4 with kanban, CodeMirror editor, dynamic view system, AI chat sidebar - Nix flake with dev shell and NixOS module - Graceful shutdown, inotify overflow recovery, tracing instrumentation Co-Authored-By: Claude Opus 4.6 diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d648fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target +.vault/ +.direnv/ +*.swp +*.swo +*~ +.DS_Store +dashboard/node_modules/ +dashboard/dist/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5cf41dc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2783 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[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 = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[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", +] + +[[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 = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[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" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +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 = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cron" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" +dependencies = [ + "chrono", + "once_cell", + "winnow", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "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 = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[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 = "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 = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[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 = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[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", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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 = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[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 = "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", +] + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "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.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[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 = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[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 = "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 = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.11.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 = "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 = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[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 = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[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 = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[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 = "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 = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] + +[[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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[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 = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.11.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", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[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 = "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 = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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 = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags 2.11.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 = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +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 = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "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 = [ + "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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[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_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", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "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_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "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 = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[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-sink", + "pin-project-lite", + "tokio", +] + +[[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", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[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 = [ + "log", + "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", +] + +[[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-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 = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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 = "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 = "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 = "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.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vault-api" +version = "0.1.0" +dependencies = [ + "axum", + "chrono", + "futures-util", + "pulldown-cmark", + "reqwest", + "rust-embed", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "tokio", + "tower", + "tower-http", + "tracing", + "uuid", + "vault-core", + "vault-scheduler", + "vault-watch", +] + +[[package]] +name = "vault-core" +version = "0.1.0" +dependencies = [ + "chrono", + "cron", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "vault-os" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "clap", + "tokio", + "tracing", + "tracing-subscriber", + "vault-api", + "vault-core", + "vault-scheduler", + "vault-watch", +] + +[[package]] +name = "vault-scheduler" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "cron", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "tokio", + "tracing", + "uuid", + "vault-core", + "vault-watch", +] + +[[package]] +name = "vault-watch" +version = "0.1.0" +dependencies = [ + "notify", + "thiserror", + "tokio", + "tracing", + "vault-core", +] + +[[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 = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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 = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[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", +] + +[[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", +] + +[[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", + "windows-strings", +] + +[[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.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.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[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", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", + "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", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f51ede6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[workspace] +members = ["crates/*"] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Harald Hoyer "] + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +serde_yaml = "0.9" +serde_json = "1" +chrono = { version = "0.4", features = ["serde"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +thiserror = "2" +anyhow = "1" +uuid = { version = "1", features = ["v4", "serde"] } +tokio = { version = "1", features = ["full"] } +axum = { version = "0.8", features = ["ws"] } +tower = "0.5" +tower-http = { version = "0.6", features = ["cors", "trace", "fs"] } +notify = "8" +cron = "0.15" +async-trait = "0.1" +reqwest = { version = "0.12", features = ["json"] } +clap = { version = "4", features = ["derive", "env"] } +rust-embed = "8" +pulldown-cmark = "0.12" + +vault-core = { path = "crates/vault-core" } +vault-watch = { path = "crates/vault-watch" } +vault-scheduler = { path = "crates/vault-scheduler" } +vault-api = { path = "crates/vault-api" } + +[package] +name = "vault-os" +version.workspace = true +edition.workspace = true + +[dependencies] +vault-core.workspace = true +vault-watch.workspace = true +vault-scheduler.workspace = true +vault-api.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +clap.workspace = true +anyhow.workspace = true +axum.workspace = true +chrono.workspace = true diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..fb2ef21 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2026 Harald Hoyer + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..54378ee --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Harald Hoyer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..503d747 --- /dev/null +++ b/README.md @@ -0,0 +1,260 @@ +# vault-os + +A personal AI operations platform — a single Rust daemon that turns a directory of markdown files with YAML frontmatter into a reactive system: knowledge base, task manager, agent orchestrator, cron scheduler, and web dashboard. + +**Everything is markdown.** Status is directory-based. The filesystem is the database. + +## Architecture + +``` +vault-os (binary) +├── vault-core # Types, frontmatter parsing, filesystem ops, validation, search +├── vault-watch # Filesystem watcher (notify/inotify) with daemon write filtering +├── vault-scheduler # Cron engine, process executor, task runner with retry logic +└── vault-api # Axum REST API, WebSocket, embedded dashboard +``` + +The daemon runs three concurrent event sources via `tokio::select!`: +- **Filesystem events** — file changes trigger cache updates, task execution, cron rescheduling +- **Cron timer** — fires scheduled agent tasks at the right time +- **Shutdown signal** — graceful shutdown waiting for running tasks + +## Vault Directory Structure + +``` +vault/ +├── agents/ # Agent definitions +├── skills/ # Reusable skill modules +├── crons/ +│ ├── active/ # Enabled cron jobs +│ ├── paused/ # Disabled cron jobs +│ └── templates/ # Cron templates +├── todos/ +│ ├── harald/ # Human tasks +│ │ ├── urgent/ +│ │ ├── open/ +│ │ ├── in-progress/ +│ │ └── done/ +│ └── agent/ # Agent task queue +│ ├── queued/ +│ ├── running/ +│ ├── done/ +│ └── failed/ +├── knowledge/ # Free-form knowledge base +├── views/ # Dashboard view definitions +│ ├── pages/ +│ ├── widgets/ +│ ├── layouts/ +│ └── notifications/ +└── .vault/ # Daemon state (git-ignored) + ├── config.yaml + └── state.json +``` + +## Building + +### Prerequisites + +**With Nix (recommended):** + +```sh +nix develop +``` + +This gives you Rust (stable, latest), rust-analyzer, clippy, Node.js 22, npm, and cargo-watch. + +**Without Nix:** + +- Rust stable (1.75+) +- Node.js 22+ +- npm +- pkg-config, openssl (on Linux) + +### Build the daemon + +```sh +cargo build --release +``` + +### Build the dashboard + +```sh +cd dashboard +npm install +npm run build +``` + +### Run tests + +```sh +cargo test --workspace +``` + +### Run clippy + +```sh +cargo clippy --workspace +``` + +## Usage + +### Start the daemon + +```sh +vault-os --vault /path/to/your/vault +``` + +The daemon creates the directory structure automatically on first run. + +### CLI Options + +| Flag | Env Var | Default | Description | +|------|---------|---------|-------------| +| `--vault ` | `VAULT_PATH` | (required) | Path to vault directory | +| `--port ` | `VAULT_PORT` | `8080` | HTTP/WebSocket port | +| `--bind ` | `VAULT_BIND` | `127.0.0.1` | Bind address | +| `--max-parallel ` | `VAULT_MAX_PARALLEL` | `4` | Max concurrent agent executions | +| `--log-level ` | `VAULT_LOG_LEVEL` | `info` | Log level (trace/debug/info/warn/error) | + +### Access the dashboard + +Open `http://localhost:8080` in your browser. + +For development with hot-reload: + +```sh +cd dashboard +npm run dev +``` + +The Vite dev server proxies `/api` and `/ws` to the Rust daemon on port 8080. + +## File Format + +Every entity is a markdown file with YAML frontmatter. Example agent: + +```markdown +--- +name: reviewer +executable: claude-code +model: claude-sonnet-4-20250514 +skills: + - read-vault + - github-pr-review +timeout: 600 +max_retries: 1 +env: + GITHUB_TOKEN: ${GITHUB_TOKEN} +--- + +You are a code reviewer. Review pull requests thoroughly, +focusing on correctness, security, and maintainability. +``` + +Example cron job: + +```markdown +--- +title: Daily Inbox Review +schedule: "0 9 * * *" +agent: reviewer +enabled: true +--- + +Review all open PRs and summarize findings. +``` + +Example human task (in `todos/harald/open/`): + +```markdown +--- +title: Fix login bug +priority: high +labels: [bug, auth] +created: 2026-03-01T10:00:00Z +--- + +The login form throws a 500 when the email contains a plus sign. +``` + +## API + +The REST API is available at `/api`. Key endpoints: + +| Endpoint | Description | +|----------|-------------| +| `GET /api/agents` | List all agents | +| `POST /api/agents/:name/trigger` | Trigger an agent | +| `GET /api/crons` | List cron jobs | +| `POST /api/crons/:name/trigger` | Fire a cron manually | +| `GET /api/todos/harald` | List human tasks | +| `PATCH /api/todos/harald/:status/:id/move` | Move task between statuses | +| `GET /api/todos/agent` | List agent tasks | +| `GET /api/knowledge` | Search knowledge base | +| `GET/PUT/DELETE /api/files/*path` | Generic file CRUD | +| `GET /api/tree` | Vault directory tree | +| `GET /api/stats` | Vault statistics | +| `GET /api/health` | Health check | +| `POST /api/assistant/chat` | AI assistant chat | +| `POST /api/validate` | Validate a vault file | +| `GET /api/templates` | List entity templates | +| `WS /ws` | Real-time events | + +## Configuration + +Create `.vault/config.yaml` in your vault root: + +```yaml +# Agent executors +executors: + ollama: + base_url: http://localhost:11434 + +# Task queue settings +queue: + max_parallel: 4 + default_timeout: 600 + retry_delay: 60 + +# Inline AI assistant +assistant: + default_model: local/qwen3 + models: + - local/qwen3 + - claude-sonnet-4-20250514 +``` + +Set `ANTHROPIC_API_KEY` in your environment to use Claude models with the assistant. + +## NixOS Deployment + +Add to your NixOS configuration: + +```nix +{ + inputs.vault-os.url = "github:you/vault-os"; + + # In your configuration.nix: + imports = [ vault-os.nixosModules.default ]; + + services.vault-os = { + enable = true; + vaultPath = "/var/lib/vault-os"; + port = 8080; + bind = "127.0.0.1"; + maxParallel = 4; + environmentFile = "/run/secrets/vault-os.env"; # for API keys + }; +} +``` + +The systemd service runs with hardened settings (NoNewPrivileges, ProtectSystem=strict, PrivateTmp, etc.). + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE)) +- MIT License ([LICENSE-MIT](LICENSE-MIT)) + +at your option. diff --git a/crates/vault-api/Cargo.toml b/crates/vault-api/Cargo.toml new file mode 100644 index 0000000..45daa61 --- /dev/null +++ b/crates/vault-api/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "vault-api" +version.workspace = true +edition.workspace = true + +[dependencies] +vault-core.workspace = true +vault-watch.workspace = true +vault-scheduler.workspace = true +axum.workspace = true +tower.workspace = true +tower-http.workspace = true +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +tracing.workspace = true +thiserror.workspace = true +rust-embed.workspace = true +pulldown-cmark.workspace = true +uuid.workspace = true +chrono.workspace = true +futures-util = "0.3" +reqwest.workspace = true diff --git a/crates/vault-api/src/error.rs b/crates/vault-api/src/error.rs new file mode 100644 index 0000000..a8b6fde --- /dev/null +++ b/crates/vault-api/src/error.rs @@ -0,0 +1,44 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use serde_json::json; + +#[derive(Debug, thiserror::Error)] +pub enum ApiError { + #[error("Not found: {0}")] + NotFound(String), + + #[error("Bad request: {0}")] + BadRequest(String), + + #[error("Internal error: {0}")] + Internal(String), + + #[error("Vault error: {0}")] + Vault(#[from] vault_core::VaultError), +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let (status, message) = match &self { + ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), + ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), + ApiError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()), + ApiError::Vault(e) => match e { + vault_core::VaultError::NotFound(msg) => { + (StatusCode::NOT_FOUND, msg.clone()) + } + vault_core::VaultError::MissingFrontmatter(p) => { + (StatusCode::BAD_REQUEST, format!("Missing frontmatter: {:?}", p)) + } + _ => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + }, + }; + + let body = json!({ + "error": message, + "status": status.as_u16(), + }); + + (status, axum::Json(body)).into_response() + } +} diff --git a/crates/vault-api/src/lib.rs b/crates/vault-api/src/lib.rs new file mode 100644 index 0000000..6f0d6a2 --- /dev/null +++ b/crates/vault-api/src/lib.rs @@ -0,0 +1,28 @@ +pub mod error; +pub mod routes; +pub mod state; +pub mod ws; +pub mod ws_protocol; + +use axum::Router; +use std::sync::Arc; +use tower_http::cors::{Any, CorsLayer}; +use tower_http::trace::TraceLayer; + +pub use state::AppState; + +pub fn build_router(state: Arc) -> Router { + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + let api = routes::api_routes(); + + Router::new() + .nest("/api", api) + .route("/ws", axum::routing::get(ws::ws_handler)) + .layer(cors) + .layer(TraceLayer::new_for_http()) + .with_state(state) +} diff --git a/crates/vault-api/src/routes/agents.rs b/crates/vault-api/src/routes/agents.rs new file mode 100644 index 0000000..fb48f3c --- /dev/null +++ b/crates/vault-api/src/routes/agents.rs @@ -0,0 +1,111 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::routing::get; +use axum::{Json, Router}; +use serde_json::{json, Value}; +use std::sync::Arc; +use vault_core::filesystem; +use vault_core::types::AgentTask; + +pub fn routes() -> Router> { + Router::new() + .route("/agents", get(list_agents)) + .route("/agents/{name}", get(get_agent)) + .route( + "/agents/{name}/trigger", + axum::routing::post(trigger_agent), + ) +} + +async fn list_agents(State(state): State>) -> Result, ApiError> { + let agents = state.agents.read().unwrap(); + let list: Vec = agents + .values() + .map(|a| { + json!({ + "name": a.frontmatter.name, + "executable": a.frontmatter.executable, + "model": a.frontmatter.model, + "skills": a.frontmatter.skills, + "timeout": a.frontmatter.timeout, + }) + }) + .collect(); + Ok(Json(json!(list))) +} + +async fn get_agent( + State(state): State>, + Path(name): Path, +) -> Result, ApiError> { + let agents = state.agents.read().unwrap(); + let agent = agents + .get(&name) + .ok_or_else(|| ApiError::NotFound(format!("Agent '{}' not found", name)))?; + + Ok(Json(json!({ + "name": agent.frontmatter.name, + "executable": agent.frontmatter.executable, + "model": agent.frontmatter.model, + "escalate_to": agent.frontmatter.escalate_to, + "mcp_servers": agent.frontmatter.mcp_servers, + "skills": agent.frontmatter.skills, + "timeout": agent.frontmatter.timeout, + "max_retries": agent.frontmatter.max_retries, + "env": agent.frontmatter.env, + "body": agent.body, + }))) +} + +async fn trigger_agent( + State(state): State>, + Path(name): Path, + body: Option>, +) -> Result, ApiError> { + let agents = state.agents.read().unwrap(); + if !agents.contains_key(&name) { + return Err(ApiError::NotFound(format!("Agent '{}' not found", name))); + } + drop(agents); + + let context = body + .and_then(|b| b.get("context").and_then(|c| c.as_str().map(String::from))) + .unwrap_or_default(); + + let title = format!("Manual trigger: {}", name); + let slug = filesystem::timestamped_slug(&title); + let task_path = state + .vault_root + .join("todos/agent/queued") + .join(format!("{}.md", slug)); + + let task = AgentTask { + title, + agent: name, + priority: vault_core::types::Priority::Medium, + task_type: Some("manual".into()), + created: chrono::Utc::now(), + started: None, + completed: None, + retry: 0, + max_retries: 0, + input: None, + output: None, + error: None, + }; + + let entity = vault_core::entity::VaultEntity { + path: task_path.clone(), + frontmatter: task, + body: context, + }; + + state.write_filter.register(task_path.clone()); + vault_core::filesystem::write_entity(&entity).map_err(ApiError::Vault)?; + + Ok(Json(json!({ + "status": "queued", + "task_path": task_path.strip_prefix(&state.vault_root).unwrap_or(&task_path), + }))) +} diff --git a/crates/vault-api/src/routes/assistant.rs b/crates/vault-api/src/routes/assistant.rs new file mode 100644 index 0000000..cc0bd76 --- /dev/null +++ b/crates/vault-api/src/routes/assistant.rs @@ -0,0 +1,390 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::State; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +// --- Types --- + +#[derive(Debug, Deserialize)] +pub struct ChatRequest { + pub messages: Vec, + pub model: Option, + /// Optional path of the file being edited (for context) + pub file_path: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatMessage { + pub role: String, + pub content: String, +} + +#[derive(Debug, Serialize)] +pub struct ChatResponse { + pub message: ChatMessage, + pub model: String, +} + +#[derive(Debug, Deserialize)] +pub struct ApplyDiffRequest { + pub file_path: String, + pub diff: String, +} + +#[derive(Debug, Serialize)] +pub struct ModelInfo { + pub id: String, + pub name: String, +} + +// --- Routes --- + +pub fn routes() -> Router> { + Router::new() + .route("/assistant/chat", post(chat)) + .route("/assistant/apply-diff", post(apply_diff)) + .route("/assistant/models", get(list_models)) +} + +/// POST /api/assistant/chat — proxy chat to configured LLM +async fn chat( + State(state): State>, + Json(req): Json, +) -> Result, ApiError> { + let model = req + .model + .unwrap_or_else(|| state.config.assistant.default_model.clone()); + + // Build system prompt with vault context + let mut system_parts = vec![ + "You are an AI assistant integrated into vault-os, a personal operations platform.".into(), + "You help the user edit markdown files with YAML frontmatter.".into(), + "When suggesting changes, output unified diffs that can be applied.".into(), + ]; + + // If a file path is provided, include its content as context + if let Some(ref fp) = req.file_path { + let full = state.vault_root.join(fp); + if let Ok(content) = tokio::fs::read_to_string(&full).await { + system_parts.push(format!("\n--- Current file: {} ---\n{}", fp, content)); + } + } + + let system_prompt = system_parts.join("\n"); + + // Build messages for the LLM + let mut messages = vec![ChatMessage { + role: "system".into(), + content: system_prompt, + }]; + messages.extend(req.messages); + + // Determine backend from model string + let response = if model.starts_with("claude") || model.starts_with("anthropic/") { + call_anthropic(&state, &model, &messages).await? + } else { + // Default: OpenAI-compatible API (works with Ollama, vLLM, LM Studio, etc.) + call_openai_compatible(&state, &model, &messages).await? + }; + + Ok(Json(ChatResponse { + message: response, + model, + })) +} + +/// Call Anthropic Messages API +async fn call_anthropic( + _state: &AppState, + model: &str, + messages: &[ChatMessage], +) -> Result { + let api_key = std::env::var("ANTHROPIC_API_KEY") + .map_err(|_| ApiError::BadRequest("ANTHROPIC_API_KEY not set".into()))?; + + // Extract system message + let system = messages + .iter() + .find(|m| m.role == "system") + .map(|m| m.content.clone()) + .unwrap_or_default(); + + let user_messages: Vec = messages + .iter() + .filter(|m| m.role != "system") + .map(|m| { + serde_json::json!({ + "role": m.role, + "content": m.content, + }) + }) + .collect(); + + let model_id = model.strip_prefix("anthropic/").unwrap_or(model); + + let body = serde_json::json!({ + "model": model_id, + "max_tokens": 4096, + "system": system, + "messages": user_messages, + }); + + let client = reqwest::Client::new(); + let resp = client + .post("https://api.anthropic.com/v1/messages") + .header("x-api-key", &api_key) + .header("anthropic-version", "2023-06-01") + .header("content-type", "application/json") + .json(&body) + .send() + .await + .map_err(|e| ApiError::Internal(format!("Anthropic request failed: {e}")))?; + + if !resp.status().is_success() { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + return Err(ApiError::Internal(format!( + "Anthropic API error {status}: {text}" + ))); + } + + let json: serde_json::Value = resp + .json() + .await + .map_err(|e| ApiError::Internal(format!("Failed to parse Anthropic response: {e}")))?; + + let content = json["content"] + .as_array() + .and_then(|arr| arr.first()) + .and_then(|block| block["text"].as_str()) + .unwrap_or("") + .to_string(); + + Ok(ChatMessage { + role: "assistant".into(), + content, + }) +} + +/// Call OpenAI-compatible API (Ollama, vLLM, LM Studio, etc.) +async fn call_openai_compatible( + state: &AppState, + model: &str, + messages: &[ChatMessage], +) -> Result { + // Check for configured executor base_url, fall back to Ollama default + let base_url = state + .config + .executors + .values() + .find_map(|e| e.base_url.clone()) + .unwrap_or_else(|| "http://localhost:11434".into()); + + let model_id = model.split('/').next_back().unwrap_or(model); + + let body = serde_json::json!({ + "model": model_id, + "messages": messages.iter().map(|m| serde_json::json!({ + "role": m.role, + "content": m.content, + })).collect::>(), + }); + + let client = reqwest::Client::new(); + let resp = client + .post(format!("{}/v1/chat/completions", base_url)) + .header("content-type", "application/json") + .json(&body) + .send() + .await + .map_err(|e| ApiError::Internal(format!("LLM request failed: {e}")))?; + + if !resp.status().is_success() { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + return Err(ApiError::Internal(format!( + "LLM API error {status}: {text}" + ))); + } + + let json: serde_json::Value = resp + .json() + .await + .map_err(|e| ApiError::Internal(format!("Failed to parse LLM response: {e}")))?; + + let content = json["choices"] + .as_array() + .and_then(|arr| arr.first()) + .and_then(|choice| choice["message"]["content"].as_str()) + .unwrap_or("") + .to_string(); + + Ok(ChatMessage { + role: "assistant".into(), + content, + }) +} + +/// POST /api/assistant/apply-diff — apply a unified diff to a file +async fn apply_diff( + State(state): State>, + Json(req): Json, +) -> Result, ApiError> { + let full_path = state.vault_root.join(&req.file_path); + if !full_path.exists() { + return Err(ApiError::NotFound(format!("File not found: {}", req.file_path))); + } + + let original = tokio::fs::read_to_string(&full_path) + .await + .map_err(|e| ApiError::Internal(format!("Failed to read file: {e}")))?; + + let patched = apply_unified_diff(&original, &req.diff) + .map_err(|e| ApiError::BadRequest(format!("Failed to apply diff: {e}")))?; + + // Register with write filter to prevent feedback loop + state.write_filter.register(full_path.clone()); + + tokio::fs::write(&full_path, &patched) + .await + .map_err(|e| ApiError::Internal(format!("Failed to write file: {e}")))?; + + Ok(Json(serde_json::json!({ "status": "ok", "path": req.file_path }))) +} + +/// Simple unified diff applier +fn apply_unified_diff(original: &str, diff: &str) -> Result { + let mut result_lines: Vec = original.lines().map(String::from).collect(); + let mut offset: i64 = 0; + + for hunk in parse_hunks(diff) { + let start = ((hunk.old_start as i64) - 1 + offset) as usize; + let end = start + hunk.old_count; + + if end > result_lines.len() { + return Err(format!( + "Hunk at line {} extends beyond file (file has {} lines)", + hunk.old_start, + result_lines.len() + )); + } + + result_lines.splice(start..end, hunk.new_lines); + + offset += hunk.new_count as i64 - hunk.old_count as i64; + } + + let mut result = result_lines.join("\n"); + if original.ends_with('\n') && !result.ends_with('\n') { + result.push('\n'); + } + Ok(result) +} + +struct Hunk { + old_start: usize, + old_count: usize, + new_count: usize, + new_lines: Vec, +} + +fn parse_hunks(diff: &str) -> Vec { + let mut hunks = Vec::new(); + let mut lines = diff.lines().peekable(); + + while let Some(line) = lines.next() { + if line.starts_with("@@") { + // Parse @@ -old_start,old_count +new_start,new_count @@ + if let Some(hunk) = parse_hunk_header(line) { + let mut old_count = 0; + let mut new_lines = Vec::new(); + let mut new_count = 0; + + while old_count < hunk.0 || new_count < hunk.1 { + match lines.next() { + Some(l) if l.starts_with('-') => { + old_count += 1; + } + Some(l) if l.starts_with('+') => { + new_lines.push(l[1..].to_string()); + new_count += 1; + } + Some(l) => { + // Context line (starts with ' ' or no prefix) + let content = l.strip_prefix(' ').unwrap_or(l); + new_lines.push(content.to_string()); + old_count += 1; + new_count += 1; + } + None => break, + } + } + + hunks.push(Hunk { + old_start: hunk.2, + old_count: hunk.0, + new_count: new_lines.len(), + new_lines, + }); + } + } + } + + hunks +} + +/// Parse "@@ -start,count +start,count @@" returning (old_count, new_count, old_start) +fn parse_hunk_header(line: &str) -> Option<(usize, usize, usize)> { + let stripped = line.trim_start_matches("@@").trim_end_matches("@@").trim(); + let parts: Vec<&str> = stripped.split_whitespace().collect(); + if parts.len() < 2 { + return None; + } + + let old_part = parts[0].trim_start_matches('-'); + let new_part = parts[1].trim_start_matches('+'); + + let (old_start, old_count) = parse_range(old_part)?; + let (_new_start, new_count) = parse_range(new_part)?; + + Some((old_count, new_count, old_start)) +} + +fn parse_range(s: &str) -> Option<(usize, usize)> { + if let Some((start, count)) = s.split_once(',') { + Some((start.parse().ok()?, count.parse().ok()?)) + } else { + Some((s.parse().ok()?, 1)) + } +} + +/// GET /api/assistant/models — list available models from config +async fn list_models(State(state): State>) -> Json> { + let mut models: Vec = state + .config + .assistant + .models + .iter() + .map(|m| ModelInfo { + id: m.clone(), + name: m.clone(), + }) + .collect(); + + // Always include the default model + let default = &state.config.assistant.default_model; + if !models.iter().any(|m| m.id == *default) { + models.insert( + 0, + ModelInfo { + id: default.clone(), + name: format!("{} (default)", default), + }, + ); + } + + Json(models) +} diff --git a/crates/vault-api/src/routes/crons.rs b/crates/vault-api/src/routes/crons.rs new file mode 100644 index 0000000..2e739a7 --- /dev/null +++ b/crates/vault-api/src/routes/crons.rs @@ -0,0 +1,127 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde_json::{json, Value}; +use std::sync::Arc; +use vault_core::filesystem; +use vault_core::types::CronJob; + +pub fn routes() -> Router> { + Router::new() + .route("/crons", get(list_crons)) + .route("/crons/{name}/trigger", post(trigger_cron)) + .route("/crons/{name}/pause", post(pause_cron)) + .route("/crons/{name}/resume", post(resume_cron)) +} + +async fn list_crons(State(state): State>) -> Result, ApiError> { + let mut crons = Vec::new(); + + for subdir in &["active", "paused"] { + let dir = state.vault_root.join("crons").join(subdir); + let files = filesystem::list_md_files(&dir).map_err(ApiError::Vault)?; + for file in files { + match filesystem::read_entity::(&file) { + Ok(entity) => { + crons.push(json!({ + "name": file.file_stem().and_then(|s| s.to_str()), + "title": entity.frontmatter.title, + "schedule": entity.frontmatter.schedule, + "agent": entity.frontmatter.agent, + "enabled": *subdir == "active" && entity.frontmatter.enabled, + "status": subdir, + "last_run": entity.frontmatter.last_run, + "last_status": entity.frontmatter.last_status, + "next_run": entity.frontmatter.next_run, + "run_count": entity.frontmatter.run_count, + })); + } + Err(e) => { + tracing::warn!(path = ?file, error = %e, "Failed to read cron"); + } + } + } + } + + Ok(Json(json!(crons))) +} + +async fn trigger_cron( + State(state): State>, + Path(name): Path, +) -> Result, ApiError> { + let cron_path = state + .vault_root + .join("crons/active") + .join(format!("{}.md", name)); + + if !cron_path.exists() { + return Err(ApiError::NotFound(format!("Cron '{}' not found in active/", name))); + } + + let mut engine = state.cron_engine.lock().unwrap(); + let task_path = engine + .fire_cron(&cron_path, &state.write_filter) + .map_err(|e| ApiError::Internal(e.to_string()))?; + + Ok(Json(json!({ + "status": "fired", + "task_path": task_path.strip_prefix(&state.vault_root).unwrap_or(&task_path), + }))) +} + +async fn pause_cron( + State(state): State>, + Path(name): Path, +) -> Result, ApiError> { + let from = state + .vault_root + .join("crons/active") + .join(format!("{}.md", name)); + let to = state + .vault_root + .join("crons/paused") + .join(format!("{}.md", name)); + + if !from.exists() { + return Err(ApiError::NotFound(format!("Cron '{}' not found in active/", name))); + } + + state.write_filter.register(to.clone()); + filesystem::move_file(&from, &to).map_err(ApiError::Vault)?; + + let mut engine = state.cron_engine.lock().unwrap(); + engine.remove_cron(&from); + + Ok(Json(json!({ "status": "paused" }))) +} + +async fn resume_cron( + State(state): State>, + Path(name): Path, +) -> Result, ApiError> { + let from = state + .vault_root + .join("crons/paused") + .join(format!("{}.md", name)); + let to = state + .vault_root + .join("crons/active") + .join(format!("{}.md", name)); + + if !from.exists() { + return Err(ApiError::NotFound(format!("Cron '{}' not found in paused/", name))); + } + + state.write_filter.register(to.clone()); + filesystem::move_file(&from, &to).map_err(ApiError::Vault)?; + + let mut engine = state.cron_engine.lock().unwrap(); + if let Err(e) = engine.upsert_cron(&to) { + tracing::warn!(error = %e, "Failed to schedule resumed cron"); + } + + Ok(Json(json!({ "status": "active" }))) +} diff --git a/crates/vault-api/src/routes/files.rs b/crates/vault-api/src/routes/files.rs new file mode 100644 index 0000000..80b352d --- /dev/null +++ b/crates/vault-api/src/routes/files.rs @@ -0,0 +1,126 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::routing::get; +use axum::{Json, Router}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::sync::Arc; + +pub fn routes() -> Router> { + Router::new() + .route("/files/{*path}", get(read_file).put(write_file).patch(patch_file).delete(delete_file)) +} + +async fn read_file( + State(state): State>, + Path(path): Path, +) -> Result, ApiError> { + let file_path = state.vault_root.join(&path); + + if !file_path.exists() { + return Err(ApiError::NotFound(format!("File '{}' not found", path))); + } + + let content = std::fs::read_to_string(&file_path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + // Try to split frontmatter + if let Ok((yaml, body)) = vault_core::frontmatter::split_frontmatter(&content) { + let frontmatter: Value = serde_yaml::from_str(yaml).unwrap_or(Value::Null); + Ok(Json(json!({ + "path": path, + "frontmatter": frontmatter, + "body": body, + }))) + } else { + Ok(Json(json!({ + "path": path, + "frontmatter": null, + "body": content, + }))) + } +} + +#[derive(Deserialize)] +struct WriteFileBody { + #[serde(default)] + frontmatter: Option, + #[serde(default)] + body: Option, + #[serde(default)] + raw: Option, +} + +async fn write_file( + State(state): State>, + Path(path): Path, + Json(data): Json, +) -> Result, ApiError> { + let file_path = state.vault_root.join(&path); + + if let Some(parent) = file_path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, parent)))?; + } + + let content = if let Some(raw) = data.raw { + raw + } else { + let body = data.body.unwrap_or_default(); + if let Some(fm) = data.frontmatter { + let yaml = serde_yaml::to_string(&fm) + .map_err(|e| ApiError::Internal(e.to_string()))?; + format!("---\n{}---\n{}", yaml, body) + } else { + body + } + }; + + state.write_filter.register(file_path.clone()); + std::fs::write(&file_path, content) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + Ok(Json(json!({ "status": "written", "path": path }))) +} + +async fn patch_file( + State(state): State>, + Path(path): Path, + Json(updates): Json, +) -> Result, ApiError> { + let file_path = state.vault_root.join(&path); + + if !file_path.exists() { + return Err(ApiError::NotFound(format!("File '{}' not found", path))); + } + + let content = std::fs::read_to_string(&file_path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + let updated = + vault_core::frontmatter::update_frontmatter_fields(&content, &file_path, &updates) + .map_err(ApiError::Vault)?; + + state.write_filter.register(file_path.clone()); + std::fs::write(&file_path, updated) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + Ok(Json(json!({ "status": "patched", "path": path }))) +} + +async fn delete_file( + State(state): State>, + Path(path): Path, +) -> Result, ApiError> { + let file_path = state.vault_root.join(&path); + + if !file_path.exists() { + return Err(ApiError::NotFound(format!("File '{}' not found", path))); + } + + std::fs::remove_file(&file_path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + Ok(Json(json!({ "status": "deleted", "path": path }))) +} diff --git a/crates/vault-api/src/routes/knowledge.rs b/crates/vault-api/src/routes/knowledge.rs new file mode 100644 index 0000000..960bd70 --- /dev/null +++ b/crates/vault-api/src/routes/knowledge.rs @@ -0,0 +1,126 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, Query, State}; +use axum::routing::get; +use axum::{Json, Router}; +use pulldown_cmark::{html, Parser}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::sync::Arc; +use vault_core::entity::VaultEntity; +use vault_core::filesystem; +use vault_core::types::KnowledgeNote; + +pub fn routes() -> Router> { + Router::new() + .route("/knowledge", get(list_knowledge)) + .route("/knowledge/{*path}", get(get_knowledge)) +} + +#[derive(Deserialize, Default)] +struct SearchQuery { + #[serde(default)] + q: Option, + #[serde(default)] + tag: Option, +} + +async fn list_knowledge( + State(state): State>, + Query(query): Query, +) -> Result, ApiError> { + let dir = state.vault_root.join("knowledge"); + let files = filesystem::list_md_files_recursive(&dir).map_err(ApiError::Vault)?; + + let mut notes = Vec::new(); + for file in files { + // Try parsing with frontmatter + let content = std::fs::read_to_string(&file) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file)))?; + + let (title, tags) = if let Ok(entity) = VaultEntity::::from_content(file.clone(), &content) { + ( + entity.frontmatter.title.unwrap_or_else(|| { + file.file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("untitled") + .to_string() + }), + entity.frontmatter.tags, + ) + } else { + ( + file.file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("untitled") + .to_string(), + vec![], + ) + }; + + // Apply filters + if let Some(ref q) = query.q { + let q_lower = q.to_lowercase(); + if !title.to_lowercase().contains(&q_lower) + && !content.to_lowercase().contains(&q_lower) + { + continue; + } + } + if let Some(ref tag) = query.tag { + if !tags.iter().any(|t| t == tag) { + continue; + } + } + + let relative = file.strip_prefix(&state.vault_root).unwrap_or(&file); + notes.push(json!({ + "path": relative, + "title": title, + "tags": tags, + })); + } + + Ok(Json(json!(notes))) +} + +async fn get_knowledge( + State(state): State>, + Path(path): Path, +) -> Result, ApiError> { + let file_path = state.vault_root.join("knowledge").join(&path); + + if !file_path.exists() { + return Err(ApiError::NotFound(format!("Knowledge note '{}' not found", path))); + } + + let content = std::fs::read_to_string(&file_path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + let (frontmatter, body) = if let Ok(entity) = VaultEntity::::from_content(file_path.clone(), &content) { + ( + json!({ + "title": entity.frontmatter.title, + "tags": entity.frontmatter.tags, + "source": entity.frontmatter.source, + "created": entity.frontmatter.created, + "related": entity.frontmatter.related, + }), + entity.body, + ) + } else { + (json!({}), content.clone()) + }; + + // Render markdown to HTML + let parser = Parser::new(&body); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + + Ok(Json(json!({ + "path": path, + "frontmatter": frontmatter, + "body": body, + "html": html_output, + }))) +} diff --git a/crates/vault-api/src/routes/mod.rs b/crates/vault-api/src/routes/mod.rs new file mode 100644 index 0000000..d82d59a --- /dev/null +++ b/crates/vault-api/src/routes/mod.rs @@ -0,0 +1,36 @@ +pub mod agents; +pub mod assistant; +pub mod crons; +pub mod files; +pub mod knowledge; +pub mod skills; +pub mod stats; +pub mod suggest; +pub mod templates; +pub mod todos_agent; +pub mod todos_human; +pub mod tree; +pub mod validate; +pub mod views; + +use crate::state::AppState; +use axum::Router; +use std::sync::Arc; + +pub fn api_routes() -> Router> { + Router::new() + .merge(agents::routes()) + .merge(skills::routes()) + .merge(crons::routes()) + .merge(todos_human::routes()) + .merge(todos_agent::routes()) + .merge(knowledge::routes()) + .merge(files::routes()) + .merge(tree::routes()) + .merge(suggest::routes()) + .merge(stats::routes()) + .merge(views::routes()) + .merge(assistant::routes()) + .merge(validate::routes()) + .merge(templates::routes()) +} diff --git a/crates/vault-api/src/routes/skills.rs b/crates/vault-api/src/routes/skills.rs new file mode 100644 index 0000000..65e3f19 --- /dev/null +++ b/crates/vault-api/src/routes/skills.rs @@ -0,0 +1,62 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::routing::get; +use axum::{Json, Router}; +use serde_json::{json, Value}; +use std::sync::Arc; + +pub fn routes() -> Router> { + Router::new() + .route("/skills", get(list_skills)) + .route("/skills/{name}", get(get_skill)) + .route("/skills/{name}/used-by", get(skill_used_by)) +} + +async fn list_skills(State(state): State>) -> Result, ApiError> { + let skills = state.skills.read().unwrap(); + let list: Vec = skills + .values() + .map(|s| { + json!({ + "name": s.frontmatter.name, + "description": s.frontmatter.description, + "version": s.frontmatter.version, + }) + }) + .collect(); + Ok(Json(json!(list))) +} + +async fn get_skill( + State(state): State>, + Path(name): Path, +) -> Result, ApiError> { + let skills = state.skills.read().unwrap(); + let skill = skills + .get(&name) + .ok_or_else(|| ApiError::NotFound(format!("Skill '{}' not found", name)))?; + + Ok(Json(json!({ + "name": skill.frontmatter.name, + "description": skill.frontmatter.description, + "version": skill.frontmatter.version, + "requires_mcp": skill.frontmatter.requires_mcp, + "inputs": skill.frontmatter.inputs, + "outputs": skill.frontmatter.outputs, + "body": skill.body, + }))) +} + +async fn skill_used_by( + State(state): State>, + Path(name): Path, +) -> Result, ApiError> { + let agents = state.agents.read().unwrap(); + let users: Vec = agents + .values() + .filter(|a| a.frontmatter.skills.contains(&name)) + .map(|a| a.frontmatter.name.clone()) + .collect(); + Ok(Json(json!(users))) +} diff --git a/crates/vault-api/src/routes/stats.rs b/crates/vault-api/src/routes/stats.rs new file mode 100644 index 0000000..3bade93 --- /dev/null +++ b/crates/vault-api/src/routes/stats.rs @@ -0,0 +1,112 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::State; +use axum::routing::get; +use axum::{Json, Router}; +use serde_json::{json, Value}; +use std::sync::Arc; +use vault_core::filesystem; + +pub fn routes() -> Router> { + Router::new() + .route("/stats", get(get_stats)) + .route("/activity", get(get_activity)) + .route("/health", get(health_check)) +} + +async fn get_stats(State(state): State>) -> Result, ApiError> { + let agents_count = state.agents.read().unwrap().len(); + let skills_count = state.skills.read().unwrap().len(); + let crons_scheduled = state.cron_engine.lock().unwrap().scheduled_count(); + + let mut task_counts = serde_json::Map::new(); + for status in &["urgent", "open", "in-progress", "done"] { + let dir = state.vault_root.join("todos/harald").join(status); + let count = filesystem::list_md_files(&dir) + .map(|f| f.len()) + .unwrap_or(0); + task_counts.insert(status.to_string(), json!(count)); + } + + let mut agent_task_counts = serde_json::Map::new(); + for status in &["queued", "running", "done", "failed"] { + let dir = state.vault_root.join("todos/agent").join(status); + let count = filesystem::list_md_files(&dir) + .map(|f| f.len()) + .unwrap_or(0); + agent_task_counts.insert(status.to_string(), json!(count)); + } + + let knowledge_count = filesystem::list_md_files_recursive(&state.vault_root.join("knowledge")) + .map(|f| f.len()) + .unwrap_or(0); + + let runtime_state = state.runtime_state.lock().unwrap(); + + Ok(Json(json!({ + "agents": agents_count, + "skills": skills_count, + "crons_scheduled": crons_scheduled, + "human_tasks": task_counts, + "agent_tasks": agent_task_counts, + "knowledge_notes": knowledge_count, + "total_tasks_executed": runtime_state.total_tasks_executed, + "total_cron_fires": runtime_state.total_cron_fires, + }))) +} + +async fn get_activity(State(state): State>) -> Result, ApiError> { + // Collect recently modified files across the vault as activity items + let mut activity = Vec::new(); + + let dirs = [ + ("todos/harald", "human_task"), + ("todos/agent", "agent_task"), + ("knowledge", "knowledge"), + ]; + + for (dir, kind) in &dirs { + if let Ok(files) = filesystem::list_md_files_recursive(&state.vault_root.join(dir)) { + for file in files.iter().rev().take(20) { + if let Ok(metadata) = std::fs::metadata(file) { + if let Ok(modified) = metadata.modified() { + let relative = file.strip_prefix(&state.vault_root).unwrap_or(file); + activity.push(json!({ + "path": relative, + "kind": kind, + "modified": chrono::DateTime::::from(modified), + "name": file.file_stem().and_then(|s| s.to_str()), + })); + } + } + } + } + } + + // Sort by modification time, newest first + activity.sort_by(|a, b| { + let a_time = a.get("modified").and_then(|t| t.as_str()).unwrap_or(""); + let b_time = b.get("modified").and_then(|t| t.as_str()).unwrap_or(""); + b_time.cmp(a_time) + }); + + activity.truncate(50); + + Ok(Json(json!(activity))) +} + +async fn health_check(State(state): State>) -> Json { + let runtime_state = state.runtime_state.lock().unwrap(); + let uptime = chrono::Utc::now() - state.startup_time; + let crons = state.cron_engine.lock().unwrap().scheduled_count(); + let agents = state.agents.read().unwrap().len(); + + Json(json!({ + "status": "ok", + "version": env!("CARGO_PKG_VERSION"), + "uptime_secs": uptime.num_seconds(), + "agents": agents, + "crons_scheduled": crons, + "total_tasks_executed": runtime_state.total_tasks_executed, + })) +} diff --git a/crates/vault-api/src/routes/suggest.rs b/crates/vault-api/src/routes/suggest.rs new file mode 100644 index 0000000..f147c97 --- /dev/null +++ b/crates/vault-api/src/routes/suggest.rs @@ -0,0 +1,141 @@ +use crate::state::AppState; +use axum::extract::{Query, State}; +use axum::routing::get; +use axum::{Json, Router}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::collections::HashSet; +use std::sync::Arc; +use vault_core::filesystem; +use vault_core::types::{HumanTask, KnowledgeNote}; + +pub fn routes() -> Router> { + Router::new() + .route("/suggest/agents", get(suggest_agents)) + .route("/suggest/skills", get(suggest_skills)) + .route("/suggest/tags", get(suggest_tags)) + .route("/suggest/repos", get(suggest_repos)) + .route("/suggest/labels", get(suggest_labels)) + .route("/suggest/files", get(suggest_files)) + .route("/suggest/models", get(suggest_models)) + .route("/suggest/mcp-servers", get(suggest_mcp_servers)) +} + +async fn suggest_agents(State(state): State>) -> Json { + let agents = state.agents.read().unwrap(); + let names: Vec<&str> = agents.keys().map(|s| s.as_str()).collect(); + Json(json!(names)) +} + +async fn suggest_skills(State(state): State>) -> Json { + let skills = state.skills.read().unwrap(); + let names: Vec<&str> = skills.keys().map(|s| s.as_str()).collect(); + Json(json!(names)) +} + +async fn suggest_tags(State(state): State>) -> Json { + let mut tags = HashSet::new(); + + // Collect from knowledge notes + if let Ok(files) = filesystem::list_md_files_recursive(&state.vault_root.join("knowledge")) { + for file in files { + if let Ok(entity) = filesystem::read_entity::(&file) { + for tag in &entity.frontmatter.tags { + tags.insert(tag.clone()); + } + } + } + } + + let mut tags: Vec = tags.into_iter().collect(); + tags.sort(); + Json(json!(tags)) +} + +async fn suggest_repos(State(state): State>) -> Json { + let mut repos = HashSet::new(); + + for status in &["urgent", "open", "in-progress", "done"] { + let dir = state.vault_root.join("todos/harald").join(status); + if let Ok(files) = filesystem::list_md_files(&dir) { + for file in files { + if let Ok(entity) = filesystem::read_entity::(&file) { + if let Some(repo) = &entity.frontmatter.repo { + repos.insert(repo.clone()); + } + } + } + } + } + + let mut repos: Vec = repos.into_iter().collect(); + repos.sort(); + Json(json!(repos)) +} + +async fn suggest_labels(State(state): State>) -> Json { + let mut labels = HashSet::new(); + + for status in &["urgent", "open", "in-progress", "done"] { + let dir = state.vault_root.join("todos/harald").join(status); + if let Ok(files) = filesystem::list_md_files(&dir) { + for file in files { + if let Ok(entity) = filesystem::read_entity::(&file) { + for label in &entity.frontmatter.labels { + labels.insert(label.clone()); + } + } + } + } + } + + let mut labels: Vec = labels.into_iter().collect(); + labels.sort(); + Json(json!(labels)) +} + +#[derive(Deserialize, Default)] +struct FileQuery { + #[serde(default)] + q: Option, +} + +async fn suggest_files( + State(state): State>, + Query(query): Query, +) -> Json { + let mut files = Vec::new(); + + if let Ok(all_files) = filesystem::list_md_files_recursive(&state.vault_root) { + for file in all_files { + if let Ok(relative) = file.strip_prefix(&state.vault_root) { + let rel_str = relative.to_string_lossy().to_string(); + + // Skip .vault internal files + if rel_str.starts_with(".vault") { + continue; + } + + if let Some(ref q) = query.q { + if !rel_str.to_lowercase().contains(&q.to_lowercase()) { + continue; + } + } + + files.push(rel_str); + } + } + } + + files.sort(); + Json(json!(files)) +} + +async fn suggest_models(State(state): State>) -> Json { + Json(json!(state.config.assistant.models)) +} + +async fn suggest_mcp_servers(State(state): State>) -> Json { + let servers: Vec<&str> = state.config.mcp_servers.keys().map(|s| s.as_str()).collect(); + Json(json!(servers)) +} diff --git a/crates/vault-api/src/routes/templates.rs b/crates/vault-api/src/routes/templates.rs new file mode 100644 index 0000000..ca73229 --- /dev/null +++ b/crates/vault-api/src/routes/templates.rs @@ -0,0 +1,144 @@ +use crate::state::AppState; +use axum::extract::Path; +use axum::routing::get; +use axum::{Json, Router}; +use std::sync::Arc; + +pub fn routes() -> Router> { + Router::new() + .route("/templates", get(list_templates)) + .route("/templates/{name}", get(get_template)) +} + +#[derive(serde::Serialize)] +struct TemplateInfo { + name: String, + description: String, + category: String, +} + +async fn list_templates() -> Json> { + Json(vec![ + TemplateInfo { + name: "agent".into(), + description: "New AI agent definition".into(), + category: "agents".into(), + }, + TemplateInfo { + name: "skill".into(), + description: "New agent skill".into(), + category: "skills".into(), + }, + TemplateInfo { + name: "cron".into(), + description: "New cron schedule".into(), + category: "crons".into(), + }, + TemplateInfo { + name: "human-task".into(), + description: "New human task".into(), + category: "todos/harald".into(), + }, + TemplateInfo { + name: "agent-task".into(), + description: "New agent task".into(), + category: "todos/agent".into(), + }, + TemplateInfo { + name: "knowledge".into(), + description: "New knowledge note".into(), + category: "knowledge".into(), + }, + TemplateInfo { + name: "view-page".into(), + description: "New dashboard view page".into(), + category: "views/pages".into(), + }, + ]) +} + +async fn get_template(Path(name): Path) -> Json { + let template = match name.as_str() { + "agent" => serde_json::json!({ + "frontmatter": { + "name": "new-agent", + "executable": "claude-code", + "model": "", + "skills": [], + "mcp_servers": [], + "timeout": 600, + "max_retries": 0, + "env": {} + }, + "body": "You are an AI agent.\n\nDescribe your agent's purpose and behavior here.\n" + }), + "skill" => serde_json::json!({ + "frontmatter": { + "name": "new-skill", + "description": "Describe what this skill does", + "version": 1, + "inputs": [], + "outputs": [], + "requires_mcp": [] + }, + "body": "## Instructions\n\nDescribe the skill instructions here.\n" + }), + "cron" => serde_json::json!({ + "frontmatter": { + "title": "New Cron Job", + "schedule": "0 9 * * *", + "agent": "", + "enabled": true + }, + "body": "Optional context for the cron job execution.\n" + }), + "human-task" => serde_json::json!({ + "frontmatter": { + "title": "New Task", + "priority": "medium", + "labels": [], + "created": chrono::Utc::now().to_rfc3339() + }, + "body": "Task description goes here.\n" + }), + "agent-task" => serde_json::json!({ + "frontmatter": { + "title": "New Agent Task", + "agent": "", + "priority": "medium", + "created": chrono::Utc::now().to_rfc3339(), + "retry": 0, + "max_retries": 0 + }, + "body": "Task instructions for the agent.\n" + }), + "knowledge" => serde_json::json!({ + "frontmatter": { + "title": "New Note", + "tags": [], + "created": chrono::Utc::now().to_rfc3339() + }, + "body": "Write your knowledge note here.\n" + }), + "view-page" => serde_json::json!({ + "frontmatter": { + "type": "page", + "title": "New View", + "icon": "", + "route": "/view/new-view", + "position": 10, + "layout": "single", + "regions": { + "main": [] + } + }, + "body": "" + }), + _ => serde_json::json!({ + "frontmatter": {}, + "body": "" + }), + }; + + Json(template) +} diff --git a/crates/vault-api/src/routes/todos_agent.rs b/crates/vault-api/src/routes/todos_agent.rs new file mode 100644 index 0000000..eb85f59 --- /dev/null +++ b/crates/vault-api/src/routes/todos_agent.rs @@ -0,0 +1,149 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::routing::get; +use axum::{Json, Router}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::sync::Arc; +use vault_core::entity::VaultEntity; +use vault_core::filesystem; +use vault_core::types::AgentTask; + +pub fn routes() -> Router> { + Router::new() + .route("/todos/agent", get(list_all).post(create_task)) + .route("/todos/agent/{id}", get(get_task)) +} + +async fn list_all(State(state): State>) -> Result, ApiError> { + let mut tasks = Vec::new(); + for status in &["queued", "running", "done", "failed"] { + let dir = state.vault_root.join("todos/agent").join(status); + let files = filesystem::list_md_files(&dir).map_err(ApiError::Vault)?; + for file in files { + if let Ok(entity) = filesystem::read_entity::(&file) { + tasks.push(agent_task_to_json(&entity, status)); + } + } + } + Ok(Json(json!(tasks))) +} + +async fn get_task( + State(state): State>, + Path(id): Path, +) -> Result, ApiError> { + for status in &["queued", "running", "done", "failed"] { + let path = state + .vault_root + .join("todos/agent") + .join(status) + .join(format!("{}.md", id)); + if path.exists() { + let entity = filesystem::read_entity::(&path).map_err(ApiError::Vault)?; + return Ok(Json(agent_task_to_json(&entity, status))); + } + } + Err(ApiError::NotFound(format!("Agent task '{}' not found", id))) +} + +#[derive(Deserialize)] +struct CreateAgentTaskBody { + title: String, + agent: String, + #[serde(default)] + priority: Option, + #[serde(default, rename = "type")] + task_type: Option, + #[serde(default)] + max_retries: Option, + #[serde(default)] + input: Option, + #[serde(default)] + body: Option, +} + +async fn create_task( + State(state): State>, + Json(body): Json, +) -> Result, ApiError> { + // Verify agent exists + { + let agents = state.agents.read().unwrap(); + if !agents.contains_key(&body.agent) { + return Err(ApiError::BadRequest(format!( + "Agent '{}' not found", + body.agent + ))); + } + } + + let priority = match body.priority.as_deref() { + Some("urgent") => vault_core::types::Priority::Urgent, + Some("high") => vault_core::types::Priority::High, + Some("low") => vault_core::types::Priority::Low, + _ => vault_core::types::Priority::Medium, + }; + + let slug = filesystem::timestamped_slug(&body.title); + let path = state + .vault_root + .join("todos/agent/queued") + .join(format!("{}.md", slug)); + + let task = AgentTask { + title: body.title, + agent: body.agent, + priority, + task_type: body.task_type, + created: chrono::Utc::now(), + started: None, + completed: None, + retry: 0, + max_retries: body.max_retries.unwrap_or(0), + input: body.input, + output: None, + error: None, + }; + + let entity = VaultEntity { + path: path.clone(), + frontmatter: task, + body: body.body.unwrap_or_default(), + }; + + state.write_filter.register(path.clone()); + filesystem::write_entity(&entity).map_err(ApiError::Vault)?; + + Ok(Json(json!({ + "status": "queued", + "path": path.strip_prefix(&state.vault_root).unwrap_or(&path), + }))) +} + +fn agent_task_to_json(entity: &VaultEntity, status: &str) -> Value { + let id = entity + .path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown"); + + json!({ + "id": id, + "title": entity.frontmatter.title, + "agent": entity.frontmatter.agent, + "priority": entity.frontmatter.priority, + "type": entity.frontmatter.task_type, + "status": status, + "created": entity.frontmatter.created, + "started": entity.frontmatter.started, + "completed": entity.frontmatter.completed, + "retry": entity.frontmatter.retry, + "max_retries": entity.frontmatter.max_retries, + "input": entity.frontmatter.input, + "output": entity.frontmatter.output, + "error": entity.frontmatter.error, + "body": entity.body, + }) +} diff --git a/crates/vault-api/src/routes/todos_human.rs b/crates/vault-api/src/routes/todos_human.rs new file mode 100644 index 0000000..be72e19 --- /dev/null +++ b/crates/vault-api/src/routes/todos_human.rs @@ -0,0 +1,205 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::routing::{get, patch}; +use axum::{Json, Router}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::sync::Arc; +use vault_core::entity::VaultEntity; +use vault_core::filesystem; +use vault_core::types::HumanTask; + +pub fn routes() -> Router> { + Router::new() + .route("/todos/harald", get(list_all).post(create_task)) + .route("/todos/harald/{status}", get(list_by_status)) + .route("/todos/harald/{status}/{id}/move", patch(move_task)) + .route( + "/todos/harald/{status}/{id}", + axum::routing::delete(delete_task), + ) +} + +async fn list_all(State(state): State>) -> Result, ApiError> { + let mut tasks = Vec::new(); + for status in &["urgent", "open", "in-progress", "done"] { + let dir = state + .vault_root + .join("todos/harald") + .join(status); + let files = filesystem::list_md_files(&dir).map_err(ApiError::Vault)?; + for file in files { + if let Ok(entity) = filesystem::read_entity::(&file) { + tasks.push(task_to_json(&entity, status)); + } + } + } + Ok(Json(json!(tasks))) +} + +async fn list_by_status( + State(state): State>, + Path(status): Path, +) -> Result, ApiError> { + let dir = state + .vault_root + .join("todos/harald") + .join(&status); + if !dir.exists() { + return Err(ApiError::NotFound(format!("Status '{}' not found", status))); + } + + let files = filesystem::list_md_files(&dir).map_err(ApiError::Vault)?; + let mut tasks = Vec::new(); + for file in files { + if let Ok(entity) = filesystem::read_entity::(&file) { + tasks.push(task_to_json(&entity, &status)); + } + } + Ok(Json(json!(tasks))) +} + +#[derive(Deserialize)] +struct CreateTaskBody { + title: String, + #[serde(default)] + priority: Option, + #[serde(default)] + labels: Vec, + #[serde(default)] + repo: Option, + #[serde(default)] + due: Option, + #[serde(default)] + body: Option, +} + +async fn create_task( + State(state): State>, + Json(body): Json, +) -> Result, ApiError> { + let priority = match body.priority.as_deref() { + Some("urgent") => vault_core::types::Priority::Urgent, + Some("high") => vault_core::types::Priority::High, + Some("low") => vault_core::types::Priority::Low, + _ => vault_core::types::Priority::Medium, + }; + + let status_dir = match priority { + vault_core::types::Priority::Urgent => "urgent", + _ => "open", + }; + + let slug = filesystem::timestamped_slug(&body.title); + let path = state + .vault_root + .join("todos/harald") + .join(status_dir) + .join(format!("{}.md", slug)); + + let due = body + .due + .and_then(|d| chrono::DateTime::parse_from_rfc3339(&d).ok()) + .map(|d| d.with_timezone(&chrono::Utc)); + + let task = HumanTask { + title: body.title, + priority, + source: Some("dashboard".into()), + repo: body.repo, + labels: body.labels, + created: chrono::Utc::now(), + due, + }; + + let entity = VaultEntity { + path: path.clone(), + frontmatter: task, + body: body.body.unwrap_or_default(), + }; + + state.write_filter.register(path.clone()); + filesystem::write_entity(&entity).map_err(ApiError::Vault)?; + + Ok(Json(json!({ + "status": "created", + "path": path.strip_prefix(&state.vault_root).unwrap_or(&path), + }))) +} + +#[derive(Deserialize)] +struct MoveBody { + to: String, +} + +async fn move_task( + State(state): State>, + Path((status, id)): Path<(String, String)>, + Json(body): Json, +) -> Result, ApiError> { + let from = state + .vault_root + .join("todos/harald") + .join(&status) + .join(format!("{}.md", id)); + + if !from.exists() { + return Err(ApiError::NotFound(format!("Task '{}' not found in {}", id, status))); + } + + let to = state + .vault_root + .join("todos/harald") + .join(&body.to) + .join(format!("{}.md", id)); + + state.write_filter.register(to.clone()); + filesystem::move_file(&from, &to).map_err(ApiError::Vault)?; + + Ok(Json(json!({ + "status": "moved", + "from": status, + "to": body.to, + }))) +} + +async fn delete_task( + State(state): State>, + Path((status, id)): Path<(String, String)>, +) -> Result, ApiError> { + let path = state + .vault_root + .join("todos/harald") + .join(&status) + .join(format!("{}.md", id)); + + if !path.exists() { + return Err(ApiError::NotFound(format!("Task '{}' not found", id))); + } + + std::fs::remove_file(&path).map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &path)))?; + + Ok(Json(json!({ "status": "deleted" }))) +} + +fn task_to_json(entity: &VaultEntity, status: &str) -> Value { + let id = entity + .path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown"); + + json!({ + "id": id, + "title": entity.frontmatter.title, + "priority": entity.frontmatter.priority, + "status": status, + "source": entity.frontmatter.source, + "repo": entity.frontmatter.repo, + "labels": entity.frontmatter.labels, + "created": entity.frontmatter.created, + "due": entity.frontmatter.due, + "body": entity.body, + }) +} diff --git a/crates/vault-api/src/routes/tree.rs b/crates/vault-api/src/routes/tree.rs new file mode 100644 index 0000000..957b517 --- /dev/null +++ b/crates/vault-api/src/routes/tree.rs @@ -0,0 +1,93 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde_json::{json, Value}; +use std::sync::Arc; + +pub fn routes() -> Router> { + Router::new() + .route("/tree", get(get_tree)) + .route("/tree/{*path}", post(create_dir).delete(delete_dir)) +} + +async fn get_tree(State(state): State>) -> Result, ApiError> { + let tree = build_tree(&state.vault_root, &state.vault_root)?; + Ok(Json(tree)) +} + +fn build_tree(root: &std::path::Path, dir: &std::path::Path) -> Result { + let mut children = Vec::new(); + + let entries = std::fs::read_dir(dir) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, dir)))?; + + let mut entries: Vec<_> = entries + .filter_map(|e| e.ok()) + .collect(); + entries.sort_by_key(|e| e.file_name()); + + for entry in entries { + let path = entry.path(); + let name = entry.file_name().to_string_lossy().to_string(); + + // Skip hidden files/dirs + if name.starts_with('.') { + continue; + } + + let relative = path.strip_prefix(root).unwrap_or(&path); + + if path.is_dir() { + let subtree = build_tree(root, &path)?; + children.push(json!({ + "name": name, + "path": relative, + "type": "directory", + "children": subtree.get("children").unwrap_or(&json!([])), + })); + } else { + children.push(json!({ + "name": name, + "path": relative, + "type": "file", + })); + } + } + + Ok(json!({ + "name": dir.file_name().and_then(|n| n.to_str()).unwrap_or("vault"), + "path": dir.strip_prefix(root).unwrap_or(dir), + "type": "directory", + "children": children, + })) +} + +async fn create_dir( + State(state): State>, + Path(path): Path, +) -> Result, ApiError> { + let dir_path = state.vault_root.join(&path); + + std::fs::create_dir_all(&dir_path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &dir_path)))?; + + Ok(Json(json!({ "status": "created", "path": path }))) +} + +async fn delete_dir( + State(state): State>, + Path(path): Path, +) -> Result, ApiError> { + let dir_path = state.vault_root.join(&path); + + if !dir_path.exists() { + return Err(ApiError::NotFound(format!("Directory '{}' not found", path))); + } + + std::fs::remove_dir_all(&dir_path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &dir_path)))?; + + Ok(Json(json!({ "status": "deleted", "path": path }))) +} diff --git a/crates/vault-api/src/routes/validate.rs b/crates/vault-api/src/routes/validate.rs new file mode 100644 index 0000000..57f2d50 --- /dev/null +++ b/crates/vault-api/src/routes/validate.rs @@ -0,0 +1,78 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::State; +use axum::routing::post; +use axum::{Json, Router}; +use serde::Deserialize; +use std::collections::HashSet; +use std::path::Path; +use std::sync::Arc; +use vault_core::validation; + +#[derive(Debug, Deserialize)] +pub struct ValidateRequest { + /// Relative path within the vault + pub path: String, + /// Raw file content to validate (optional; if omitted, reads from disk) + pub content: Option, +} + +pub fn routes() -> Router> { + Router::new().route("/validate", post(validate)) +} + +async fn validate( + State(state): State>, + Json(req): Json, +) -> Result, ApiError> { + let relative = Path::new(&req.path); + + let content = if let Some(c) = req.content { + c + } else { + let full = state.vault_root.join(&req.path); + tokio::fs::read_to_string(&full) + .await + .map_err(|e| ApiError::NotFound(format!("File not found: {} ({})", req.path, e)))? + }; + + let issues = validation::validate(relative, &content); + + // Also check references + let agent_names: HashSet = state + .agents + .read() + .unwrap() + .keys() + .cloned() + .collect(); + let skill_names: HashSet = state + .skills + .read() + .unwrap() + .keys() + .cloned() + .collect(); + let ref_issues = validation::validate_references(&state.vault_root, &agent_names, &skill_names); + + let mut all_issues: Vec = issues + .into_iter() + .map(|i| serde_json::to_value(i).unwrap_or_default()) + .collect(); + + for (entity, issue) in ref_issues { + let mut val = serde_json::to_value(&issue).unwrap_or_default(); + if let Some(obj) = val.as_object_mut() { + obj.insert("entity".into(), serde_json::Value::String(entity)); + } + all_issues.push(val); + } + + Ok(Json(serde_json::json!({ + "path": req.path, + "issues": all_issues, + "valid": all_issues.iter().all(|i| + i.get("level").and_then(|l| l.as_str()) != Some("error") + ), + }))) +} diff --git a/crates/vault-api/src/routes/views.rs b/crates/vault-api/src/routes/views.rs new file mode 100644 index 0000000..1ce1318 --- /dev/null +++ b/crates/vault-api/src/routes/views.rs @@ -0,0 +1,214 @@ +use crate::error::ApiError; +use crate::state::AppState; +use axum::extract::{Path, State}; +use axum::routing::get; +use axum::{Json, Router}; +use serde_json::{json, Value}; +use std::sync::Arc; +use vault_core::filesystem; +use vault_core::types::{Notification, ViewDefinition}; + +pub fn routes() -> Router> { + Router::new() + .route("/views/pages", get(list_pages)) + .route("/views/widgets", get(list_widgets)) + .route("/views/layouts", get(list_layouts)) + .route("/views/{*path}", get(get_view).put(put_view).delete(delete_view)) + .route("/notifications", get(list_notifications)) + .route( + "/notifications/{id}", + axum::routing::delete(dismiss_notification), + ) +} + +async fn list_pages(State(state): State>) -> Result, ApiError> { + list_view_dir(&state, "views/pages").await +} + +async fn list_widgets(State(state): State>) -> Result, ApiError> { + list_view_dir(&state, "views/widgets").await +} + +async fn list_layouts(State(state): State>) -> Result, ApiError> { + list_view_dir(&state, "views/layouts").await +} + +async fn list_view_dir(state: &AppState, subdir: &str) -> Result, ApiError> { + let dir = state.vault_root.join(subdir); + let files = filesystem::list_md_files(&dir).map_err(ApiError::Vault)?; + + let mut views = Vec::new(); + for file in files { + match filesystem::read_entity::(&file) { + Ok(entity) => { + let name = file.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown"); + views.push(json!({ + "name": name, + "type": entity.frontmatter.view_type, + "title": entity.frontmatter.title, + "icon": entity.frontmatter.icon, + "route": entity.frontmatter.route, + "position": entity.frontmatter.position, + "layout": entity.frontmatter.layout, + "component": entity.frontmatter.component, + "description": entity.frontmatter.description, + })); + } + Err(e) => { + tracing::warn!(path = ?file, error = %e, "Failed to read view definition"); + } + } + } + + views.sort_by_key(|v| v.get("position").and_then(|p| p.as_i64()).unwrap_or(999)); + Ok(Json(json!(views))) +} + +async fn get_view( + State(state): State>, + Path(path): Path, +) -> Result, ApiError> { + let file_path = state.vault_root.join("views").join(&path); + let file_path = if file_path.extension().is_none() { + file_path.with_extension("md") + } else { + file_path + }; + + if !file_path.exists() { + return Err(ApiError::NotFound(format!("View '{}' not found", path))); + } + + let content = std::fs::read_to_string(&file_path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + if let Ok((yaml, body)) = vault_core::frontmatter::split_frontmatter(&content) { + let frontmatter: Value = serde_yaml::from_str(yaml).unwrap_or(Value::Null); + Ok(Json(json!({ + "path": path, + "frontmatter": frontmatter, + "body": body, + }))) + } else { + Ok(Json(json!({ + "path": path, + "frontmatter": null, + "body": content, + }))) + } +} + +async fn put_view( + State(state): State>, + Path(path): Path, + Json(data): Json, +) -> Result, ApiError> { + let file_path = state.vault_root.join("views").join(&path); + let file_path = if file_path.extension().is_none() { + file_path.with_extension("md") + } else { + file_path + }; + + if let Some(parent) = file_path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, parent)))?; + } + + let content = if let Some(raw) = data.get("raw").and_then(|r| r.as_str()) { + raw.to_string() + } else { + let body = data.get("body").and_then(|b| b.as_str()).unwrap_or(""); + if let Some(fm) = data.get("frontmatter") { + let yaml = serde_yaml::to_string(fm).map_err(|e| ApiError::Internal(e.to_string()))?; + format!("---\n{}---\n{}", yaml, body) + } else { + body.to_string() + } + }; + + state.write_filter.register(file_path.clone()); + std::fs::write(&file_path, content) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + Ok(Json(json!({ "status": "saved", "path": path }))) +} + +async fn delete_view( + State(state): State>, + Path(path): Path, +) -> Result, ApiError> { + let file_path = state.vault_root.join("views").join(&path); + let file_path = if file_path.extension().is_none() { + file_path.with_extension("md") + } else { + file_path + }; + + if !file_path.exists() { + return Err(ApiError::NotFound(format!("View '{}' not found", path))); + } + + std::fs::remove_file(&file_path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?; + + Ok(Json(json!({ "status": "deleted", "path": path }))) +} + +async fn list_notifications(State(state): State>) -> Result, ApiError> { + let dir = state.vault_root.join("views/notifications"); + let files = filesystem::list_md_files(&dir).map_err(ApiError::Vault)?; + + let mut notifications = Vec::new(); + let now = chrono::Utc::now(); + + for file in files { + match filesystem::read_entity::(&file) { + Ok(entity) => { + // Skip expired notifications + if let Some(expires) = entity.frontmatter.expires { + if expires < now { + // Auto-clean expired + let _ = std::fs::remove_file(&file); + continue; + } + } + + let id = file.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown"); + notifications.push(json!({ + "id": id, + "title": entity.frontmatter.title, + "message": entity.frontmatter.message, + "level": entity.frontmatter.level, + "source": entity.frontmatter.source, + "created": entity.frontmatter.created, + "expires": entity.frontmatter.expires, + })); + } + Err(e) => { + tracing::warn!(path = ?file, error = %e, "Failed to read notification"); + } + } + } + + Ok(Json(json!(notifications))) +} + +async fn dismiss_notification( + State(state): State>, + Path(id): Path, +) -> Result, ApiError> { + let path = state + .vault_root + .join("views/notifications") + .join(format!("{}.md", id)); + + if !path.exists() { + return Err(ApiError::NotFound(format!("Notification '{}' not found", id))); + } + + std::fs::remove_file(&path) + .map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &path)))?; + + Ok(Json(json!({ "status": "dismissed" }))) +} diff --git a/crates/vault-api/src/state.rs b/crates/vault-api/src/state.rs new file mode 100644 index 0000000..9f6e7d4 --- /dev/null +++ b/crates/vault-api/src/state.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::{Arc, Mutex, RwLock}; +use vault_core::config::VaultConfig; +use vault_core::entity::VaultEntity; +use vault_core::filesystem; +use vault_core::types::{Agent, Skill}; +use vault_scheduler::cron_engine::CronEngine; +use vault_scheduler::executor::Executor; +use vault_scheduler::executors::process::GenericProcessExecutor; +use vault_scheduler::state::RuntimeState; +use vault_scheduler::task_runner::TaskRunner; +use vault_watch::events::VaultEvent; +use vault_watch::write_filter::DaemonWriteFilter; + +pub struct AppState { + pub vault_root: PathBuf, + pub config: VaultConfig, + pub cron_engine: Mutex, + pub write_filter: Arc, + pub event_tx: tokio::sync::broadcast::Sender>, + pub agents: RwLock>>, + pub skills: RwLock>>, + pub runtime_state: Mutex, + pub startup_time: chrono::DateTime, + executor: Arc, + max_parallel: usize, +} + +impl AppState { + pub fn new(vault_root: PathBuf, config: VaultConfig, max_parallel: usize) -> Self { + let (event_tx, _) = tokio::sync::broadcast::channel(256); + let write_filter = Arc::new(DaemonWriteFilter::new()); + let executor: Arc = + Arc::new(GenericProcessExecutor::new(vault_root.clone())); + + let now = chrono::Utc::now(); + let mut runtime_state = RuntimeState::load(&vault_root).unwrap_or_default(); + runtime_state.last_startup = Some(now); + let _ = runtime_state.save(&vault_root); + + Self { + cron_engine: Mutex::new(CronEngine::new(vault_root.clone())), + vault_root, + config, + write_filter, + event_tx, + agents: RwLock::new(HashMap::new()), + skills: RwLock::new(HashMap::new()), + runtime_state: Mutex::new(runtime_state), + startup_time: now, + executor, + max_parallel, + } + } + + pub fn task_runner(&self) -> TaskRunner { + TaskRunner::new( + self.vault_root.clone(), + self.max_parallel, + self.executor.clone(), + self.write_filter.clone(), + ) + } + + /// Load all agent and skill definitions from disk. + pub fn reload_definitions(&self) -> Result<(), vault_core::VaultError> { + // Load agents + let agent_files = filesystem::list_md_files(&self.vault_root.join("agents"))?; + let mut agents = HashMap::new(); + for path in agent_files { + match filesystem::read_entity::(&path) { + Ok(entity) => { + agents.insert(entity.frontmatter.name.clone(), entity); + } + Err(e) => { + tracing::warn!(path = ?path, error = %e, "Failed to load agent"); + } + } + } + tracing::info!(count = agents.len(), "Loaded agents"); + *self.agents.write().unwrap() = agents; + + // Load skills + let skill_files = + filesystem::list_md_files_recursive(&self.vault_root.join("skills"))?; + let mut skills = HashMap::new(); + for path in skill_files { + match filesystem::read_entity::(&path) { + Ok(entity) => { + skills.insert(entity.frontmatter.name.clone(), entity); + } + Err(e) => { + tracing::warn!(path = ?path, error = %e, "Failed to load skill"); + } + } + } + tracing::info!(count = skills.len(), "Loaded skills"); + *self.skills.write().unwrap() = skills; + + Ok(()) + } + + pub fn broadcast(&self, event: VaultEvent) { + let _ = self.event_tx.send(Arc::new(event)); + } + + pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver> { + self.event_tx.subscribe() + } +} diff --git a/crates/vault-api/src/ws.rs b/crates/vault-api/src/ws.rs new file mode 100644 index 0000000..3e8008d --- /dev/null +++ b/crates/vault-api/src/ws.rs @@ -0,0 +1,129 @@ +use crate::state::AppState; +use crate::ws_protocol::{WsAction, WsEvent}; +use axum::extract::ws::{Message, WebSocket}; +use axum::extract::{State, WebSocketUpgrade}; +use axum::response::Response; +use std::sync::Arc; + +pub async fn ws_handler( + ws: WebSocketUpgrade, + State(state): State>, +) -> Response { + ws.on_upgrade(move |socket| handle_socket(socket, state)) +} + +async fn handle_socket(socket: WebSocket, state: Arc) { + let (mut sender, mut receiver) = socket.split(); + use futures_util::{SinkExt, StreamExt}; + + let mut event_rx = state.subscribe(); + + // Send task: forward vault events to the client + let send_state = state.clone(); + let send_task = tokio::spawn(async move { + while let Ok(event) = event_rx.recv().await { + let ws_event = WsEvent::from_vault_event(&event, &send_state.vault_root); + match serde_json::to_string(&ws_event) { + Ok(json) => { + if sender.send(Message::Text(json.into())).await.is_err() { + break; + } + } + Err(e) => { + tracing::warn!(error = %e, "Failed to serialize WS event"); + } + } + } + }); + + // Receive task: handle client actions + let recv_state = state.clone(); + let recv_task = tokio::spawn(async move { + while let Some(msg) = receiver.next().await { + match msg { + Ok(Message::Text(text)) => { + match serde_json::from_str::(&text) { + Ok(action) => handle_action(&recv_state, action).await, + Err(e) => { + tracing::warn!(error = %e, text = %text, "Invalid WS action"); + } + } + } + Ok(Message::Close(_)) => break, + Err(e) => { + tracing::debug!(error = %e, "WebSocket error"); + break; + } + _ => {} + } + } + }); + + // Wait for either task to finish + tokio::select! { + _ = send_task => {}, + _ = recv_task => {}, + } + + tracing::debug!("WebSocket connection closed"); +} + +async fn handle_action(state: &AppState, action: WsAction) { + match action { + WsAction::MoveTask { from, to } => { + let from_path = state.vault_root.join(&from); + let to_path = state.vault_root.join(&to); + state.write_filter.register(to_path.clone()); + if let Err(e) = vault_core::filesystem::move_file(&from_path, &to_path) { + tracing::error!(error = %e, "WS move_task failed"); + } + } + WsAction::TriggerCron { name } => { + let cron_path = state + .vault_root + .join("crons/active") + .join(format!("{}.md", name)); + let mut engine = state.cron_engine.lock().unwrap(); + if let Err(e) = engine.fire_cron(&cron_path, &state.write_filter) { + tracing::error!(error = %e, "WS trigger_cron failed"); + } + } + WsAction::TriggerAgent { name, context } => { + let title = format!("WS trigger: {}", name); + let slug = vault_core::filesystem::timestamped_slug(&title); + let task_path = state + .vault_root + .join("todos/agent/queued") + .join(format!("{}.md", slug)); + + let task = vault_core::types::AgentTask { + title, + agent: name, + priority: vault_core::types::Priority::Medium, + task_type: Some("ws-trigger".into()), + created: chrono::Utc::now(), + started: None, + completed: None, + retry: 0, + max_retries: 0, + input: None, + output: None, + error: None, + }; + + let entity = vault_core::entity::VaultEntity { + path: task_path.clone(), + frontmatter: task, + body: context.unwrap_or_default(), + }; + + state.write_filter.register(task_path.clone()); + if let Err(e) = vault_core::filesystem::write_entity(&entity) { + tracing::error!(error = %e, "WS trigger_agent failed"); + } + } + WsAction::Ping => { + tracing::debug!("WS ping received"); + } + } +} diff --git a/crates/vault-api/src/ws_protocol.rs b/crates/vault-api/src/ws_protocol.rs new file mode 100644 index 0000000..1a1a08c --- /dev/null +++ b/crates/vault-api/src/ws_protocol.rs @@ -0,0 +1,76 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::path::Path; +use vault_watch::events::VaultEvent; + +/// Server -> Client event +#[derive(Debug, Clone, Serialize)] +pub struct WsEvent { + #[serde(rename = "type")] + pub event_type: String, + pub area: String, + pub path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +impl WsEvent { + pub fn from_vault_event(event: &VaultEvent, vault_root: &Path) -> Self { + let path = event.path(); + let relative = path + .strip_prefix(vault_root) + .unwrap_or(path) + .to_string_lossy() + .to_string(); + + // Derive area from relative path (first two components) + let area = relative + .split('/') + .take(2) + .collect::>() + .join("/"); + + // Try to read frontmatter data + let data = if path.exists() { + std::fs::read_to_string(path) + .ok() + .and_then(|content| { + vault_core::frontmatter::split_frontmatter(&content) + .ok() + .and_then(|(yaml, _)| serde_yaml::from_str::(yaml).ok()) + }) + } else { + None + }; + + Self { + event_type: event.event_type().to_string(), + area, + path: relative, + data, + } + } +} + +/// Client -> Server action +#[derive(Debug, Deserialize)] +#[serde(tag = "action")] +pub enum WsAction { + #[serde(rename = "move_task")] + MoveTask { + from: String, + to: String, + }, + #[serde(rename = "trigger_cron")] + TriggerCron { + name: String, + }, + #[serde(rename = "trigger_agent")] + TriggerAgent { + name: String, + #[serde(default)] + context: Option, + }, + #[serde(rename = "ping")] + Ping, +} diff --git a/crates/vault-core/Cargo.toml b/crates/vault-core/Cargo.toml new file mode 100644 index 0000000..d2140e6 --- /dev/null +++ b/crates/vault-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "vault-core" +version.workspace = true +edition.workspace = true + +[dependencies] +serde.workspace = true +serde_yaml.workspace = true +serde_json.workspace = true +chrono.workspace = true +thiserror.workspace = true +uuid.workspace = true +tracing.workspace = true +cron.workspace = true diff --git a/crates/vault-core/src/config.rs b/crates/vault-core/src/config.rs new file mode 100644 index 0000000..14825d3 --- /dev/null +++ b/crates/vault-core/src/config.rs @@ -0,0 +1,101 @@ +use crate::error::VaultError; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::Path; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct VaultConfig { + #[serde(default)] + pub mcp_servers: HashMap, + #[serde(default)] + pub executors: HashMap, + #[serde(default)] + pub queue: QueueConfig, + #[serde(default)] + pub assistant: AssistantConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpServerConfig { + pub command: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub env: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExecutorConfig { + #[serde(default)] + pub command: Option, + #[serde(default)] + pub base_url: Option, + #[serde(default)] + pub default_model: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueueConfig { + #[serde(default = "default_max_parallel")] + pub max_parallel: usize, + #[serde(default = "default_timeout")] + pub default_timeout: u64, + #[serde(default = "default_retry_delay")] + pub retry_delay: u64, +} + +impl Default for QueueConfig { + fn default() -> Self { + Self { + max_parallel: default_max_parallel(), + default_timeout: default_timeout(), + retry_delay: default_retry_delay(), + } + } +} + +fn default_max_parallel() -> usize { + 4 +} +fn default_timeout() -> u64 { + 600 +} +fn default_retry_delay() -> u64 { + 60 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssistantConfig { + #[serde(default = "default_assistant_model")] + pub default_model: String, + #[serde(default)] + pub models: Vec, +} + +impl Default for AssistantConfig { + fn default() -> Self { + Self { + default_model: default_assistant_model(), + models: vec![], + } + } +} + +fn default_assistant_model() -> String { + "local/qwen3".into() +} + +impl VaultConfig { + /// Load config from `.vault/config.yaml` in the vault root. + /// Returns default config if file doesn't exist. + pub fn load(vault_root: &Path) -> Result { + let config_path = vault_root.join(".vault/config.yaml"); + if !config_path.exists() { + return Ok(Self::default()); + } + let content = std::fs::read_to_string(&config_path) + .map_err(|e| VaultError::io(e, &config_path))?; + let config: VaultConfig = serde_yaml::from_str(&content)?; + Ok(config) + } +} diff --git a/crates/vault-core/src/entity.rs b/crates/vault-core/src/entity.rs new file mode 100644 index 0000000..cf33d86 --- /dev/null +++ b/crates/vault-core/src/entity.rs @@ -0,0 +1,195 @@ +use crate::error::VaultError; +use crate::types::{AgentTaskStatus, TaskStatus}; +use serde::{de::DeserializeOwned, Serialize}; +use std::path::{Path, PathBuf}; + +/// A vault entity: parsed frontmatter + markdown body + file path. +#[derive(Debug, Clone)] +pub struct VaultEntity { + pub path: PathBuf, + pub frontmatter: T, + pub body: String, +} + +/// The kind of entity inferred from its relative path within the vault. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EntityKind { + Agent, + Skill, + CronActive, + CronPaused, + CronTemplate, + HumanTask(TaskStatus), + AgentTask(AgentTaskStatus), + Knowledge, + ViewPage, + ViewWidget, + ViewLayout, + ViewCustom, + Notification, + Unknown, +} + +/// Classify a relative path within the vault to determine entity kind. +pub fn classify_path(relative: &Path) -> EntityKind { + let components: Vec<&str> = relative + .components() + .filter_map(|c| c.as_os_str().to_str()) + .collect(); + + match components.as_slice() { + ["agents", ..] => EntityKind::Agent, + ["skills", ..] => EntityKind::Skill, + ["crons", "active", ..] => EntityKind::CronActive, + ["crons", "paused", ..] => EntityKind::CronPaused, + ["crons", "templates", ..] => EntityKind::CronTemplate, + ["todos", "harald", status, ..] => { + EntityKind::HumanTask(task_status_from_dir(status)) + } + ["todos", "agent", status, ..] => { + EntityKind::AgentTask(agent_task_status_from_dir(status)) + } + ["knowledge", ..] => EntityKind::Knowledge, + ["views", "pages", ..] => EntityKind::ViewPage, + ["views", "widgets", ..] => EntityKind::ViewWidget, + ["views", "layouts", ..] => EntityKind::ViewLayout, + ["views", "custom", ..] => EntityKind::ViewCustom, + ["views", "notifications", ..] => EntityKind::Notification, + _ => EntityKind::Unknown, + } +} + +pub fn task_status_from_dir(dir: &str) -> TaskStatus { + match dir { + "urgent" => TaskStatus::Urgent, + "open" => TaskStatus::Open, + "in-progress" => TaskStatus::InProgress, + "done" => TaskStatus::Done, + _ => TaskStatus::Open, + } +} + +pub fn agent_task_status_from_dir(dir: &str) -> AgentTaskStatus { + match dir { + "queued" => AgentTaskStatus::Queued, + "running" => AgentTaskStatus::Running, + "done" => AgentTaskStatus::Done, + "failed" => AgentTaskStatus::Failed, + _ => AgentTaskStatus::Queued, + } +} + +pub fn task_status_dir(status: &TaskStatus) -> &'static str { + match status { + TaskStatus::Urgent => "urgent", + TaskStatus::Open => "open", + TaskStatus::InProgress => "in-progress", + TaskStatus::Done => "done", + } +} + +pub fn agent_task_status_dir(status: &AgentTaskStatus) -> &'static str { + match status { + AgentTaskStatus::Queued => "queued", + AgentTaskStatus::Running => "running", + AgentTaskStatus::Done => "done", + AgentTaskStatus::Failed => "failed", + } +} + +impl VaultEntity +where + T: DeserializeOwned + Serialize, +{ + pub fn from_content(path: PathBuf, content: &str) -> Result { + let (yaml, body) = + crate::frontmatter::split_frontmatter_with_path(content, &path)?; + let frontmatter: T = crate::frontmatter::parse_entity(yaml)?; + Ok(Self { + path, + frontmatter, + body: body.to_string(), + }) + } + + pub fn to_string(&self) -> Result { + crate::frontmatter::write_frontmatter(&self.frontmatter, &self.body) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_classify_agent() { + assert_eq!( + classify_path(Path::new("agents/reviewer.md")), + EntityKind::Agent + ); + } + + #[test] + fn test_classify_skill() { + assert_eq!( + classify_path(Path::new("skills/vault/read-vault.md")), + EntityKind::Skill + ); + } + + #[test] + fn test_classify_cron() { + assert_eq!( + classify_path(Path::new("crons/active/daily-review.md")), + EntityKind::CronActive + ); + assert_eq!( + classify_path(Path::new("crons/paused/old-job.md")), + EntityKind::CronPaused + ); + } + + #[test] + fn test_classify_human_task() { + assert_eq!( + classify_path(Path::new("todos/harald/urgent/fix-bug.md")), + EntityKind::HumanTask(TaskStatus::Urgent) + ); + assert_eq!( + classify_path(Path::new("todos/harald/in-progress/feature.md")), + EntityKind::HumanTask(TaskStatus::InProgress) + ); + } + + #[test] + fn test_classify_agent_task() { + assert_eq!( + classify_path(Path::new("todos/agent/queued/task-1.md")), + EntityKind::AgentTask(AgentTaskStatus::Queued) + ); + assert_eq!( + classify_path(Path::new("todos/agent/running/task-2.md")), + EntityKind::AgentTask(AgentTaskStatus::Running) + ); + } + + #[test] + fn test_classify_knowledge() { + assert_eq!( + classify_path(Path::new("knowledge/notes/rust-tips.md")), + EntityKind::Knowledge + ); + } + + #[test] + fn test_classify_views() { + assert_eq!( + classify_path(Path::new("views/pages/home.md")), + EntityKind::ViewPage + ); + assert_eq!( + classify_path(Path::new("views/notifications/alert.md")), + EntityKind::Notification + ); + } +} diff --git a/crates/vault-core/src/error.rs b/crates/vault-core/src/error.rs new file mode 100644 index 0000000..46c49d7 --- /dev/null +++ b/crates/vault-core/src/error.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; + +#[derive(Debug, thiserror::Error)] +pub enum VaultError { + #[error("IO error: {source} (path: {path:?})")] + Io { + source: std::io::Error, + path: PathBuf, + }, + + #[error("YAML parsing error: {0}")] + Yaml(#[from] serde_yaml::Error), + + #[error("Missing frontmatter in {0}")] + MissingFrontmatter(PathBuf), + + #[error("Invalid entity at {path}: {reason}")] + InvalidEntity { path: PathBuf, reason: String }, + + #[error("Not found: {0}")] + NotFound(String), + + #[error("Broken reference from {from} to {to}")] + BrokenReference { from: PathBuf, to: String }, +} + +impl VaultError { + pub fn io(source: std::io::Error, path: impl Into) -> Self { + Self::Io { + source, + path: path.into(), + } + } +} diff --git a/crates/vault-core/src/filesystem.rs b/crates/vault-core/src/filesystem.rs new file mode 100644 index 0000000..bcaf267 --- /dev/null +++ b/crates/vault-core/src/filesystem.rs @@ -0,0 +1,167 @@ +use crate::entity::VaultEntity; +use crate::error::VaultError; +use serde::{de::DeserializeOwned, Serialize}; +use std::path::{Path, PathBuf}; + +/// Read and parse a vault entity from a markdown file. +pub fn read_entity(path: &Path) -> Result, VaultError> { + let content = + std::fs::read_to_string(path).map_err(|e| VaultError::io(e, path))?; + VaultEntity::from_content(path.to_path_buf(), &content) +} + +/// Write a vault entity to disk. +pub fn write_entity(entity: &VaultEntity) -> Result<(), VaultError> { + let content = entity.to_string()?; + if let Some(parent) = entity.path.parent() { + std::fs::create_dir_all(parent).map_err(|e| VaultError::io(e, parent))?; + } + std::fs::write(&entity.path, content).map_err(|e| VaultError::io(e, &entity.path)) +} + +/// Move a file from one path to another, creating parent dirs as needed. +pub fn move_file(from: &Path, to: &Path) -> Result<(), VaultError> { + if let Some(parent) = to.parent() { + std::fs::create_dir_all(parent).map_err(|e| VaultError::io(e, parent))?; + } + std::fs::rename(from, to).map_err(|e| VaultError::io(e, from)) +} + +/// Ensure the standard vault directory structure exists. +pub fn ensure_vault_structure(vault_root: &Path) -> Result<(), VaultError> { + let dirs = [ + "agents", + "skills/vault", + "crons/active", + "crons/paused", + "crons/templates", + "todos/harald/urgent", + "todos/harald/open", + "todos/harald/in-progress", + "todos/harald/done", + "todos/agent/queued", + "todos/agent/running", + "todos/agent/done", + "todos/agent/failed", + "knowledge", + "views/pages", + "views/widgets", + "views/layouts", + "views/custom", + "views/notifications", + ".vault/logs", + ".vault/templates", + ]; + + for dir in &dirs { + let path = vault_root.join(dir); + std::fs::create_dir_all(&path).map_err(|e| VaultError::io(e, &path))?; + } + + Ok(()) +} + +/// List all .md files in a directory (non-recursive). +pub fn list_md_files(dir: &Path) -> Result, VaultError> { + if !dir.exists() { + return Ok(vec![]); + } + let mut files = Vec::new(); + let entries = std::fs::read_dir(dir).map_err(|e| VaultError::io(e, dir))?; + for entry in entries { + let entry = entry.map_err(|e| VaultError::io(e, dir))?; + let path = entry.path(); + if path.is_file() && path.extension().is_some_and(|e| e == "md") { + files.push(path); + } + } + files.sort(); + Ok(files) +} + +/// List all .md files in a directory tree (recursive). +pub fn list_md_files_recursive(dir: &Path) -> Result, VaultError> { + if !dir.exists() { + return Ok(vec![]); + } + let mut files = Vec::new(); + walk_dir_recursive(dir, &mut files)?; + files.sort(); + Ok(files) +} + +fn walk_dir_recursive(dir: &Path, files: &mut Vec) -> Result<(), VaultError> { + let entries = std::fs::read_dir(dir).map_err(|e| VaultError::io(e, dir))?; + for entry in entries { + let entry = entry.map_err(|e| VaultError::io(e, dir))?; + let path = entry.path(); + if path.is_dir() { + // Skip dotfiles/dirs + if path + .file_name() + .is_some_and(|n| n.to_str().is_some_and(|s| s.starts_with('.'))) + { + continue; + } + walk_dir_recursive(&path, files)?; + } else if path.is_file() && path.extension().is_some_and(|e| e == "md") { + files.push(path); + } + } + Ok(()) +} + +/// Convert a string to a URL-safe slug. +pub fn slugify(s: &str) -> String { + s.to_lowercase() + .chars() + .map(|c| { + if c.is_alphanumeric() { + c + } else { + '-' + } + }) + .collect::() + .split('-') + .filter(|s| !s.is_empty()) + .collect::>() + .join("-") +} + +/// Create a timestamped slug: `YYYYMMDD-HHMMSS-slug` +pub fn timestamped_slug(title: &str) -> String { + let now = chrono::Utc::now(); + format!("{}-{}", now.format("%Y%m%d-%H%M%S"), slugify(title)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_slugify() { + assert_eq!(slugify("Hello World!"), "hello-world"); + assert_eq!(slugify("Review PR #1234"), "review-pr-1234"); + assert_eq!(slugify(" spaces everywhere "), "spaces-everywhere"); + } + + #[test] + fn test_timestamped_slug() { + let slug = timestamped_slug("My Task"); + assert!(slug.ends_with("-my-task")); + assert!(slug.len() > 20); + } + + #[test] + fn test_ensure_vault_structure() { + let tmp = std::env::temp_dir().join("vault-os-test-structure"); + let _ = std::fs::remove_dir_all(&tmp); + ensure_vault_structure(&tmp).unwrap(); + assert!(tmp.join("agents").is_dir()); + assert!(tmp.join("todos/harald/urgent").is_dir()); + assert!(tmp.join("todos/agent/queued").is_dir()); + assert!(tmp.join(".vault/logs").is_dir()); + let _ = std::fs::remove_dir_all(&tmp); + } +} diff --git a/crates/vault-core/src/frontmatter.rs b/crates/vault-core/src/frontmatter.rs new file mode 100644 index 0000000..20e8254 --- /dev/null +++ b/crates/vault-core/src/frontmatter.rs @@ -0,0 +1,180 @@ +use crate::error::VaultError; +use serde::{de::DeserializeOwned, Serialize}; +use std::path::Path; + +const DELIMITER: &str = "---"; + +/// Split a markdown file into frontmatter YAML and body. +/// Returns (yaml_str, body_str). Body preserves original content byte-for-byte. +pub fn split_frontmatter(content: &str) -> Result<(&str, &str), VaultError> { + let trimmed = content.trim_start(); + if !trimmed.starts_with(DELIMITER) { + return Err(VaultError::MissingFrontmatter( + "".into(), + )); + } + + // Find the opening delimiter + let after_first = &trimmed[DELIMITER.len()..]; + let after_first = after_first.strip_prefix('\n').unwrap_or( + after_first.strip_prefix("\r\n").unwrap_or(after_first), + ); + + // Find the closing delimiter + if let Some(end_pos) = find_closing_delimiter(after_first) { + let yaml = &after_first[..end_pos]; + let rest = &after_first[end_pos + DELIMITER.len()..]; + // Skip the newline after closing --- + let body = rest + .strip_prefix('\n') + .unwrap_or(rest.strip_prefix("\r\n").unwrap_or(rest)); + Ok((yaml, body)) + } else { + Err(VaultError::MissingFrontmatter( + "".into(), + )) + } +} + +/// Split frontmatter with path context for error messages. +pub fn split_frontmatter_with_path<'a>( + content: &'a str, + path: &Path, +) -> Result<(&'a str, &'a str), VaultError> { + split_frontmatter(content).map_err(|e| match e { + VaultError::MissingFrontmatter(_) => VaultError::MissingFrontmatter(path.to_path_buf()), + other => other, + }) +} + +fn find_closing_delimiter(s: &str) -> Option { + for (i, line) in s.lines().enumerate() { + if line.trim() == DELIMITER { + // Calculate byte offset + let offset: usize = s.lines().take(i).map(|l| l.len() + 1).sum(); + return Some(offset); + } + } + None +} + +/// Parse frontmatter YAML into a typed struct. +pub fn parse_entity(yaml: &str) -> Result { + serde_yaml::from_str(yaml).map_err(VaultError::Yaml) +} + +/// Serialize frontmatter and combine with body, preserving body byte-for-byte. +pub fn write_frontmatter(frontmatter: &T, body: &str) -> Result { + let yaml = serde_yaml::to_string(frontmatter).map_err(VaultError::Yaml)?; + let mut out = String::new(); + out.push_str(DELIMITER); + out.push('\n'); + out.push_str(&yaml); + // serde_yaml adds trailing newline, but ensure delimiter is on its own line + if !yaml.ends_with('\n') { + out.push('\n'); + } + out.push_str(DELIMITER); + out.push('\n'); + if !body.is_empty() { + out.push_str(body); + } + Ok(out) +} + +/// Update specific fields in frontmatter YAML without re-serializing the entire struct. +/// This preserves unknown fields and ordering as much as possible. +pub fn update_frontmatter_fields( + content: &str, + path: &Path, + updates: &serde_json::Value, +) -> Result { + let (yaml, body) = split_frontmatter_with_path(content, path)?; + + let mut mapping: serde_yaml::Value = serde_yaml::from_str(yaml).map_err(VaultError::Yaml)?; + + if let (serde_yaml::Value::Mapping(ref mut map), serde_json::Value::Object(ref obj)) = + (&mut mapping, updates) + { + for (key, value) in obj { + let yaml_key = serde_yaml::Value::String(key.clone()); + let yaml_value: serde_yaml::Value = + serde_json::from_value(value.clone()).unwrap_or(serde_yaml::Value::Null); + map.insert(yaml_key, yaml_value); + } + } + + let yaml_out = serde_yaml::to_string(&mapping).map_err(VaultError::Yaml)?; + let mut out = String::new(); + out.push_str(DELIMITER); + out.push('\n'); + out.push_str(&yaml_out); + if !yaml_out.ends_with('\n') { + out.push('\n'); + } + out.push_str(DELIMITER); + out.push('\n'); + if !body.is_empty() { + out.push_str(body); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::Agent; + + #[test] + fn test_split_frontmatter() { + let content = "---\nname: test\n---\nHello world\n"; + let (yaml, body) = split_frontmatter(content).unwrap(); + assert_eq!(yaml, "name: test\n"); + assert_eq!(body, "Hello world\n"); + } + + #[test] + fn test_split_missing_frontmatter() { + let content = "Hello world\n"; + assert!(split_frontmatter(content).is_err()); + } + + #[test] + fn test_roundtrip() { + let original_body = "# System Prompt\n\nYou are a helpful agent.\n\n- Rule 1\n- Rule 2\n"; + let agent = Agent { + name: "test-agent".into(), + executable: "claude-code".into(), + model: Some("sonnet".into()), + escalate_to: None, + escalate_when: vec![], + mcp_servers: vec![], + skills: vec!["read-vault".into()], + timeout: 600, + max_retries: 2, + env: Default::default(), + }; + + let written = write_frontmatter(&agent, original_body).unwrap(); + let (yaml, body) = split_frontmatter(&written).unwrap(); + let parsed: Agent = parse_entity(yaml).unwrap(); + + assert_eq!(parsed.name, "test-agent"); + assert_eq!(parsed.executable, "claude-code"); + assert_eq!(body, original_body); + } + + #[test] + fn test_update_fields() { + let content = "---\nname: test\nschedule: '* * * * *'\n---\nBody\n"; + let updates = serde_json::json!({ + "last_run": "2024-01-01T00:00:00Z", + "run_count": 5 + }); + let result = + update_frontmatter_fields(content, Path::new("test.md"), &updates).unwrap(); + assert!(result.contains("last_run")); + assert!(result.contains("run_count")); + assert!(result.contains("Body\n")); + } +} diff --git a/crates/vault-core/src/lib.rs b/crates/vault-core/src/lib.rs new file mode 100644 index 0000000..c83f6e2 --- /dev/null +++ b/crates/vault-core/src/lib.rs @@ -0,0 +1,12 @@ +pub mod config; +pub mod entity; +pub mod error; +pub mod filesystem; +pub mod frontmatter; +pub mod prompt; +pub mod search; +pub mod types; +pub mod validation; + +pub use error::VaultError; +pub type Result = std::result::Result; diff --git a/crates/vault-core/src/prompt.rs b/crates/vault-core/src/prompt.rs new file mode 100644 index 0000000..e075716 --- /dev/null +++ b/crates/vault-core/src/prompt.rs @@ -0,0 +1,64 @@ +use crate::entity::VaultEntity; +use crate::error::VaultError; +use crate::filesystem; +use crate::types::{Agent, Skill}; +use std::path::Path; + +/// Resolve a skill name to its file path under the vault's `skills/` directory. +pub fn resolve_skill_path(vault_root: &Path, skill_name: &str) -> Option { + // Try direct: skills/{name}.md + let direct = vault_root.join("skills").join(format!("{}.md", skill_name)); + if direct.exists() { + return Some(direct); + } + // Try nested: skills/vault/{name}.md + let nested = vault_root + .join("skills/vault") + .join(format!("{}.md", skill_name)); + if nested.exists() { + return Some(nested); + } + // Try recursive search + if let Ok(files) = filesystem::list_md_files_recursive(&vault_root.join("skills")) { + for file in files { + if let Some(stem) = file.file_stem() { + if stem == skill_name { + return Some(file); + } + } + } + } + None +} + +/// Compose the full prompt for an agent execution. +/// Agent body + skill bodies appended under `## Skills` sections. +pub fn compose_prompt( + vault_root: &Path, + agent: &VaultEntity, + task_context: Option<&str>, +) -> Result { + let mut prompt = agent.body.clone(); + + // Append skills + if !agent.frontmatter.skills.is_empty() { + prompt.push_str("\n\n## Skills\n"); + for skill_name in &agent.frontmatter.skills { + if let Some(skill_path) = resolve_skill_path(vault_root, skill_name) { + let skill_entity: VaultEntity = filesystem::read_entity(&skill_path)?; + prompt.push_str(&format!("\n### {}\n", skill_entity.frontmatter.name)); + prompt.push_str(&skill_entity.body); + } else { + tracing::warn!(skill = %skill_name, "Skill not found, skipping"); + } + } + } + + // Append task context if provided + if let Some(ctx) = task_context { + prompt.push_str("\n\n## Task\n\n"); + prompt.push_str(ctx); + } + + Ok(prompt) +} diff --git a/crates/vault-core/src/search.rs b/crates/vault-core/src/search.rs new file mode 100644 index 0000000..e2740d9 --- /dev/null +++ b/crates/vault-core/src/search.rs @@ -0,0 +1,164 @@ +use crate::filesystem::{list_md_files_recursive, read_entity}; +use crate::frontmatter::split_frontmatter_with_path; +use crate::types::KnowledgeNote; +use std::path::Path; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct SearchResult { + pub path: String, + pub title: String, + pub snippet: String, + pub score: f64, +} + +/// Search vault files by query string. +/// Matches against frontmatter title, tags, and body content. +pub fn search_vault(vault_root: &Path, query: &str) -> Vec { + let query_lower = query.to_lowercase(); + let terms: Vec<&str> = query_lower.split_whitespace().collect(); + if terms.is_empty() { + return Vec::new(); + } + + let mut results = Vec::new(); + + // Search across key directories + let dirs = ["knowledge", "agents", "skills", "todos/harald", "todos/agent"]; + + for dir in dirs { + let full_dir = vault_root.join(dir); + if !full_dir.exists() { + continue; + } + if let Ok(files) = list_md_files_recursive(&full_dir) { + for path in files { + if let Some(result) = score_file(&path, vault_root, &terms) { + results.push(result); + } + } + } + } + + results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); + results.truncate(50); + results +} + +/// Search specifically by tag. +pub fn search_by_tag(vault_root: &Path, tag: &str) -> Vec { + let tag_lower = tag.to_lowercase(); + let mut results = Vec::new(); + + let knowledge_dir = vault_root.join("knowledge"); + if let Ok(files) = list_md_files_recursive(&knowledge_dir) { + for path in files { + if let Ok(entity) = read_entity::(&path) { + let has_tag = entity + .frontmatter + .tags + .iter() + .any(|t| t.to_lowercase() == tag_lower); + if has_tag { + let relative = path + .strip_prefix(vault_root) + .unwrap_or(&path) + .to_string_lossy() + .to_string(); + let title = entity + .frontmatter + .title + .unwrap_or_else(|| relative.clone()); + results.push(SearchResult { + path: relative, + title, + snippet: entity.body.chars().take(120).collect(), + score: 1.0, + }); + } + } + } + } + + results +} + +fn score_file(path: &Path, vault_root: &Path, terms: &[&str]) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let content_lower = content.to_lowercase(); + + let relative = path + .strip_prefix(vault_root) + .unwrap_or(path) + .to_string_lossy() + .to_string(); + + let mut score = 0.0; + let mut all_matched = true; + + for term in terms { + let mut term_score = 0.0; + + // Title matches (higher weight) + if relative.to_lowercase().contains(term) { + term_score += 3.0; + } + + // Body/content matches + let count = content_lower.matches(term).count(); + if count > 0 { + term_score += 1.0 + (count as f64).min(5.0) * 0.2; + } + + if term_score == 0.0 { + all_matched = false; + break; + } + score += term_score; + } + + if !all_matched || score == 0.0 { + return None; + } + + // Extract title from frontmatter if possible + let title = if let Ok((yaml, _body)) = split_frontmatter_with_path(&content, path) { + serde_yaml::from_str::(yaml) + .ok() + .and_then(|v| v.get("title").and_then(|t| t.as_str()).map(String::from)) + .or_else(|| { + serde_yaml::from_str::(yaml) + .ok() + .and_then(|v| v.get("name").and_then(|t| t.as_str()).map(String::from)) + }) + .unwrap_or_else(|| relative.clone()) + } else { + relative.clone() + }; + + // Extract a snippet around the first match + let snippet = extract_snippet(&content, terms.first().unwrap_or(&"")); + + Some(SearchResult { + path: relative, + title, + snippet, + score, + }) +} + +fn extract_snippet(content: &str, term: &str) -> String { + let lower = content.to_lowercase(); + if let Some(pos) = lower.find(&term.to_lowercase()) { + let start = content[..pos] + .rfind('\n') + .map(|p| p + 1) + .unwrap_or(pos.saturating_sub(60)); + let end = content[pos..] + .find('\n') + .map(|p| pos + p) + .unwrap_or((pos + 120).min(content.len())); + content[start..end].chars().take(150).collect() + } else { + content.lines().next().unwrap_or("").chars().take(150).collect() + } +} diff --git a/crates/vault-core/src/types.rs b/crates/vault-core/src/types.rs new file mode 100644 index 0000000..60eae93 --- /dev/null +++ b/crates/vault-core/src/types.rs @@ -0,0 +1,204 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum Priority { + Urgent, + High, + #[default] + Medium, + Low, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum RunStatus { + Success, + Failure, + Timeout, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum TaskStatus { + Urgent, + Open, + #[serde(rename = "in-progress")] + InProgress, + Done, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum AgentTaskStatus { + Queued, + Running, + Done, + Failed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Agent { + pub name: String, + pub executable: String, + #[serde(default)] + pub model: Option, + #[serde(default)] + pub escalate_to: Option, + #[serde(default)] + pub escalate_when: Vec, + #[serde(default)] + pub mcp_servers: Vec, + #[serde(default)] + pub skills: Vec, + #[serde(default = "default_timeout")] + pub timeout: u64, + #[serde(default)] + pub max_retries: u32, + #[serde(default)] + pub env: HashMap, +} + +fn default_timeout() -> u64 { + 600 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Skill { + pub name: String, + pub description: String, + #[serde(default)] + pub version: Option, + #[serde(default)] + pub requires_mcp: Vec, + #[serde(default)] + pub inputs: Vec, + #[serde(default)] + pub outputs: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CronJob { + pub schedule: String, + pub agent: String, + pub title: String, + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default)] + pub last_run: Option>, + #[serde(default)] + pub last_status: Option, + #[serde(default)] + pub next_run: Option>, + #[serde(default)] + pub run_count: u64, +} + +fn default_true() -> bool { + true +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HumanTask { + pub title: String, + #[serde(default)] + pub priority: Priority, + #[serde(default)] + pub source: Option, + #[serde(default)] + pub repo: Option, + #[serde(default)] + pub labels: Vec, + pub created: DateTime, + #[serde(default)] + pub due: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentTask { + pub title: String, + pub agent: String, + #[serde(default)] + pub priority: Priority, + #[serde(default, rename = "type")] + pub task_type: Option, + pub created: DateTime, + #[serde(default)] + pub started: Option>, + #[serde(default)] + pub completed: Option>, + #[serde(default)] + pub retry: u32, + #[serde(default)] + pub max_retries: u32, + #[serde(default)] + pub input: Option, + #[serde(default)] + pub output: Option, + #[serde(default)] + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KnowledgeNote { + #[serde(default)] + pub title: Option, + #[serde(default)] + pub tags: Vec, + #[serde(default)] + pub source: Option, + #[serde(default)] + pub created: Option>, + #[serde(default)] + pub related: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ViewDefinition { + #[serde(rename = "type")] + pub view_type: String, + pub title: Option, + #[serde(default)] + pub icon: Option, + #[serde(default)] + pub route: Option, + #[serde(default)] + pub position: Option, + #[serde(default)] + pub layout: Option, + #[serde(default)] + pub regions: HashMap>, + // Widget-specific fields + #[serde(default)] + pub name: Option, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub component: Option, + #[serde(default)] + pub props_schema: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WidgetInstance { + pub widget: String, + #[serde(default)] + pub props: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Notification { + pub title: String, + #[serde(default)] + pub message: Option, + #[serde(default)] + pub level: Option, + #[serde(default)] + pub source: Option, + #[serde(default)] + pub created: Option>, + #[serde(default)] + pub expires: Option>, +} diff --git a/crates/vault-core/src/validation.rs b/crates/vault-core/src/validation.rs new file mode 100644 index 0000000..79416ec --- /dev/null +++ b/crates/vault-core/src/validation.rs @@ -0,0 +1,317 @@ +use crate::entity::{classify_path, EntityKind}; +use crate::frontmatter::split_frontmatter_with_path; +use crate::types::*; +use std::collections::HashSet; +use std::path::Path; + +#[derive(Debug, Clone)] +pub struct ValidationIssue { + pub level: IssueLevel, + pub field: Option, + pub message: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "lowercase")] +pub enum IssueLevel { + Error, + Warning, +} + +impl serde::Serialize for ValidationIssue { + fn serialize(&self, s: S) -> Result { + use serde::ser::SerializeStruct; + let mut st = s.serialize_struct("ValidationIssue", 3)?; + st.serialize_field("level", &self.level)?; + st.serialize_field("field", &self.field)?; + st.serialize_field("message", &self.message)?; + st.end() + } +} + +/// Validate a vault file given its relative path and raw content. +pub fn validate(relative_path: &Path, content: &str) -> Vec { + let mut issues = Vec::new(); + + // Check frontmatter exists + let (yaml, _body) = match split_frontmatter_with_path(content, relative_path) { + Ok(pair) => pair, + Err(_) => { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: None, + message: "Missing or malformed frontmatter".into(), + }); + return issues; + } + }; + + let kind = classify_path(relative_path); + + match kind { + EntityKind::Agent => validate_agent(yaml, &mut issues), + EntityKind::Skill => validate_skill(yaml, &mut issues), + EntityKind::CronActive | EntityKind::CronPaused | EntityKind::CronTemplate => { + validate_cron(yaml, &mut issues) + } + EntityKind::HumanTask(_) => validate_human_task(yaml, &mut issues), + EntityKind::AgentTask(_) => validate_agent_task(yaml, &mut issues), + _ => {} + } + + issues +} + +fn validate_agent(yaml: &str, issues: &mut Vec) { + match serde_yaml::from_str::(yaml) { + Ok(agent) => { + if agent.name.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("name".into()), + message: "Agent name is required".into(), + }); + } + if agent.executable.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("executable".into()), + message: "Agent executable is required".into(), + }); + } + let valid_executables = ["claude-code", "ollama", "openai-compat"]; + if !valid_executables.contains(&agent.executable.as_str()) + && !agent.executable.starts_with('/') + && !agent.executable.contains('/') + { + issues.push(ValidationIssue { + level: IssueLevel::Warning, + field: Some("executable".into()), + message: format!( + "Executable '{}' is not a known executor. Expected one of: {:?} or an absolute path", + agent.executable, valid_executables + ), + }); + } + } + Err(e) => { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: None, + message: format!("Invalid agent frontmatter: {e}"), + }); + } + } +} + +fn validate_skill(yaml: &str, issues: &mut Vec) { + match serde_yaml::from_str::(yaml) { + Ok(skill) => { + if skill.name.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("name".into()), + message: "Skill name is required".into(), + }); + } + if skill.description.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Warning, + field: Some("description".into()), + message: "Skill should have a description".into(), + }); + } + } + Err(e) => { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: None, + message: format!("Invalid skill frontmatter: {e}"), + }); + } + } +} + +fn validate_cron(yaml: &str, issues: &mut Vec) { + match serde_yaml::from_str::(yaml) { + Ok(cron) => { + if cron.title.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("title".into()), + message: "Cron title is required".into(), + }); + } + if cron.agent.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("agent".into()), + message: "Cron agent is required".into(), + }); + } + // Validate cron expression + let expr = if cron.schedule.split_whitespace().count() == 5 { + format!("0 {}", cron.schedule) + } else { + cron.schedule.clone() + }; + if cron::Schedule::from_str(&expr).is_err() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("schedule".into()), + message: format!("Invalid cron expression: '{}'", cron.schedule), + }); + } + } + Err(e) => { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: None, + message: format!("Invalid cron frontmatter: {e}"), + }); + } + } +} + +fn validate_human_task(yaml: &str, issues: &mut Vec) { + match serde_yaml::from_str::(yaml) { + Ok(task) => { + if task.title.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("title".into()), + message: "Task title is required".into(), + }); + } + } + Err(e) => { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: None, + message: format!("Invalid task frontmatter: {e}"), + }); + } + } +} + +fn validate_agent_task(yaml: &str, issues: &mut Vec) { + match serde_yaml::from_str::(yaml) { + Ok(task) => { + if task.title.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("title".into()), + message: "Task title is required".into(), + }); + } + if task.agent.is_empty() { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: Some("agent".into()), + message: "Agent name is required for agent tasks".into(), + }); + } + } + Err(e) => { + issues.push(ValidationIssue { + level: IssueLevel::Error, + field: None, + message: format!("Invalid agent task frontmatter: {e}"), + }); + } + } +} + +/// Validate that references between entities are valid. +/// Checks that agent skills and cron agents exist. +pub fn validate_references( + vault_root: &Path, + agent_names: &HashSet, + skill_names: &HashSet, +) -> Vec<(String, ValidationIssue)> { + let mut issues = Vec::new(); + + // Check agent skill references + let agents_dir = vault_root.join("agents"); + if let Ok(files) = crate::filesystem::list_md_files(&agents_dir) { + for path in files { + if let Ok(entity) = crate::filesystem::read_entity::(&path) { + for skill in &entity.frontmatter.skills { + if !skill_names.contains(skill) { + issues.push(( + entity.frontmatter.name.clone(), + ValidationIssue { + level: IssueLevel::Warning, + field: Some("skills".into()), + message: format!("Referenced skill '{}' not found", skill), + }, + )); + } + } + } + } + } + + // Check cron agent references + let crons_dir = vault_root.join("crons/active"); + if let Ok(files) = crate::filesystem::list_md_files(&crons_dir) { + for path in files { + if let Ok(entity) = crate::filesystem::read_entity::(&path) { + if !agent_names.contains(&entity.frontmatter.agent) { + issues.push(( + entity.frontmatter.title.clone(), + ValidationIssue { + level: IssueLevel::Warning, + field: Some("agent".into()), + message: format!( + "Referenced agent '{}' not found", + entity.frontmatter.agent + ), + }, + )); + } + } + } + } + + issues +} + +use std::str::FromStr; + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn test_validate_valid_agent() { + let content = "---\nname: test-agent\nexecutable: claude-code\n---\nBody"; + let issues = validate(Path::new("agents/test-agent.md"), content); + assert!(issues.is_empty(), "Expected no issues: {:?}", issues); + } + + #[test] + fn test_validate_agent_missing_name() { + let content = "---\nname: \"\"\nexecutable: claude-code\n---\n"; + let issues = validate(Path::new("agents/bad.md"), content); + assert!(issues.iter().any(|i| i.field.as_deref() == Some("name"))); + } + + #[test] + fn test_validate_missing_frontmatter() { + let content = "No frontmatter here"; + let issues = validate(Path::new("agents/bad.md"), content); + assert_eq!(issues.len(), 1); + assert_eq!(issues[0].level, IssueLevel::Error); + } + + #[test] + fn test_validate_cron_bad_expression() { + let content = "---\ntitle: bad\nagent: test\nschedule: \"not a cron\"\n---\n"; + let issues = validate(Path::new("crons/active/bad.md"), content); + assert!(issues + .iter() + .any(|i| i.field.as_deref() == Some("schedule"))); + } +} diff --git a/crates/vault-scheduler/Cargo.toml b/crates/vault-scheduler/Cargo.toml new file mode 100644 index 0000000..38be552 --- /dev/null +++ b/crates/vault-scheduler/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "vault-scheduler" +version.workspace = true +edition.workspace = true + +[dependencies] +vault-core.workspace = true +vault-watch.workspace = true +tokio.workspace = true +cron.workspace = true +chrono.workspace = true +tracing.workspace = true +thiserror.workspace = true +async-trait.workspace = true +reqwest.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +uuid.workspace = true diff --git a/crates/vault-scheduler/src/cron_engine.rs b/crates/vault-scheduler/src/cron_engine.rs new file mode 100644 index 0000000..65197fc --- /dev/null +++ b/crates/vault-scheduler/src/cron_engine.rs @@ -0,0 +1,206 @@ +use chrono::{DateTime, Utc}; +use cron::Schedule; +use std::cmp::Reverse; +use std::collections::BinaryHeap; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use vault_core::entity::VaultEntity; +use vault_core::error::VaultError; +use vault_core::filesystem; +use vault_core::frontmatter; +use vault_core::types::CronJob; + +#[derive(Debug, thiserror::Error)] +pub enum CronError { + #[error("Invalid cron expression '{expr}': {reason}")] + InvalidExpression { expr: String, reason: String }, + + #[error("Vault error: {0}")] + Vault(#[from] VaultError), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ScheduleEntry { + next_fire: DateTime, + path: PathBuf, +} + +impl PartialOrd for ScheduleEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ScheduleEntry { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.next_fire.cmp(&other.next_fire) + } +} + +pub struct CronEngine { + vault_root: PathBuf, + schedule: BinaryHeap>, +} + +impl CronEngine { + pub fn new(vault_root: PathBuf) -> Self { + Self { + vault_root, + schedule: BinaryHeap::new(), + } + } + + /// Rebuild the entire schedule by scanning `crons/active/`. + pub fn rebuild_schedule(&mut self) -> Result<(), CronError> { + self.schedule.clear(); + let active_dir = self.vault_root.join("crons/active"); + let files = filesystem::list_md_files(&active_dir)?; + + for file in files { + if let Err(e) = self.add_cron(&file) { + tracing::warn!(?file, error = %e, "Skipping invalid cron"); + } + } + + tracing::info!(count = self.schedule.len(), "Rebuilt cron schedule"); + Ok(()) + } + + /// Add or update a cron job in the schedule. + pub fn upsert_cron(&mut self, path: &Path) -> Result<(), CronError> { + self.remove_cron(path); + self.add_cron(path) + } + + /// Remove a cron job from the schedule. + pub fn remove_cron(&mut self, path: &Path) { + let entries: Vec<_> = self + .schedule + .drain() + .filter(|Reverse(e)| e.path != path) + .collect(); + self.schedule = entries.into_iter().collect(); + } + + /// Get the next fire time, if any crons are scheduled. + pub fn next_fire_time(&self) -> Option> { + self.schedule.peek().map(|Reverse(e)| e.next_fire) + } + + /// Pop all crons that are due (fire time <= now). + pub fn pop_due(&mut self) -> Vec { + let now = Utc::now(); + let mut due = Vec::new(); + + while let Some(Reverse(entry)) = self.schedule.peek() { + if entry.next_fire <= now { + let Reverse(entry) = self.schedule.pop().unwrap(); + due.push(entry.path); + } else { + break; + } + } + + due + } + + /// Fire a cron: create an agent task in queued/, update cron frontmatter. + /// Returns the path to the created agent task. + #[tracing::instrument(skip(self, write_filter), fields(cron = ?cron_path.file_name()))] + pub fn fire_cron( + &mut self, + cron_path: &Path, + write_filter: &vault_watch::write_filter::DaemonWriteFilter, + ) -> Result { + let entity: VaultEntity = filesystem::read_entity(cron_path)?; + let cron = &entity.frontmatter; + + // Create agent task + let slug = filesystem::timestamped_slug(&cron.title); + let task_path = self + .vault_root + .join("todos/agent/queued") + .join(format!("{}.md", slug)); + + let now = Utc::now(); + let agent_task = vault_core::types::AgentTask { + title: cron.title.clone(), + agent: cron.agent.clone(), + priority: vault_core::types::Priority::Medium, + task_type: Some("cron".into()), + created: now, + started: None, + completed: None, + retry: 0, + max_retries: 0, + input: None, + output: None, + error: None, + }; + + let task_entity = VaultEntity { + path: task_path.clone(), + frontmatter: agent_task, + body: entity.body.clone(), + }; + + write_filter.register(task_path.clone()); + filesystem::write_entity(&task_entity)?; + + // Update cron frontmatter + let content = std::fs::read_to_string(cron_path) + .map_err(|e| VaultError::io(e, cron_path))?; + let updates = serde_json::json!({ + "last_run": now.to_rfc3339(), + "last_status": "success", + "run_count": cron.run_count + 1, + }); + let updated = frontmatter::update_frontmatter_fields(&content, cron_path, &updates)?; + write_filter.register(cron_path.to_path_buf()); + std::fs::write(cron_path, updated).map_err(|e| VaultError::io(e, cron_path))?; + + // Re-schedule this cron + if let Err(e) = self.add_cron(cron_path) { + tracing::warn!(?cron_path, error = %e, "Failed to reschedule cron"); + } + + tracing::info!( + cron = %cron.title, + agent = %cron.agent, + task = ?task_path, + "Fired cron job" + ); + + Ok(task_path) + } + + fn add_cron(&mut self, path: &Path) -> Result<(), CronError> { + let entity: VaultEntity = filesystem::read_entity(path)?; + let cron = &entity.frontmatter; + + if !cron.enabled { + return Ok(()); + } + + // cron crate expects 6 or 7 fields (sec min hour dom month dow [year]) + // Standard 5-field cron: prepend "0 " for seconds + let expr = format!("0 {}", cron.schedule); + let schedule = Schedule::from_str(&expr).map_err(|e| CronError::InvalidExpression { + expr: cron.schedule.clone(), + reason: e.to_string(), + })?; + + if let Some(next) = schedule.upcoming(Utc).next() { + self.schedule.push(Reverse(ScheduleEntry { + next_fire: next, + path: path.to_path_buf(), + })); + } + + Ok(()) + } + + pub fn scheduled_count(&self) -> usize { + self.schedule.len() + } +} diff --git a/crates/vault-scheduler/src/executor.rs b/crates/vault-scheduler/src/executor.rs new file mode 100644 index 0000000..0cc2716 --- /dev/null +++ b/crates/vault-scheduler/src/executor.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; +use std::time::Duration; + +#[derive(Debug, Clone)] +pub struct ExecutionResult { + pub stdout: String, + pub stderr: String, + pub exit_code: Option, + pub duration: Duration, +} + +#[derive(Debug, thiserror::Error)] +pub enum ExecutionError { + #[error("Execution timed out after {0:?}")] + Timeout(Duration), + + #[error("Process failed to start: {0}")] + SpawnFailed(String), + + #[error("Process exited with code {code}: {stderr}")] + NonZeroExit { code: i32, stderr: String }, + + #[error("HTTP error: {0}")] + Http(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), +} + +#[async_trait::async_trait] +pub trait Executor: Send + Sync { + async fn execute( + &self, + executable: &str, + model: Option<&str>, + system_prompt: &str, + task_context: &str, + env: &HashMap, + timeout: Duration, + ) -> Result; +} diff --git a/crates/vault-scheduler/src/executors/mod.rs b/crates/vault-scheduler/src/executors/mod.rs new file mode 100644 index 0000000..80fe812 --- /dev/null +++ b/crates/vault-scheduler/src/executors/mod.rs @@ -0,0 +1 @@ +pub mod process; diff --git a/crates/vault-scheduler/src/executors/process.rs b/crates/vault-scheduler/src/executors/process.rs new file mode 100644 index 0000000..a86cdd9 --- /dev/null +++ b/crates/vault-scheduler/src/executors/process.rs @@ -0,0 +1,132 @@ +use crate::executor::{ExecutionError, ExecutionResult, Executor}; +use std::collections::HashMap; +use std::time::{Duration, Instant}; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; + +/// Generic process executor: spawns a child process, pipes prompt to stdin, +/// captures stdout/stderr. +pub struct GenericProcessExecutor { + vault_path: std::path::PathBuf, +} + +impl GenericProcessExecutor { + pub fn new(vault_path: std::path::PathBuf) -> Self { + Self { vault_path } + } + + /// Expand `${VAR}` references in environment variable values. + fn expand_env(value: &str) -> String { + let mut result = value.to_string(); + // Simple ${VAR} expansion from process environment + while let Some(start) = result.find("${") { + if let Some(end) = result[start..].find('}') { + let var_name = &result[start + 2..start + end]; + let replacement = std::env::var(var_name).unwrap_or_default(); + result = format!("{}{}{}", &result[..start], replacement, &result[start + end + 1..]); + } else { + break; + } + } + result + } +} + +#[async_trait::async_trait] +impl Executor for GenericProcessExecutor { + async fn execute( + &self, + executable: &str, + model: Option<&str>, + system_prompt: &str, + task_context: &str, + env: &HashMap, + timeout: Duration, + ) -> Result { + let start = Instant::now(); + + // Build the full prompt + let full_prompt = if task_context.is_empty() { + system_prompt.to_string() + } else { + format!("{}\n\n## Task\n\n{}", system_prompt, task_context) + }; + + // Determine command and args based on executable type + let (cmd, args) = if executable == "claude-code" { + ( + "claude".to_string(), + vec![ + "--print".to_string(), + "--dangerously-skip-permissions".to_string(), + full_prompt.clone(), + ], + ) + } else { + (executable.to_string(), vec![]) + }; + + let mut command = Command::new(&cmd); + command.args(&args); + + // Set environment + command.env("VAULT_PATH", &self.vault_path); + for (key, value) in env { + command.env(key, Self::expand_env(value)); + } + if let Some(model) = model { + command.env("MODEL", model); + } + + // For non-claude executables, pipe prompt via stdin + if executable != "claude-code" { + command + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + } else { + command + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + } + + let mut child = command + .spawn() + .map_err(|e| ExecutionError::SpawnFailed(format!("{}: {}", cmd, e)))?; + + // Write prompt to stdin for non-claude executables + if executable != "claude-code" { + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(full_prompt.as_bytes()).await?; + drop(stdin); + } + } + + // Wait with timeout + let output = match tokio::time::timeout(timeout, child.wait_with_output()).await { + Ok(result) => result.map_err(|e| ExecutionError::SpawnFailed(e.to_string()))?, + Err(_) => { + return Err(ExecutionError::Timeout(timeout)); + } + }; + + let duration = start.elapsed(); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let exit_code = output.status.code(); + + if output.status.success() { + Ok(ExecutionResult { + stdout, + stderr, + exit_code, + duration, + }) + } else { + Err(ExecutionError::NonZeroExit { + code: exit_code.unwrap_or(-1), + stderr, + }) + } + } +} diff --git a/crates/vault-scheduler/src/lib.rs b/crates/vault-scheduler/src/lib.rs new file mode 100644 index 0000000..4417cdc --- /dev/null +++ b/crates/vault-scheduler/src/lib.rs @@ -0,0 +1,5 @@ +pub mod cron_engine; +pub mod executor; +pub mod executors; +pub mod state; +pub mod task_runner; diff --git a/crates/vault-scheduler/src/state.rs b/crates/vault-scheduler/src/state.rs new file mode 100644 index 0000000..71a46e2 --- /dev/null +++ b/crates/vault-scheduler/src/state.rs @@ -0,0 +1,36 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use vault_core::error::VaultError; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RuntimeState { + pub last_startup: Option>, + pub last_shutdown: Option>, + pub total_tasks_executed: u64, + pub total_cron_fires: u64, +} + +impl RuntimeState { + pub fn load(vault_root: &Path) -> Result { + let state_path = vault_root.join(".vault/state.json"); + if !state_path.exists() { + return Ok(Self::default()); + } + let content = std::fs::read_to_string(&state_path) + .map_err(|e| VaultError::io(e, &state_path))?; + let state: RuntimeState = + serde_json::from_str(&content).unwrap_or_default(); + Ok(state) + } + + pub fn save(&self, vault_root: &Path) -> Result<(), VaultError> { + let state_path = vault_root.join(".vault/state.json"); + let content = serde_json::to_string_pretty(self) + .map_err(|e| VaultError::InvalidEntity { + path: state_path.clone(), + reason: e.to_string(), + })?; + std::fs::write(&state_path, content).map_err(|e| VaultError::io(e, &state_path)) + } +} diff --git a/crates/vault-scheduler/src/task_runner.rs b/crates/vault-scheduler/src/task_runner.rs new file mode 100644 index 0000000..fa45140 --- /dev/null +++ b/crates/vault-scheduler/src/task_runner.rs @@ -0,0 +1,253 @@ +use crate::executor::{ExecutionError, Executor}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::sync::Semaphore; +use vault_core::entity::VaultEntity; +use vault_core::error::VaultError; +use vault_core::filesystem; +use vault_core::frontmatter; +use vault_core::types::{Agent, AgentTask}; +use vault_watch::write_filter::DaemonWriteFilter; + +pub struct TaskRunner { + vault_root: PathBuf, + semaphore: Arc, + executor: Arc, + write_filter: Arc, +} + +impl TaskRunner { + pub fn new( + vault_root: PathBuf, + max_parallel: usize, + executor: Arc, + write_filter: Arc, + ) -> Self { + Self { + vault_root, + semaphore: Arc::new(Semaphore::new(max_parallel)), + executor, + write_filter, + } + } + + /// Process all currently queued tasks. + pub async fn process_queued(&self) -> Result, VaultError> { + let queued_dir = self.vault_root.join("todos/agent/queued"); + let files = filesystem::list_md_files(&queued_dir)?; + let mut spawned = Vec::new(); + + for file in files { + spawned.push(file.clone()); + let runner = TaskRunner { + vault_root: self.vault_root.clone(), + semaphore: self.semaphore.clone(), + executor: self.executor.clone(), + write_filter: self.write_filter.clone(), + }; + tokio::spawn(async move { + if let Err(e) = runner.execute_task(&file).await { + tracing::error!(task = ?file, error = %e, "Task execution failed"); + } + }); + } + + Ok(spawned) + } + + /// Execute a single agent task. + #[tracing::instrument(skip(self), fields(task = ?task_path.file_name()))] + pub async fn execute_task(&self, task_path: &Path) -> Result<(), VaultError> { + let _permit = self + .semaphore + .acquire() + .await + .map_err(|e| VaultError::InvalidEntity { + path: task_path.to_path_buf(), + reason: format!("Semaphore closed: {}", e), + })?; + + let task_entity: VaultEntity = filesystem::read_entity(task_path)?; + let agent_name = &task_entity.frontmatter.agent; + + // Load agent definition + let agent_path = self.vault_root.join("agents").join(format!("{}.md", agent_name)); + let agent_entity: VaultEntity = filesystem::read_entity(&agent_path)?; + + // Move queued -> running + let running_path = self + .vault_root + .join("todos/agent/running") + .join(task_path.file_name().unwrap()); + self.write_filter.register(running_path.clone()); + filesystem::move_file(task_path, &running_path)?; + + // Update started timestamp + let content = std::fs::read_to_string(&running_path) + .map_err(|e| VaultError::io(e, &running_path))?; + let updates = serde_json::json!({ + "started": chrono::Utc::now().to_rfc3339(), + }); + let updated = frontmatter::update_frontmatter_fields(&content, &running_path, &updates)?; + self.write_filter.register(running_path.clone()); + std::fs::write(&running_path, updated).map_err(|e| VaultError::io(e, &running_path))?; + + // Compose prompt + let system_prompt = + vault_core::prompt::compose_prompt(&self.vault_root, &agent_entity, None)?; + let task_context = &task_entity.body; + + let timeout = std::time::Duration::from_secs(agent_entity.frontmatter.timeout); + + // Execute + let result = self + .executor + .execute( + &agent_entity.frontmatter.executable, + agent_entity.frontmatter.model.as_deref(), + &system_prompt, + task_context, + &agent_entity.frontmatter.env, + timeout, + ) + .await; + + match result { + Ok(exec_result) => { + // Move running -> done + let done_path = self + .vault_root + .join("todos/agent/done") + .join(running_path.file_name().unwrap()); + + let content = std::fs::read_to_string(&running_path) + .map_err(|e| VaultError::io(e, &running_path))?; + let updates = serde_json::json!({ + "completed": chrono::Utc::now().to_rfc3339(), + "output": { + "stdout": exec_result.stdout, + "duration_secs": exec_result.duration.as_secs(), + }, + }); + let updated = + frontmatter::update_frontmatter_fields(&content, &running_path, &updates)?; + self.write_filter.register(running_path.clone()); + std::fs::write(&running_path, updated) + .map_err(|e| VaultError::io(e, &running_path))?; + + self.write_filter.register(done_path.clone()); + filesystem::move_file(&running_path, &done_path)?; + + tracing::info!(task = ?done_path, "Task completed successfully"); + } + Err(exec_error) => { + let task_entity: VaultEntity = filesystem::read_entity(&running_path)?; + let retry = task_entity.frontmatter.retry; + let max_retries = task_entity.frontmatter.max_retries; + + if retry < max_retries { + // Re-queue with incremented retry count + let content = std::fs::read_to_string(&running_path) + .map_err(|e| VaultError::io(e, &running_path))?; + let updates = serde_json::json!({ + "retry": retry + 1, + "started": null, + "error": format!("Attempt {}: {}", retry + 1, exec_error), + }); + let updated = + frontmatter::update_frontmatter_fields(&content, &running_path, &updates)?; + self.write_filter.register(running_path.clone()); + std::fs::write(&running_path, updated) + .map_err(|e| VaultError::io(e, &running_path))?; + + let queued_path = self + .vault_root + .join("todos/agent/queued") + .join(running_path.file_name().unwrap()); + self.write_filter.register(queued_path.clone()); + filesystem::move_file(&running_path, &queued_path)?; + + tracing::warn!( + task = ?queued_path, + retry = retry + 1, + max_retries, + "Task failed, re-queued" + ); + } else { + // Move running -> failed + let failed_path = self + .vault_root + .join("todos/agent/failed") + .join(running_path.file_name().unwrap()); + + let content = std::fs::read_to_string(&running_path) + .map_err(|e| VaultError::io(e, &running_path))?; + let error_msg = match &exec_error { + ExecutionError::Timeout(d) => format!("Timed out after {:?}", d), + ExecutionError::NonZeroExit { code, stderr } => { + format!("Exit code {}: {}", code, stderr) + } + other => other.to_string(), + }; + let updates = serde_json::json!({ + "completed": chrono::Utc::now().to_rfc3339(), + "error": error_msg, + }); + let updated = + frontmatter::update_frontmatter_fields(&content, &running_path, &updates)?; + self.write_filter.register(running_path.clone()); + std::fs::write(&running_path, updated) + .map_err(|e| VaultError::io(e, &running_path))?; + + self.write_filter.register(failed_path.clone()); + filesystem::move_file(&running_path, &failed_path)?; + + tracing::error!( + task = ?failed_path, + error = %exec_error, + "Task failed permanently" + ); + } + } + } + + Ok(()) + } + + /// On startup, recover tasks that were left in running/ (daemon crashed). + /// Move them back to queued/ for re-execution. + pub fn recover_running_tasks(&self) -> Result, VaultError> { + let running_dir = self.vault_root.join("todos/agent/running"); + let files = filesystem::list_md_files(&running_dir)?; + let mut recovered = Vec::new(); + + for file in &files { + let queued_path = self + .vault_root + .join("todos/agent/queued") + .join(file.file_name().unwrap()); + + // Reset started timestamp + let content = + std::fs::read_to_string(file).map_err(|e| VaultError::io(e, file))?; + let updates = serde_json::json!({ + "started": null, + }); + if let Ok(updated) = frontmatter::update_frontmatter_fields(&content, file, &updates) { + self.write_filter.register(file.clone()); + let _ = std::fs::write(file, updated); + } + + self.write_filter.register(queued_path.clone()); + filesystem::move_file(file, &queued_path)?; + recovered.push(queued_path); + tracing::info!(task = ?file, "Recovered running task"); + } + + if !recovered.is_empty() { + tracing::info!(count = recovered.len(), "Recovered tasks from previous run"); + } + + Ok(recovered) + } +} diff --git a/crates/vault-watch/Cargo.toml b/crates/vault-watch/Cargo.toml new file mode 100644 index 0000000..22c9ff9 --- /dev/null +++ b/crates/vault-watch/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "vault-watch" +version.workspace = true +edition.workspace = true + +[dependencies] +vault-core.workspace = true +notify.workspace = true +tokio.workspace = true +tracing.workspace = true +thiserror.workspace = true diff --git a/crates/vault-watch/src/classifier.rs b/crates/vault-watch/src/classifier.rs new file mode 100644 index 0000000..70bc75f --- /dev/null +++ b/crates/vault-watch/src/classifier.rs @@ -0,0 +1,214 @@ +use crate::events::VaultEvent; +use notify::event::{CreateKind, ModifyKind, RemoveKind, RenameMode}; +use notify::EventKind; +use std::path::{Path, PathBuf}; +use vault_core::entity::{classify_path, EntityKind}; + +/// Classify a raw notify event into typed VaultEvents. +pub fn classify( + event: ¬ify::Event, + vault_root: &Path, +) -> Vec { + let mut vault_events = Vec::new(); + + for path in &event.paths { + // Skip non-.md files + if path.extension().is_none_or(|e| e != "md") { + continue; + } + + // Skip dotfiles and temp files + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + if name.starts_with('.') || name.starts_with('~') || name.ends_with(".tmp") { + continue; + } + } + + let relative = match path.strip_prefix(vault_root) { + Ok(r) => r, + Err(_) => continue, + }; + + // Skip .vault/ internal files + if relative.starts_with(".vault") { + continue; + } + + let kind = classify_path(relative); + + match event.kind { + EventKind::Create(CreateKind::File) | EventKind::Create(CreateKind::Any) => { + vault_events.push(make_created(kind, path.clone())); + } + EventKind::Modify(ModifyKind::Data(_)) + | EventKind::Modify(ModifyKind::Any) + | EventKind::Modify(ModifyKind::Metadata(_)) => { + vault_events.push(make_modified(kind, path.clone())); + } + EventKind::Remove(RemoveKind::File) | EventKind::Remove(RemoveKind::Any) => { + vault_events.push(make_deleted(kind, path.clone())); + } + _ => {} + } + } + + // Handle renames (two paths: from, to) + if matches!(event.kind, EventKind::Modify(ModifyKind::Name(RenameMode::Both))) + && event.paths.len() == 2 + { + let from = &event.paths[0]; + let to = &event.paths[1]; + + if to.extension().is_some_and(|e| e == "md") { + if let Ok(rel_to) = to.strip_prefix(vault_root) { + let kind_to = classify_path(rel_to); + let moved = make_moved(kind_to, from.clone(), to.clone()); + // Replace any Created/Deleted pair we may have emitted above + vault_events.clear(); + vault_events.push(moved); + } + } + } + + vault_events +} + +fn make_created(kind: EntityKind, path: PathBuf) -> VaultEvent { + match kind { + EntityKind::Agent => VaultEvent::AgentCreated(path), + EntityKind::Skill => VaultEvent::SkillCreated(path), + EntityKind::CronActive | EntityKind::CronPaused | EntityKind::CronTemplate => { + VaultEvent::CronCreated(path) + } + EntityKind::HumanTask(_) => VaultEvent::HumanTaskCreated(path), + EntityKind::AgentTask(_) => VaultEvent::AgentTaskCreated(path), + EntityKind::Knowledge => VaultEvent::KnowledgeCreated(path), + EntityKind::ViewPage | EntityKind::ViewWidget | EntityKind::ViewLayout | EntityKind::ViewCustom => { + VaultEvent::ViewCreated(path) + } + EntityKind::Notification => VaultEvent::NotificationCreated(path), + EntityKind::Unknown => VaultEvent::FileChanged(path), + } +} + +fn make_modified(kind: EntityKind, path: PathBuf) -> VaultEvent { + match kind { + EntityKind::Agent => VaultEvent::AgentModified(path), + EntityKind::Skill => VaultEvent::SkillModified(path), + EntityKind::CronActive | EntityKind::CronPaused | EntityKind::CronTemplate => { + VaultEvent::CronModified(path) + } + EntityKind::HumanTask(_) => VaultEvent::HumanTaskModified(path), + EntityKind::AgentTask(_) => VaultEvent::AgentTaskModified(path), + EntityKind::Knowledge => VaultEvent::KnowledgeModified(path), + EntityKind::ViewPage | EntityKind::ViewWidget | EntityKind::ViewLayout | EntityKind::ViewCustom => { + VaultEvent::ViewModified(path) + } + EntityKind::Notification => VaultEvent::NotificationCreated(path), + EntityKind::Unknown => VaultEvent::FileChanged(path), + } +} + +fn make_deleted(kind: EntityKind, path: PathBuf) -> VaultEvent { + match kind { + EntityKind::Agent => VaultEvent::AgentDeleted(path), + EntityKind::Skill => VaultEvent::SkillDeleted(path), + EntityKind::CronActive | EntityKind::CronPaused | EntityKind::CronTemplate => { + VaultEvent::CronDeleted(path) + } + EntityKind::HumanTask(_) => VaultEvent::HumanTaskDeleted(path), + EntityKind::AgentTask(_) => VaultEvent::AgentTaskDeleted(path), + EntityKind::Knowledge => VaultEvent::KnowledgeDeleted(path), + EntityKind::ViewPage | EntityKind::ViewWidget | EntityKind::ViewLayout | EntityKind::ViewCustom => { + VaultEvent::ViewDeleted(path) + } + EntityKind::Notification => VaultEvent::NotificationExpired(path), + EntityKind::Unknown => VaultEvent::FileChanged(path), + } +} + +fn make_moved(kind: EntityKind, from: PathBuf, to: PathBuf) -> VaultEvent { + match kind { + EntityKind::CronActive | EntityKind::CronPaused => { + VaultEvent::CronMoved { from, to } + } + EntityKind::HumanTask(_) => VaultEvent::HumanTaskMoved { from, to }, + EntityKind::AgentTask(_) => VaultEvent::AgentTaskMoved { from, to }, + _ => make_created(kind, to), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use notify::event::{CreateKind, DataChange, ModifyKind}; + + fn make_event(kind: EventKind, paths: Vec) -> notify::Event { + notify::Event { + kind, + paths, + attrs: Default::default(), + } + } + + #[test] + fn test_classify_agent_created() { + let root = PathBuf::from("/vault"); + let event = make_event( + EventKind::Create(CreateKind::File), + vec![PathBuf::from("/vault/agents/reviewer.md")], + ); + let events = classify(&event, &root); + assert_eq!(events.len(), 1); + assert!(matches!(events[0], VaultEvent::AgentCreated(_))); + } + + #[test] + fn test_skip_non_md() { + let root = PathBuf::from("/vault"); + let event = make_event( + EventKind::Create(CreateKind::File), + vec![PathBuf::from("/vault/agents/readme.txt")], + ); + let events = classify(&event, &root); + assert!(events.is_empty()); + } + + #[test] + fn test_skip_dotfiles() { + let root = PathBuf::from("/vault"); + let event = make_event( + EventKind::Create(CreateKind::File), + vec![PathBuf::from("/vault/agents/.hidden.md")], + ); + let events = classify(&event, &root); + assert!(events.is_empty()); + } + + #[test] + fn test_classify_task_modified() { + let root = PathBuf::from("/vault"); + let event = make_event( + EventKind::Modify(ModifyKind::Data(DataChange::Content)), + vec![PathBuf::from("/vault/todos/agent/running/task-1.md")], + ); + let events = classify(&event, &root); + assert_eq!(events.len(), 1); + assert!(matches!(events[0], VaultEvent::AgentTaskModified(_))); + } + + #[test] + fn test_classify_rename() { + let root = PathBuf::from("/vault"); + let event = make_event( + EventKind::Modify(ModifyKind::Name(RenameMode::Both)), + vec![ + PathBuf::from("/vault/todos/agent/queued/task.md"), + PathBuf::from("/vault/todos/agent/running/task.md"), + ], + ); + let events = classify(&event, &root); + assert_eq!(events.len(), 1); + assert!(matches!(events[0], VaultEvent::AgentTaskMoved { .. })); + } +} diff --git a/crates/vault-watch/src/events.rs b/crates/vault-watch/src/events.rs new file mode 100644 index 0000000..3277f67 --- /dev/null +++ b/crates/vault-watch/src/events.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub enum VaultEvent { + AgentCreated(PathBuf), + AgentModified(PathBuf), + AgentDeleted(PathBuf), + + SkillCreated(PathBuf), + SkillModified(PathBuf), + SkillDeleted(PathBuf), + + CronCreated(PathBuf), + CronModified(PathBuf), + CronDeleted(PathBuf), + CronMoved { from: PathBuf, to: PathBuf }, + + HumanTaskCreated(PathBuf), + HumanTaskModified(PathBuf), + HumanTaskMoved { from: PathBuf, to: PathBuf }, + HumanTaskDeleted(PathBuf), + + AgentTaskCreated(PathBuf), + AgentTaskModified(PathBuf), + AgentTaskMoved { from: PathBuf, to: PathBuf }, + AgentTaskDeleted(PathBuf), + + KnowledgeCreated(PathBuf), + KnowledgeModified(PathBuf), + KnowledgeDeleted(PathBuf), + + ViewCreated(PathBuf), + ViewModified(PathBuf), + ViewDeleted(PathBuf), + + NotificationCreated(PathBuf), + NotificationExpired(PathBuf), + + FileChanged(PathBuf), +} + +impl VaultEvent { + /// Get the primary path associated with this event. + pub fn path(&self) -> &PathBuf { + match self { + Self::AgentCreated(p) + | Self::AgentModified(p) + | Self::AgentDeleted(p) + | Self::SkillCreated(p) + | Self::SkillModified(p) + | Self::SkillDeleted(p) + | Self::CronCreated(p) + | Self::CronModified(p) + | Self::CronDeleted(p) + | Self::HumanTaskCreated(p) + | Self::HumanTaskModified(p) + | Self::HumanTaskDeleted(p) + | Self::AgentTaskCreated(p) + | Self::AgentTaskModified(p) + | Self::AgentTaskDeleted(p) + | Self::KnowledgeCreated(p) + | Self::KnowledgeModified(p) + | Self::KnowledgeDeleted(p) + | Self::ViewCreated(p) + | Self::ViewModified(p) + | Self::ViewDeleted(p) + | Self::NotificationCreated(p) + | Self::NotificationExpired(p) + | Self::FileChanged(p) => p, + Self::CronMoved { to, .. } + | Self::HumanTaskMoved { to, .. } + | Self::AgentTaskMoved { to, .. } => to, + } + } + + /// Return a string event type name for serialization. + pub fn event_type(&self) -> &'static str { + match self { + Self::AgentCreated(_) => "agent_created", + Self::AgentModified(_) => "agent_modified", + Self::AgentDeleted(_) => "agent_deleted", + Self::SkillCreated(_) => "skill_created", + Self::SkillModified(_) => "skill_modified", + Self::SkillDeleted(_) => "skill_deleted", + Self::CronCreated(_) => "cron_created", + Self::CronModified(_) => "cron_modified", + Self::CronDeleted(_) => "cron_deleted", + Self::CronMoved { .. } => "cron_moved", + Self::HumanTaskCreated(_) => "human_task_created", + Self::HumanTaskModified(_) => "human_task_modified", + Self::HumanTaskMoved { .. } => "human_task_moved", + Self::HumanTaskDeleted(_) => "human_task_deleted", + Self::AgentTaskCreated(_) => "agent_task_created", + Self::AgentTaskModified(_) => "agent_task_modified", + Self::AgentTaskMoved { .. } => "agent_task_moved", + Self::AgentTaskDeleted(_) => "agent_task_deleted", + Self::KnowledgeCreated(_) => "knowledge_created", + Self::KnowledgeModified(_) => "knowledge_modified", + Self::KnowledgeDeleted(_) => "knowledge_deleted", + Self::ViewCreated(_) => "view_created", + Self::ViewModified(_) => "view_modified", + Self::ViewDeleted(_) => "view_deleted", + Self::NotificationCreated(_) => "notification_created", + Self::NotificationExpired(_) => "notification_expired", + Self::FileChanged(_) => "file_changed", + } + } +} diff --git a/crates/vault-watch/src/lib.rs b/crates/vault-watch/src/lib.rs new file mode 100644 index 0000000..80ab070 --- /dev/null +++ b/crates/vault-watch/src/lib.rs @@ -0,0 +1,4 @@ +pub mod classifier; +pub mod events; +pub mod watcher; +pub mod write_filter; diff --git a/crates/vault-watch/src/watcher.rs b/crates/vault-watch/src/watcher.rs new file mode 100644 index 0000000..fa94714 --- /dev/null +++ b/crates/vault-watch/src/watcher.rs @@ -0,0 +1,83 @@ +use crate::classifier; +use crate::events::VaultEvent; +use crate::write_filter::DaemonWriteFilter; +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::mpsc; + +#[derive(Debug, thiserror::Error)] +pub enum WatchError { + #[error("Notify error: {0}")] + Notify(#[from] notify::Error), + + #[error("Channel closed")] + ChannelClosed, +} + +pub struct VaultWatcher { + vault_root: PathBuf, + write_filter: Arc, + _watcher: RecommendedWatcher, + rx: mpsc::Receiver, +} + +impl VaultWatcher { + pub fn new( + vault_root: PathBuf, + write_filter: Arc, + ) -> Result { + let (event_tx, event_rx) = mpsc::channel(256); + let root = vault_root.clone(); + let filter = write_filter.clone(); + + let (notify_tx, mut notify_rx) = mpsc::channel(512); + + let mut watcher = RecommendedWatcher::new( + move |res: Result| { + if let Ok(event) = res { + let _ = notify_tx.blocking_send(event); + } + }, + Config::default(), + )?; + + watcher.watch(&vault_root, RecursiveMode::Recursive)?; + + // Spawn classification task + let tx = event_tx.clone(); + tokio::spawn(async move { + while let Some(raw_event) = notify_rx.recv().await { + let vault_events = classifier::classify(&raw_event, &root); + for event in vault_events { + if filter.should_suppress(event.path()) { + tracing::debug!(?event, "Suppressed daemon-originated event"); + continue; + } + if tx.send(event).await.is_err() { + return; + } + } + } + }); + + Ok(Self { + vault_root, + write_filter, + _watcher: watcher, + rx: event_rx, + }) + } + + pub fn vault_root(&self) -> &PathBuf { + &self.vault_root + } + + pub fn write_filter(&self) -> &Arc { + &self.write_filter + } + + pub async fn recv(&mut self) -> Option { + self.rx.recv().await + } +} diff --git a/crates/vault-watch/src/write_filter.rs b/crates/vault-watch/src/write_filter.rs new file mode 100644 index 0000000..3775c74 --- /dev/null +++ b/crates/vault-watch/src/write_filter.rs @@ -0,0 +1,67 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +const WRITE_FILTER_TTL: Duration = Duration::from_secs(5); + +/// Filters out filesystem events triggered by daemon-originated writes. +/// Before writing a file, register the path. When an event arrives, +/// check if it should be suppressed. +pub struct DaemonWriteFilter { + pending: Mutex>, +} + +impl DaemonWriteFilter { + pub fn new() -> Self { + Self { + pending: Mutex::new(HashMap::new()), + } + } + + /// Register a path that the daemon is about to write. + pub fn register(&self, path: PathBuf) { + let mut pending = self.pending.lock().unwrap(); + pending.insert(path, Instant::now()); + } + + /// Check if an event for this path should be suppressed. + /// Returns true if the event should be suppressed (i.e., it was daemon-originated). + pub fn should_suppress(&self, path: &PathBuf) -> bool { + let mut pending = self.pending.lock().unwrap(); + + // Clean up stale entries + pending.retain(|_, ts| ts.elapsed() < WRITE_FILTER_TTL); + + pending.remove(path).is_some() + } +} + +impl Default for DaemonWriteFilter { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_register_and_suppress() { + let filter = DaemonWriteFilter::new(); + let path = PathBuf::from("/vault/crons/active/test.md"); + + filter.register(path.clone()); + assert!(filter.should_suppress(&path)); + // Second check should not suppress (already consumed) + assert!(!filter.should_suppress(&path)); + } + + #[test] + fn test_unregistered_not_suppressed() { + let filter = DaemonWriteFilter::new(); + let path = PathBuf::from("/vault/agents/test.md"); + assert!(!filter.should_suppress(&path)); + } +} diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/dashboard/eslint.config.js b/dashboard/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/dashboard/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..cb8ab52 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,13 @@ + + + + + + + dashboard + + +
+ + + diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..4ac9119 --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,4235 @@ +{ + "name": "dashboard", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dashboard", + "version": "0.0.0", + "dependencies": { + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/language": "^6.12.2", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.16", + "@hello-pangea/dnd": "^18.0.1", + "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-query": "^5.90.21", + "codemirror": "^6.0.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "tailwindcss": "^4.2.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", + "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", + "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.39.16", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.16.tgz", + "integrity": "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz", + "integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..d940a34 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,43 @@ +{ + "name": "dashboard", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/language": "^6.12.2", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.16", + "@hello-pangea/dnd": "^18.0.1", + "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-query": "^5.90.21", + "codemirror": "^6.0.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "tailwindcss": "^4.2.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx new file mode 100644 index 0000000..89041ad --- /dev/null +++ b/dashboard/src/App.tsx @@ -0,0 +1,47 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Layout } from './components/Layout'; +import { TasksPage } from './pages/TasksPage'; +import { AgentsPage } from './pages/AgentsPage'; +import { CronsPage } from './pages/CronsPage'; +import { AgentQueuePage } from './pages/AgentQueuePage'; +import { KnowledgePage } from './pages/KnowledgePage'; +import { EditorPage } from './pages/EditorPage'; +import { ViewPage } from './pages/ViewPage'; +import { CommandPalette } from './components/CommandPalette'; +import { useWebSocket } from './hooks/useWebSocket'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { staleTime: 5000, retry: 1 }, + }, +}); + +function AppInner() { + useWebSocket(); + + return ( + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); +} + +export default function App() { + return ( + + + + ); +} diff --git a/dashboard/src/api/client.ts b/dashboard/src/api/client.ts new file mode 100644 index 0000000..96c3d52 --- /dev/null +++ b/dashboard/src/api/client.ts @@ -0,0 +1,152 @@ +import type { + Agent, + AgentTask, + CronJob, + HumanTask, + KnowledgeNote, + Skill, + TreeNode, + VaultStats, + HealthStatus, + ViewPageDef, + ViewDetail, + NotificationItem, +} from './types'; + +const BASE = '/api'; + +async function fetchJson(url: string, init?: RequestInit): Promise { + const res = await fetch(url, { + headers: { 'Content-Type': 'application/json', ...init?.headers }, + ...init, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(body.error || res.statusText); + } + return res.json(); +} + +// Agents +export const listAgents = () => fetchJson(`${BASE}/agents`); +export const getAgent = (name: string) => fetchJson(`${BASE}/agents/${name}`); +export const triggerAgent = (name: string, context?: string) => + fetchJson<{ status: string }>(`${BASE}/agents/${name}/trigger`, { + method: 'POST', + body: JSON.stringify({ context }), + }); + +// Skills +export const listSkills = () => fetchJson(`${BASE}/skills`); +export const getSkill = (name: string) => fetchJson(`${BASE}/skills/${name}`); +export const skillUsedBy = (name: string) => fetchJson(`${BASE}/skills/${name}/used-by`); + +// Crons +export const listCrons = () => fetchJson(`${BASE}/crons`); +export const triggerCron = (name: string) => + fetchJson<{ status: string }>(`${BASE}/crons/${name}/trigger`, { method: 'POST' }); +export const pauseCron = (name: string) => + fetchJson<{ status: string }>(`${BASE}/crons/${name}/pause`, { method: 'POST' }); +export const resumeCron = (name: string) => + fetchJson<{ status: string }>(`${BASE}/crons/${name}/resume`, { method: 'POST' }); + +// Human Tasks +export const listHumanTasks = () => fetchJson(`${BASE}/todos/harald`); +export const listHumanTasksByStatus = (status: string) => + fetchJson(`${BASE}/todos/harald/${status}`); +export const createHumanTask = (task: { + title: string; + priority?: string; + labels?: string[]; + body?: string; +}) => + fetchJson<{ status: string; path: string }>(`${BASE}/todos/harald`, { + method: 'POST', + body: JSON.stringify(task), + }); +export const moveHumanTask = (status: string, id: string, to: string) => + fetchJson<{ status: string }>(`${BASE}/todos/harald/${status}/${id}/move`, { + method: 'PATCH', + body: JSON.stringify({ to }), + }); +export const deleteHumanTask = (status: string, id: string) => + fetchJson<{ status: string }>(`${BASE}/todos/harald/${status}/${id}`, { method: 'DELETE' }); + +// Agent Tasks +export const listAgentTasks = () => fetchJson(`${BASE}/todos/agent`); +export const getAgentTask = (id: string) => fetchJson(`${BASE}/todos/agent/${id}`); +export const createAgentTask = (task: { + title: string; + agent: string; + priority?: string; + body?: string; +}) => + fetchJson<{ status: string; path: string }>(`${BASE}/todos/agent`, { + method: 'POST', + body: JSON.stringify(task), + }); + +// Knowledge +export const listKnowledge = (q?: string, tag?: string) => { + const params = new URLSearchParams(); + if (q) params.set('q', q); + if (tag) params.set('tag', tag); + const qs = params.toString(); + return fetchJson(`${BASE}/knowledge${qs ? `?${qs}` : ''}`); +}; +export const getKnowledge = (path: string) => + fetchJson<{ path: string; frontmatter: unknown; body: string; html: string }>( + `${BASE}/knowledge/${path}`, + ); + +// Files +export const readFile = (path: string) => + fetchJson<{ path: string; frontmatter: unknown; body: string }>(`${BASE}/files/${path}`); +export const writeFile = (path: string, data: { frontmatter?: unknown; body?: string; raw?: string }) => + fetchJson<{ status: string }>(`${BASE}/files/${path}`, { + method: 'PUT', + body: JSON.stringify(data), + }); +export const patchFile = (path: string, updates: Record) => + fetchJson<{ status: string }>(`${BASE}/files/${path}`, { + method: 'PATCH', + body: JSON.stringify(updates), + }); +export const deleteFile = (path: string) => + fetchJson<{ status: string }>(`${BASE}/files/${path}`, { method: 'DELETE' }); + +// Tree +export const getTree = () => fetchJson(`${BASE}/tree`); + +// Suggest +export const suggestAgents = () => fetchJson(`${BASE}/suggest/agents`); +export const suggestSkills = () => fetchJson(`${BASE}/suggest/skills`); +export const suggestTags = () => fetchJson(`${BASE}/suggest/tags`); +export const suggestFiles = (q?: string) => + fetchJson(`${BASE}/suggest/files${q ? `?q=${encodeURIComponent(q)}` : ''}`); +export const suggestModels = () => fetchJson(`${BASE}/suggest/models`); +export const suggestMcpServers = () => fetchJson(`${BASE}/suggest/mcp-servers`); + +// Stats +export const getStats = () => fetchJson(`${BASE}/stats`); +export const getActivity = () => + fetchJson<{ path: string; kind: string; modified: string; name: string }[]>(`${BASE}/activity`); +export const getHealth = () => fetchJson(`${BASE}/health`); + +// Views +export const listViewPages = () => fetchJson(`${BASE}/views/pages`); +export const listViewWidgets = () => fetchJson(`${BASE}/views/widgets`); +export const listViewLayouts = () => fetchJson(`${BASE}/views/layouts`); +export const getView = (path: string) => fetchJson(`${BASE}/views/${path}`); +export const putView = (path: string, data: { frontmatter?: unknown; body?: string; raw?: string }) => + fetchJson<{ status: string }>(`${BASE}/views/${path}`, { + method: 'PUT', + body: JSON.stringify(data), + }); +export const deleteView = (path: string) => + fetchJson<{ status: string }>(`${BASE}/views/${path}`, { method: 'DELETE' }); + +// Notifications +export const listNotifications = () => fetchJson(`${BASE}/notifications`); +export const dismissNotification = (id: string) => + fetchJson<{ status: string }>(`${BASE}/notifications/${id}`, { method: 'DELETE' }); diff --git a/dashboard/src/api/types.ts b/dashboard/src/api/types.ts new file mode 100644 index 0000000..e33dc85 --- /dev/null +++ b/dashboard/src/api/types.ts @@ -0,0 +1,161 @@ +export type Priority = 'urgent' | 'high' | 'medium' | 'low'; +export type TaskStatus = 'urgent' | 'open' | 'in-progress' | 'done'; +export type AgentTaskStatus = 'queued' | 'running' | 'done' | 'failed'; +export type RunStatus = 'success' | 'failure' | 'timeout'; + +export interface Agent { + name: string; + executable: string; + model?: string; + escalate_to?: string; + mcp_servers: string[]; + skills: string[]; + timeout: number; + max_retries: number; + env: Record; + body?: string; +} + +export interface Skill { + name: string; + description: string; + version?: number; + requires_mcp: string[]; + inputs: string[]; + outputs: string[]; + body?: string; +} + +export interface CronJob { + name: string; + title: string; + schedule: string; + agent: string; + enabled: boolean; + status: 'active' | 'paused'; + last_run?: string; + last_status?: RunStatus; + next_run?: string; + run_count: number; +} + +export interface HumanTask { + id: string; + title: string; + priority: Priority; + status: TaskStatus; + source?: string; + repo?: string; + labels: string[]; + created: string; + due?: string; + body: string; +} + +export interface AgentTask { + id: string; + title: string; + agent: string; + priority: Priority; + type?: string; + status: AgentTaskStatus; + created: string; + started?: string; + completed?: string; + retry: number; + max_retries: number; + input?: unknown; + output?: unknown; + error?: string; + body: string; +} + +export interface KnowledgeNote { + path: string; + title: string; + tags: string[]; +} + +export interface TreeNode { + name: string; + path: string; + type: 'file' | 'directory'; + children?: TreeNode[]; +} + +export interface VaultStats { + agents: number; + skills: number; + crons_scheduled: number; + human_tasks: Record; + agent_tasks: Record; + knowledge_notes: number; + total_tasks_executed: number; + total_cron_fires: number; +} + +export interface WsEvent { + type: string; + area: string; + path: string; + data?: Record; +} + +export interface HealthStatus { + status: string; + version: string; + uptime_secs: number; + agents: number; + crons_scheduled: number; + total_tasks_executed: number; +} + +// View system types + +export interface ViewPageDef { + name: string; + type: string; + title?: string; + icon?: string; + route?: string; + position?: number; + layout?: string; + component?: string; + description?: string; +} + +export interface WidgetInstanceDef { + widget: string; + props?: Record; +} + +export interface ViewRegions { + [region: string]: WidgetInstanceDef[]; +} + +export interface ViewDetail { + path: string; + frontmatter: { + type: string; + title?: string; + icon?: string; + route?: string; + position?: number; + layout?: string; + regions?: ViewRegions; + name?: string; + description?: string; + component?: string; + } | null; + body: string; +} + +export interface NotificationItem { + id: string; + title: string; + message?: string; + level?: string; + source?: string; + created?: string; + expires?: string; +} diff --git a/dashboard/src/api/ws.ts b/dashboard/src/api/ws.ts new file mode 100644 index 0000000..c227501 --- /dev/null +++ b/dashboard/src/api/ws.ts @@ -0,0 +1,80 @@ +import type { WsEvent } from './types'; + +type Listener = (event: WsEvent) => void; + +export class VaultWebSocket { + private ws: WebSocket | null = null; + private listeners: Map> = new Map(); + private globalListeners: Set = new Set(); + private reconnectTimer: ReturnType | null = null; + private url: string; + + constructor(url?: string) { + const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + this.url = url || `${proto}//${window.location.host}/ws`; + } + + connect() { + if (this.ws?.readyState === WebSocket.OPEN) return; + + this.ws = new WebSocket(this.url); + + this.ws.onmessage = (msg) => { + try { + const event: WsEvent = JSON.parse(msg.data); + this.globalListeners.forEach((fn) => fn(event)); + this.listeners.get(event.type)?.forEach((fn) => fn(event)); + } catch { + // ignore malformed messages + } + }; + + this.ws.onclose = () => { + this.scheduleReconnect(); + }; + + this.ws.onerror = () => { + this.ws?.close(); + }; + } + + disconnect() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + this.ws?.close(); + this.ws = null; + } + + /** Listen to a specific event type */ + on(type: string, fn: Listener): () => void { + if (!this.listeners.has(type)) { + this.listeners.set(type, new Set()); + } + this.listeners.get(type)!.add(fn); + return () => this.listeners.get(type)?.delete(fn); + } + + /** Listen to all events */ + onAny(fn: Listener): () => void { + this.globalListeners.add(fn); + return () => this.globalListeners.delete(fn); + } + + send(action: Record) { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(action)); + } + } + + private scheduleReconnect() { + if (this.reconnectTimer) return; + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + this.connect(); + }, 3000); + } +} + +export const vaultWs = new VaultWebSocket(); diff --git a/dashboard/src/components/ActivityFeed.tsx b/dashboard/src/components/ActivityFeed.tsx new file mode 100644 index 0000000..6623486 --- /dev/null +++ b/dashboard/src/components/ActivityFeed.tsx @@ -0,0 +1,39 @@ +import { useActivity } from '../hooks/useApi'; + +export function ActivityFeed() { + const { data: activity, isLoading } = useActivity(); + + if (isLoading) return
Loading...
; + if (!activity?.length) return
No recent activity
; + + return ( +
+ {activity.slice(0, 20).map((item, i) => ( +
+ + {item.name} + {timeAgo(item.modified)} +
+ ))} +
+ ); +} + +function kindColor(kind: string): string { + switch (kind) { + case 'human_task': return 'bg-accent'; + case 'agent_task': return 'bg-warning'; + case 'knowledge': return 'bg-success'; + default: return 'bg-text-muted'; + } +} + +function timeAgo(iso: string): string { + const ms = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(ms / 60000); + if (mins < 1) return 'now'; + if (mins < 60) return `${mins}m`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h`; + return `${Math.floor(hrs / 24)}d`; +} diff --git a/dashboard/src/components/AgentCard.tsx b/dashboard/src/components/AgentCard.tsx new file mode 100644 index 0000000..9253eaf --- /dev/null +++ b/dashboard/src/components/AgentCard.tsx @@ -0,0 +1,42 @@ +import type { Agent } from '../api/types'; + +interface Props { + agent: Agent; + onTrigger: (name: string) => void; +} + +export function AgentCard({ agent, onTrigger }: Props) { + return ( +
+
+

{agent.name}

+ +
+ +
+ {agent.executable} + {agent.model && {agent.model}} +
+ + {agent.skills.length > 0 && ( +
+ {agent.skills.map((s) => ( + + {s} + + ))} +
+ )} + +
+ timeout: {agent.timeout}s + {agent.max_retries > 0 && retries: {agent.max_retries}} +
+
+ ); +} diff --git a/dashboard/src/components/CommandPalette.tsx b/dashboard/src/components/CommandPalette.tsx new file mode 100644 index 0000000..44daa0e --- /dev/null +++ b/dashboard/src/components/CommandPalette.tsx @@ -0,0 +1,94 @@ +import { useState, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; + +interface Command { + id: string; + label: string; + action: () => void; +} + +export function CommandPalette() { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const inputRef = useRef(null); + const navigate = useNavigate(); + + const commands: Command[] = [ + { id: 'new-task', label: 'New Human Task', action: () => navigate('/editor?new=todos/harald/open') }, + { id: 'new-agent-task', label: 'New Agent Task', action: () => navigate('/editor?new=todos/agent/queued') }, + { id: 'new-agent', label: 'New Agent', action: () => navigate('/editor?new=agents') }, + { id: 'new-skill', label: 'New Skill', action: () => navigate('/editor?new=skills') }, + { id: 'new-cron', label: 'New Cron Job', action: () => navigate('/editor?new=crons/active') }, + { id: 'new-note', label: 'New Knowledge Note', action: () => navigate('/editor?new=knowledge') }, + { id: 'nav-tasks', label: 'Go to Tasks', action: () => navigate('/') }, + { id: 'nav-agents', label: 'Go to Agents', action: () => navigate('/agents') }, + { id: 'nav-crons', label: 'Go to Crons', action: () => navigate('/crons') }, + { id: 'nav-queue', label: 'Go to Agent Queue', action: () => navigate('/queue') }, + { id: 'nav-knowledge', label: 'Go to Knowledge', action: () => navigate('/knowledge') }, + { id: 'nav-editor', label: 'Open Editor', action: () => navigate('/editor') }, + ]; + + const filtered = query + ? commands.filter((c) => c.label.toLowerCase().includes(query.toLowerCase())) + : commands; + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + setOpen((prev) => !prev); + setQuery(''); + } + if (e.key === 'Escape') setOpen(false); + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, []); + + useEffect(() => { + if (open) inputRef.current?.focus(); + }, [open]); + + if (!open) return null; + + return ( +
setOpen(false)}> +
+
e.stopPropagation()} + > + setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && filtered.length > 0) { + filtered[0].action(); + setOpen(false); + } + }} + /> +
+ {filtered.map((cmd) => ( + + ))} + {filtered.length === 0 && ( +
No matching commands
+ )} +
+
+
+ ); +} diff --git a/dashboard/src/components/CronRow.tsx b/dashboard/src/components/CronRow.tsx new file mode 100644 index 0000000..8db4965 --- /dev/null +++ b/dashboard/src/components/CronRow.tsx @@ -0,0 +1,44 @@ +import type { CronJob } from '../api/types'; +import { StatusBadge } from './StatusBadge'; + +interface Props { + cron: CronJob; + onTrigger: (name: string) => void; + onToggle: (name: string, active: boolean) => void; +} + +export function CronRow({ cron, onTrigger, onToggle }: Props) { + const isActive = cron.status === 'active'; + + return ( +
+
+
{cron.title}
+
+ {cron.schedule} + agent: {cron.agent} + runs: {cron.run_count} +
+
+ +
+ {cron.last_status && } + + + + + +
+
+ ); +} diff --git a/dashboard/src/components/FileTree.tsx b/dashboard/src/components/FileTree.tsx new file mode 100644 index 0000000..0f68c94 --- /dev/null +++ b/dashboard/src/components/FileTree.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import type { TreeNode } from '../api/types'; + +interface Props { + tree: TreeNode; + selectedPath?: string; + onSelect: (path: string) => void; + onCreateFile?: (dir: string) => void; +} + +export function FileTree({ tree, selectedPath, onSelect, onCreateFile }: Props) { + return ( +
+ {tree.children?.map((node) => ( + + ))} +
+ ); +} + +function TreeItem({ + node, + depth, + selectedPath, + onSelect, + onCreateFile, +}: { + node: TreeNode; + depth: number; + selectedPath?: string; + onSelect: (path: string) => void; + onCreateFile?: (dir: string) => void; +}) { + const [expanded, setExpanded] = useState(depth < 1); + const isDir = node.type === 'directory'; + const isSelected = node.path === selectedPath; + const pad = `${depth * 12 + 8}px`; + + if (isDir) { + return ( +
+
setExpanded(!expanded)} + onContextMenu={(e) => { + e.preventDefault(); + onCreateFile?.(node.path); + }} + > + {expanded ? '\u25BE' : '\u25B8'} + {node.name} +
+ {expanded && + node.children?.map((child) => ( + + ))} +
+ ); + } + + return ( +
onSelect(node.path)} + > + {node.name} +
+ ); +} diff --git a/dashboard/src/components/Kanban.tsx b/dashboard/src/components/Kanban.tsx new file mode 100644 index 0000000..bb13809 --- /dev/null +++ b/dashboard/src/components/Kanban.tsx @@ -0,0 +1,77 @@ +import { + DragDropContext, + Droppable, + Draggable, + type DropResult, +} from '@hello-pangea/dnd'; +import type { HumanTask, TaskStatus } from '../api/types'; +import { TaskCard } from './TaskCard'; + +const COLUMNS: { id: TaskStatus; label: string }[] = [ + { id: 'urgent', label: 'Urgent' }, + { id: 'open', label: 'Open' }, + { id: 'in-progress', label: 'In Progress' }, + { id: 'done', label: 'Done' }, +]; + +interface Props { + tasks: HumanTask[]; + onMove: (id: string, fromStatus: string, toStatus: string) => void; +} + +export function Kanban({ tasks, onMove }: Props) { + const byStatus = (status: TaskStatus) => tasks.filter((t) => t.status === status); + + const handleDragEnd = (result: DropResult) => { + if (!result.destination) return; + const fromStatus = result.source.droppableId; + const toStatus = result.destination.droppableId; + if (fromStatus === toStatus) return; + onMove(result.draggableId, fromStatus, toStatus); + }; + + return ( + +
+ {COLUMNS.map((col) => { + const items = byStatus(col.id); + return ( +
+
+

{col.label}

+ {items.length} +
+ + + {(provided, snapshot) => ( +
+ {items.map((task, idx) => ( + + {(prov) => ( +
+ +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+ ); + })} +
+
+ ); +} diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx new file mode 100644 index 0000000..7da13ec --- /dev/null +++ b/dashboard/src/components/Layout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from 'react-router-dom'; +import { NavigationSidebar } from './NavigationSidebar'; + +export function Layout() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/dashboard/src/components/NavigationSidebar.tsx b/dashboard/src/components/NavigationSidebar.tsx new file mode 100644 index 0000000..e777c8a --- /dev/null +++ b/dashboard/src/components/NavigationSidebar.tsx @@ -0,0 +1,96 @@ +import { NavLink } from 'react-router-dom'; +import { useHealth } from '../hooks/useApi'; +import { useViewPages } from '../views/ViewRenderer'; + +/** Static built-in navigation entries */ +const builtinNav = [ + { to: '/', label: 'Tasks', icon: '/' }, + { to: '/agents', label: 'Agents' }, + { to: '/crons', label: 'Crons' }, + { to: '/queue', label: 'Queue' }, + { to: '/knowledge', label: 'Knowledge' }, + { to: '/editor', label: 'Editor' }, +]; + +export function NavigationSidebar() { + const { data: health } = useHealth(); + const { data: viewPages } = useViewPages(); + + // Build dynamic nav entries from view pages, sorted by position + const dynamicNav = (viewPages || []) + .filter((p) => p.route && p.title) + .sort((a, b) => (a.position ?? 100) - (b.position ?? 100)) + .map((p) => ({ + to: p.route!.startsWith('/') ? p.route! : `/${p.route}`, + label: p.title || p.name, + icon: p.icon, + })); + + return ( + + ); +} diff --git a/dashboard/src/components/NotificationBanner.tsx b/dashboard/src/components/NotificationBanner.tsx new file mode 100644 index 0000000..3104cab --- /dev/null +++ b/dashboard/src/components/NotificationBanner.tsx @@ -0,0 +1,49 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { listNotifications, dismissNotification } from '../api/client'; + +const LEVEL_STYLES: Record = { + info: 'bg-accent/10 border-accent/30 text-accent', + warning: 'bg-warning/10 border-warning/30 text-warning', + error: 'bg-danger/10 border-danger/30 text-danger', + success: 'bg-success/10 border-success/30 text-success', +}; + +export function NotificationBanner() { + const queryClient = useQueryClient(); + const { data: notifications } = useQuery({ + queryKey: ['notifications'], + queryFn: listNotifications, + refetchInterval: 30000, + }); + + const dismiss = useMutation({ + mutationFn: dismissNotification, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['notifications'] }), + }); + + if (!notifications?.length) return null; + + return ( +
+ {notifications.map((n) => ( +
+
+
{n.title}
+ {n.message &&
{n.message}
} +
+ +
+ ))} +
+ ); +} diff --git a/dashboard/src/components/StatusBadge.tsx b/dashboard/src/components/StatusBadge.tsx new file mode 100644 index 0000000..b059e5a --- /dev/null +++ b/dashboard/src/components/StatusBadge.tsx @@ -0,0 +1,26 @@ +const styles: Record = { + urgent: 'bg-urgent/20 text-urgent', + high: 'bg-danger/20 text-danger', + medium: 'bg-warning/20 text-warning', + low: 'bg-text-muted/20 text-text-secondary', + open: 'bg-accent/20 text-accent', + 'in-progress': 'bg-warning/20 text-warning', + done: 'bg-success/20 text-success', + queued: 'bg-text-muted/20 text-text-secondary', + running: 'bg-accent/20 text-accent', + failed: 'bg-danger/20 text-danger', + active: 'bg-success/20 text-success', + paused: 'bg-text-muted/20 text-text-secondary', + success: 'bg-success/20 text-success', + failure: 'bg-danger/20 text-danger', + timeout: 'bg-warning/20 text-warning', +}; + +export function StatusBadge({ value }: { value: string }) { + const cls = styles[value] || 'bg-surface-overlay text-text-secondary'; + return ( + + {value} + + ); +} diff --git a/dashboard/src/components/TaskCard.tsx b/dashboard/src/components/TaskCard.tsx new file mode 100644 index 0000000..3a1bd88 --- /dev/null +++ b/dashboard/src/components/TaskCard.tsx @@ -0,0 +1,35 @@ +import type { HumanTask } from '../api/types'; +import { StatusBadge } from './StatusBadge'; + +export function TaskCard({ task }: { task: HumanTask }) { + const age = timeAgo(task.created); + + return ( +
+
{task.title}
+
+ + {task.labels.map((l) => ( + + {l} + + ))} +
+
+ {age} + {task.source && via {task.source}} +
+
+ ); +} + +function timeAgo(iso: string): string { + const ms = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(ms / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + const days = Math.floor(hrs / 24); + return `${days}d ago`; +} diff --git a/dashboard/src/components/assistant/ChatSidebar.tsx b/dashboard/src/components/assistant/ChatSidebar.tsx new file mode 100644 index 0000000..96d8b1c --- /dev/null +++ b/dashboard/src/components/assistant/ChatSidebar.tsx @@ -0,0 +1,185 @@ +import { useState, useRef, useEffect } from 'react'; +import { ModelSelector } from './ModelSelector'; +import { DiffView } from './DiffView'; + +interface Message { + role: 'user' | 'assistant'; + content: string; +} + +interface Props { + filePath?: string; + onClose: () => void; +} + +/** Extract unified diff blocks from markdown-formatted assistant response */ +function extractDiffs(content: string): string[] { + const diffs: string[] = []; + const regex = /```(?:diff)?\n([\s\S]*?)```/g; + let match; + while ((match = regex.exec(content)) !== null) { + const block = match[1].trim(); + if (block.includes('@@') || block.startsWith('---') || block.startsWith('diff ')) { + diffs.push(block); + } + } + return diffs; +} + +export function ChatSidebar({ filePath, onClose }: Props) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [model, setModel] = useState('local/qwen3'); + const [loading, setLoading] = useState(false); + const [applyingDiff, setApplyingDiff] = useState(null); + const scrollRef = useRef(null); + + useEffect(() => { + scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }); + }, [messages]); + + const sendMessage = async () => { + const text = input.trim(); + if (!text || loading) return; + + const userMsg: Message = { role: 'user', content: text }; + const newMessages = [...messages, userMsg]; + setMessages(newMessages); + setInput(''); + setLoading(true); + + try { + const res = await fetch('/api/assistant/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: newMessages.map((m) => ({ role: m.role, content: m.content })), + model, + file_path: filePath, + }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: 'Request failed' })); + setMessages([...newMessages, { role: 'assistant', content: `Error: ${err.error}` }]); + return; + } + + const data = await res.json(); + setMessages([...newMessages, { role: 'assistant', content: data.message.content }]); + } catch (e) { + setMessages([ + ...newMessages, + { role: 'assistant', content: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }, + ]); + } finally { + setLoading(false); + } + }; + + const applyDiff = async (diff: string) => { + if (!filePath) return; + setApplyingDiff(diff); + try { + const res = await fetch('/api/assistant/apply-diff', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ file_path: filePath, diff }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: 'Apply failed' })); + alert(`Failed to apply diff: ${err.error}`); + } + } finally { + setApplyingDiff(null); + } + }; + + const removeDiff = (diff: string) => { + // Remove the diff from the last assistant message display (user chose to reject) + void diff; + }; + + return ( +
+ {/* Header */} +
+ Assistant +
+ + +
+
+ + {/* Messages */} +
+ {messages.length === 0 && ( +
+ Ask about the current file or request edits. The assistant will suggest diffs you can + apply directly. +
+ )} + {messages.map((msg, i) => ( +
+
+
{msg.content}
+
+ {/* Render extractable diffs as apply/reject widgets */} + {msg.role === 'assistant' && + extractDiffs(msg.content).map((diff, di) => ( +
+ applyDiff(diff)} + onReject={() => removeDiff(diff)} + applying={applyingDiff === diff} + /> +
+ ))} +
+ ))} + {loading && ( +
+ Thinking... +
+ )} +
+ + {/* Input */} +
+
+ setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }} + placeholder="Ask about this file..." + className="flex-1 rounded border border-border bg-surface-base px-2 py-1.5 text-sm text-text-primary outline-none placeholder:text-text-muted focus:border-accent" + /> + +
+ {filePath && ( +
Context: {filePath}
+ )} +
+
+ ); +} diff --git a/dashboard/src/components/assistant/DiffView.tsx b/dashboard/src/components/assistant/DiffView.tsx new file mode 100644 index 0000000..3ef82d6 --- /dev/null +++ b/dashboard/src/components/assistant/DiffView.tsx @@ -0,0 +1,51 @@ +interface Props { + diff: string; + onApply: () => void; + onReject: () => void; + applying?: boolean; +} + +export function DiffView({ diff, onApply, onReject, applying }: Props) { + const lines = diff.split('\n'); + + return ( +
+
+ Suggested Changes +
+ + +
+
+
+        {lines.map((line, i) => {
+          let cls = 'text-text-secondary';
+          if (line.startsWith('+') && !line.startsWith('+++')) {
+            cls = 'text-success bg-success/10';
+          } else if (line.startsWith('-') && !line.startsWith('---')) {
+            cls = 'text-danger bg-danger/10';
+          } else if (line.startsWith('@@')) {
+            cls = 'text-accent';
+          }
+          return (
+            
+ {line} +
+ ); + })} +
+
+ ); +} diff --git a/dashboard/src/components/assistant/ModelSelector.tsx b/dashboard/src/components/assistant/ModelSelector.tsx new file mode 100644 index 0000000..8733e8c --- /dev/null +++ b/dashboard/src/components/assistant/ModelSelector.tsx @@ -0,0 +1,31 @@ +import { useQuery } from '@tanstack/react-query'; + +async function fetchModels() { + const res = await fetch('/api/assistant/models'); + if (!res.ok) throw new Error('Failed to fetch models'); + return res.json() as Promise<{ id: string; name: string }[]>; +} + +interface Props { + value: string; + onChange: (model: string) => void; +} + +export function ModelSelector({ value, onChange }: Props) { + const { data: models } = useQuery({ queryKey: ['assistant-models'], queryFn: fetchModels }); + + return ( + + ); +} diff --git a/dashboard/src/components/editor/FileEditor.tsx b/dashboard/src/components/editor/FileEditor.tsx new file mode 100644 index 0000000..9aa7ace --- /dev/null +++ b/dashboard/src/components/editor/FileEditor.tsx @@ -0,0 +1,254 @@ +import { useState, useEffect } from 'react'; +import { MarkdownEditor } from './MarkdownEditor'; +import { MarkdownPreview, renderMarkdown } from './MarkdownPreview'; +import { AgentForm } from '../forms/AgentForm'; +import { CronForm } from '../forms/CronForm'; +import { HumanTaskForm } from '../forms/HumanTaskForm'; +import { AgentTaskForm } from '../forms/AgentTaskForm'; +import { SkillForm } from '../forms/SkillForm'; +import { KnowledgeForm } from '../forms/KnowledgeForm'; +import { readFile, writeFile } from '../../api/client'; + +type ViewMode = 'edit' | 'preview' | 'split'; +type FmMode = 'form' | 'yaml'; + +interface Props { + path: string; + onSaved?: () => void; + onToggleAssistant?: () => void; +} + +export function FileEditor({ path, onSaved, onToggleAssistant }: Props) { + const [frontmatter, setFrontmatter] = useState>({}); + const [body, setBody] = useState(''); + const [rawYaml, setRawYaml] = useState(''); + const [viewMode, setViewMode] = useState('edit'); + const [fmMode, setFmMode] = useState('form'); + const [dirty, setDirty] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(); + + const entityType = detectEntityType(path); + + // Load file + useEffect(() => { + if (!path) return; + setError(undefined); + readFile(path) + .then((data) => { + const fm = (data.frontmatter as Record) || {}; + setFrontmatter(fm); + setBody(data.body || ''); + setRawYaml(toYaml(fm)); + setDirty(false); + }) + .catch((e) => setError(e.message)); + }, [path]); + + const handleFmChange = (values: Record) => { + setFrontmatter(values); + setRawYaml(toYaml(values)); + setDirty(true); + }; + + const handleYamlChange = (yaml: string) => { + setRawYaml(yaml); + try { + // We don't parse YAML client-side; just track the raw value + setDirty(true); + } catch { + // ignore parse errors during editing + } + }; + + const handleBodyChange = (value: string) => { + setBody(value); + setDirty(true); + }; + + const handleSave = async () => { + setSaving(true); + setError(undefined); + try { + if (fmMode === 'yaml') { + // Send raw content + const raw = rawYaml.trim() + ? `---\n${rawYaml.trimEnd()}\n---\n${body}` + : body; + await writeFile(path, { raw }); + } else { + await writeFile(path, { frontmatter, body }); + } + setDirty(false); + onSaved?.(); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Save failed'); + } finally { + setSaving(false); + } + }; + + // Ctrl+S / Cmd+S + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 's') { + e.preventDefault(); + if (dirty) handleSave(); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }); + + return ( +
+ {/* Toolbar */} +
+
+ {path} + {dirty && unsaved} +
+
+ {/* View mode toggle */} + {(['edit', 'split', 'preview'] as ViewMode[]).map((m) => ( + + ))} + | + {/* Frontmatter mode toggle */} + + {onToggleAssistant && ( + + )} + +
+
+ + {error && ( +
{error}
+ )} + +
+ {/* Frontmatter panel */} +
+ {fmMode === 'form' ? ( + renderEntityForm(entityType, frontmatter, handleFmChange) + ) : ( +
+ +
+ )} +
+ + {/* Body editor / preview */} +
+ {(viewMode === 'edit' || viewMode === 'split') && ( +
+ +
+ )} + {(viewMode === 'preview' || viewMode === 'split') && ( +
+ +
+ )} +
+
+
+ ); +} + +type EntityType = 'agent' | 'skill' | 'cron' | 'human-task' | 'agent-task' | 'knowledge' | 'generic'; + +function detectEntityType(path: string): EntityType { + if (path.startsWith('agents/')) return 'agent'; + if (path.startsWith('skills/')) return 'skill'; + if (path.startsWith('crons/')) return 'cron'; + if (path.startsWith('todos/harald/')) return 'human-task'; + if (path.startsWith('todos/agent/')) return 'agent-task'; + if (path.startsWith('knowledge/')) return 'knowledge'; + return 'generic'; +} + +function renderEntityForm( + type: EntityType, + values: Record, + onChange: (v: Record) => void, +) { + switch (type) { + case 'agent': + return ; + case 'skill': + return ; + case 'cron': + return ; + case 'human-task': + return ; + case 'agent-task': + return ; + case 'knowledge': + return ; + default: + return ( +
+ No structured form for this file type. Switch to YAML mode. +
+ ); + } +} + +function toYaml(obj: Record): string { + // Simple YAML serialization for the form -> YAML toggle + const lines: string[] = []; + for (const [key, value] of Object.entries(obj)) { + if (value === undefined || value === null) continue; + if (Array.isArray(value)) { + if (value.length === 0) { + lines.push(`${key}: []`); + } else { + lines.push(`${key}:`); + for (const item of value) { + lines.push(` - ${typeof item === 'string' ? item : JSON.stringify(item)}`); + } + } + } else if (typeof value === 'object') { + lines.push(`${key}: ${JSON.stringify(value)}`); + } else if (typeof value === 'string' && value.includes('\n')) { + lines.push(`${key}: |`); + for (const line of value.split('\n')) { + lines.push(` ${line}`); + } + } else { + lines.push(`${key}: ${value}`); + } + } + return lines.join('\n') + '\n'; +} diff --git a/dashboard/src/components/editor/FrontmatterForm.tsx b/dashboard/src/components/editor/FrontmatterForm.tsx new file mode 100644 index 0000000..dd77409 --- /dev/null +++ b/dashboard/src/components/editor/FrontmatterForm.tsx @@ -0,0 +1,190 @@ +import { useState } from 'react'; + +interface Props { + fields: FieldDef[]; + values: Record; + onChange: (values: Record) => void; +} + +export interface FieldDef { + name: string; + label: string; + type: 'text' | 'textarea' | 'number' | 'select' | 'tags' | 'datetime' | 'checkbox' | 'json'; + options?: string[]; + placeholder?: string; + required?: boolean; +} + +export function FrontmatterForm({ fields, values, onChange }: Props) { + const update = (name: string, value: unknown) => { + onChange({ ...values, [name]: value }); + }; + + return ( +
+ {fields.map((field) => ( +
+ + update(field.name, v)} /> +
+ ))} +
+ ); +} + +function FieldInput({ + field, + value, + onChange, +}: { + field: FieldDef; + value: unknown; + onChange: (v: unknown) => void; +}) { + const cls = + 'w-full rounded border border-border bg-surface px-3 py-1.5 text-sm text-text-primary outline-none focus:border-accent'; + + switch (field.type) { + case 'text': + return ( + onChange(e.target.value)} + placeholder={field.placeholder} + /> + ); + + case 'textarea': + return ( +