diff --git a/.loki-project.json b/.loki-project.json
new file mode 100644
index 0000000..165f4c7
--- /dev/null
+++ b/.loki-project.json
@@ -0,0 +1 @@
+{"type":"rust","build":"cargo build","test":"cargo test","check":"cargo check","_detected_by":"heuristic","_cached_at":"2026-04-13T13:36:33-06:00"}
diff --git a/Cargo.lock b/Cargo.lock
index 62acbb4..2f9f5c2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -65,9 +65,9 @@ dependencies = [
[[package]]
name = "anndists"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8238f99889a837cd6641360f9f3ead18f70b07bf6ce1f04a319bc6bd8a2f48f1"
+checksum = "9a8396b473aa0bceed68fb32462505387ea39fa47c7029417e0a49f10592b036"
dependencies = [
"anyhow",
"cfg-if",
@@ -165,9 +165,9 @@ dependencies = [
[[package]]
name = "arc-swap"
-version = "1.9.0"
+version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6"
+checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
dependencies = [
"rustversion",
]
@@ -183,7 +183,7 @@ dependencies = [
"convert_case 0.8.0",
"dirs",
"either",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"natord",
"nom 8.0.0",
"num_cpus",
@@ -310,11 +310,11 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
-version = "1.16.2"
+version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
+checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
dependencies = [
- "aws-lc-sys",
+ "aws-lc-sys 0.40.0",
"zeroize",
]
@@ -331,6 +331,18 @@ dependencies = [
"fs_extra",
]
+[[package]]
+name = "aws-lc-sys"
+version = "0.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
[[package]]
name = "aws-runtime"
version = "1.6.0"
@@ -534,11 +546,11 @@ dependencies = [
"hyper 0.14.32",
"hyper 1.9.0",
"hyper-rustls 0.24.2",
- "hyper-rustls 0.27.7",
+ "hyper-rustls 0.27.9",
"hyper-util",
"pin-project-lite",
"rustls 0.21.12",
- "rustls 0.23.37",
+ "rustls 0.23.39",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
@@ -668,9 +680,9 @@ dependencies = [
[[package]]
name = "axum"
-version = "0.8.8"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
"bytes",
@@ -869,9 +881,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
-version = "2.11.0"
+version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
dependencies = [
"serde_core",
]
@@ -987,9 +999,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "cc"
-version = "1.2.58"
+version = "1.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
+checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1037,7 +1049,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
- "rand_core 0.10.0",
+ "rand_core 0.10.1",
]
[[package]]
@@ -1091,9 +1103,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.6.0"
+version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1114,9 +1126,9 @@ dependencies = [
[[package]]
name = "clap_complete"
-version = "4.6.0"
+version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
+checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb"
dependencies = [
"clap",
"clap_lex",
@@ -1136,9 +1148,9 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.6.0"
+version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
@@ -1351,23 +1363,6 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
-[[package]]
-name = "crossterm"
-version = "0.28.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
-dependencies = [
- "bitflags",
- "crossterm_winapi",
- "filedescriptor",
- "mio",
- "parking_lot",
- "rustix 0.38.44",
- "signal-hook",
- "signal-hook-mio",
- "winapi",
-]
-
[[package]]
name = "crossterm"
version = "0.29.0"
@@ -1378,9 +1373,10 @@ dependencies = [
"crossterm_winapi",
"derive_more 2.1.1",
"document-features",
+ "filedescriptor",
"mio",
"parking_lot",
- "rustix 1.1.4",
+ "rustix",
"serde",
"signal-hook",
"signal-hook-mio",
@@ -1811,9 +1807,9 @@ dependencies = [
[[package]]
name = "fastrand"
-version = "2.3.0"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "fd-lock"
@@ -1822,7 +1818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
- "rustix 1.1.4",
+ "rustix",
"windows-sys 0.59.0",
]
@@ -2038,7 +2034,7 @@ dependencies = [
"once_cell",
"prost",
"prost-types",
- "reqwest",
+ "reqwest 0.12.28",
"secret-vault-value",
"serde",
"serde_json",
@@ -2082,7 +2078,7 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
- "rustix 1.1.4",
+ "rustix",
"windows-link",
]
@@ -2122,7 +2118,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
- "rand_core 0.10.0",
+ "rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -2149,7 +2145,7 @@ dependencies = [
"argon2",
"async-trait",
"aws-config",
- "aws-lc-sys",
+ "aws-lc-sys 0.39.1",
"aws-sdk-secretsmanager",
"azure_core",
"azure_identity",
@@ -2162,7 +2158,7 @@ dependencies = [
"clap_complete",
"confy",
"crc32c",
- "crossterm 0.29.0",
+ "crossterm",
"dialoguer",
"dirs",
"futures",
@@ -2199,7 +2195,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"slab",
"tokio",
"tokio-util",
@@ -2218,7 +2214,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.4.0",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"slab",
"tokio",
"tokio-util",
@@ -2244,9 +2240,9 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.16.1"
+version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heck"
@@ -2288,14 +2284,14 @@ dependencies = [
"cpu-time",
"env_logger",
"hashbrown 0.15.5",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"lazy_static",
"log",
"mmap-rs",
"num-traits",
"num_cpus",
"parking_lot",
- "rand 0.9.2",
+ "rand 0.9.4",
"rayon",
"serde",
]
@@ -2499,16 +2495,15 @@ dependencies = [
[[package]]
name = "hyper-rustls"
-version = "0.27.7"
+version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http 1.4.0",
"hyper 1.9.0",
"hyper-util",
- "rustls 0.23.37",
+ "rustls 0.23.39",
"rustls-native-certs",
- "rustls-pki-types",
"tokio",
"tokio-rustls 0.26.4",
"tower-service",
@@ -2719,12 +2714,12 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.13.0"
+version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
- "hashbrown 0.16.1",
+ "hashbrown 0.17.0",
"serde",
"serde_core",
]
@@ -2754,7 +2749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
dependencies = [
"bitflags",
- "crossterm 0.29.0",
+ "crossterm",
"dyn-clone",
"fuzzy-matcher",
"unicode-segmentation",
@@ -2848,9 +2843,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
-version = "0.2.23"
+version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
+checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
dependencies = [
"jiff-static",
"log",
@@ -2861,9 +2856,9 @@ dependencies = [
[[package]]
name = "jiff-static"
-version = "0.2.23"
+version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
+checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7"
dependencies = [
"proc-macro2",
"quote",
@@ -2882,9 +2877,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.94"
+version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
+checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
"cfg-if",
"futures-util",
@@ -2944,9 +2939,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
-version = "0.2.184"
+version = "0.2.185"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
+checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
name = "libloading"
@@ -2960,19 +2955,13 @@ dependencies = [
[[package]]
name = "libredox"
-version = "0.1.15"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
dependencies = [
"libc",
]
-[[package]]
-name = "linux-raw-sys"
-version = "0.4.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
-
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -3032,7 +3021,7 @@ dependencies = [
"log-mdc",
"mock_instant",
"parking_lot",
- "rand 0.9.2",
+ "rand 0.9.4",
"serde",
"serde-value",
"serde_json",
@@ -3065,8 +3054,7 @@ dependencies = [
"clap_complete",
"clap_complete_nushell",
"colored",
- "crossterm 0.28.1",
- "crossterm 0.29.0",
+ "crossterm",
"dirs",
"duct",
"dunce",
@@ -3081,7 +3069,7 @@ dependencies = [
"http-body-util",
"hyper 1.9.0",
"hyper-util",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"indoc",
"inquire",
"is-terminal",
@@ -3095,10 +3083,10 @@ dependencies = [
"parking_lot",
"path-absolutize",
"pretty_assertions",
- "rand 0.10.0",
+ "rand 0.10.1",
"rayon",
"reedline",
- "reqwest",
+ "reqwest 0.12.28",
"reqwest-eventsource",
"rmcp",
"rust-embed",
@@ -3106,6 +3094,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
+ "serial_test",
"sha2",
"shell-words",
"strum_macros 0.27.2",
@@ -3671,9 +3660,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "open"
-version = "5.3.3"
+version = "5.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
+checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd"
dependencies = [
"is-wsl",
"libc",
@@ -3682,9 +3671,9 @@ dependencies = [
[[package]]
name = "openssl"
-version = "0.10.76"
+version = "0.10.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
+checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
dependencies = [
"bitflags",
"cfg-if",
@@ -3714,18 +3703,18 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-src"
-version = "300.5.5+3.5.5"
+version = "300.6.0+3.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
+checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
-version = "0.9.112"
+version = "0.9.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
+checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
dependencies = [
"cc",
"libc",
@@ -3822,9 +3811,9 @@ dependencies = [
[[package]]
name = "pastey"
-version = "0.2.1"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec"
+checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a"
[[package]]
name = "path-absolutize"
@@ -3874,7 +3863,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
]
[[package]]
@@ -3904,7 +3893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
- "rand 0.8.5",
+ "rand 0.8.6",
]
[[package]]
@@ -3963,9 +3952,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
-version = "0.3.32"
+version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "plist"
@@ -3974,7 +3963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
dependencies = [
"base64",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"quick-xml 0.38.4",
"serde",
"time",
@@ -3999,9 +3988,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
-version = "0.2.6"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
@@ -4094,7 +4083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55"
dependencies = [
"futures",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"nix 0.31.2",
"tokio",
"tracing",
@@ -4163,7 +4152,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash",
- "rustls 0.23.37",
+ "rustls 0.23.39",
"socket2 0.6.3",
"thiserror 2.0.18",
"tokio",
@@ -4180,10 +4169,10 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
- "rand 0.9.2",
+ "rand 0.9.4",
"ring",
"rustc-hash",
- "rustls 0.23.37",
+ "rustls 0.23.39",
"rustls-pki-types",
"slab",
"thiserror 2.0.18",
@@ -4229,18 +4218,18 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
-version = "0.8.5"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
-version = "0.9.2"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha",
"rand_core 0.9.5",
@@ -4248,13 +4237,13 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.2",
- "rand_core 0.10.0",
+ "rand_core 0.10.1",
]
[[package]]
@@ -4287,15 +4276,15 @@ dependencies = [
[[package]]
name = "rand_core"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rayon"
-version = "1.11.0"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
@@ -4338,7 +4327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9e7c532bfc2759bc8a28902c04e8b993fc13ebd085ee4292eb1b230fa9beef"
dependencies = [
"chrono",
- "crossterm 0.29.0",
+ "crossterm",
"fd-lock",
"itertools 0.13.0",
"nu-ansi-term",
@@ -4421,7 +4410,7 @@ dependencies = [
"http-body 1.0.1",
"http-body-util",
"hyper 1.9.0",
- "hyper-rustls 0.27.7",
+ "hyper-rustls 0.27.9",
"hyper-tls",
"hyper-util",
"js-sys",
@@ -4431,7 +4420,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
- "rustls 0.23.37",
+ "rustls 0.23.39",
"rustls-native-certs",
"rustls-pki-types",
"serde",
@@ -4448,11 +4437,49 @@ dependencies = [
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
- "wasm-streams",
+ "wasm-streams 0.4.2",
"web-sys",
"webpki-roots",
]
+[[package]]
+name = "reqwest"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http 1.4.0",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.9.0",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams 0.5.0",
+ "web-sys",
+]
+
[[package]]
name = "reqwest-eventsource"
version = "0.6.0"
@@ -4465,7 +4492,7 @@ dependencies = [
"mime",
"nom 7.1.3",
"pin-project-lite",
- "reqwest",
+ "reqwest 0.12.28",
"thiserror 1.0.69",
]
@@ -4494,21 +4521,24 @@ dependencies = [
[[package]]
name = "rmcp"
-version = "0.16.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc4c9c94680f75470ee8083a0667988b5d7b5beb70b9f998a8e51de7c682ce60"
+checksum = "67d69668de0b0ccd9cc435f700f3b39a7861863cf37a15e1f304ea78688a4826"
dependencies = [
"async-trait",
"base64",
"chrono",
"futures",
+ "http 1.4.0",
"pastey",
"pin-project-lite",
"process-wrap",
+ "reqwest 0.13.2",
"rmcp-macros",
"schemars 1.2.1",
"serde",
"serde_json",
+ "sse-stream",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
@@ -4518,9 +4548,9 @@ dependencies = [
[[package]]
name = "rmcp-macros"
-version = "0.16.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90c23c8f26cae4da838fbc3eadfaecf2d549d97c04b558e7bd90526a9c28b42a"
+checksum = "48fdc01c81097b0aed18633e676e269fefa3a78ec1df56b4fe597c1241b92025"
dependencies = [
"darling 0.23.0",
"proc-macro2",
@@ -4548,12 +4578,12 @@ dependencies = [
[[package]]
name = "rtoolbox"
-version = "0.0.3"
+version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f"
+checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -4621,19 +4651,6 @@ dependencies = [
"semver",
]
-[[package]]
-name = "rustix"
-version = "0.38.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
-]
-
[[package]]
name = "rustix"
version = "1.1.4"
@@ -4643,7 +4660,7 @@ dependencies = [
"bitflags",
"errno",
"libc",
- "linux-raw-sys 0.12.1",
+ "linux-raw-sys",
"windows-sys 0.61.2",
]
@@ -4661,16 +4678,16 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.37"
+version = "0.23.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
+checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"ring",
"rustls-pki-types",
- "rustls-webpki 0.103.10",
+ "rustls-webpki 0.103.13",
"subtle",
"zeroize",
]
@@ -4709,9 +4726,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.103.10"
+version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
@@ -4740,6 +4757,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "scc"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
+dependencies = [
+ "sdd",
+]
+
[[package]]
name = "schannel"
version = "0.1.29"
@@ -4808,7 +4834,7 @@ dependencies = [
"cssparser",
"ego-tree",
"html5ever 0.29.1",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"precomputed-hash",
"selectors",
"tendril",
@@ -4824,6 +4850,12 @@ dependencies = [
"untrusted",
]
+[[package]]
+name = "sdd"
+version = "3.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
+
[[package]]
name = "secrecy"
version = "0.10.3"
@@ -4888,9 +4920,9 @@ dependencies = [
[[package]]
name = "semver"
-version = "1.0.27"
+version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
@@ -4949,7 +4981,7 @@ version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"itoa",
"memchr",
"serde",
@@ -4988,7 +5020,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"schemars 0.9.0",
"schemars 1.2.1",
"serde_core",
@@ -5015,13 +5047,39 @@ version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
+[[package]]
+name = "serial_test"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f"
+dependencies = [
+ "futures-executor",
+ "futures-util",
+ "log",
+ "once_cell",
+ "parking_lot",
+ "scc",
+ "serial_test_derive",
+]
+
+[[package]]
+name = "serial_test_derive"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "servo_arc"
version = "0.4.3"
@@ -5193,6 +5251,19 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "sse-stream"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c5e6deb40826033bd7b11c7ef25ef71193fabd71f680f40dd16538a2704d2f4"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http-body 1.0.1",
+ "http-body-util",
+ "pin-project-lite",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -5388,7 +5459,7 @@ dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
- "rustix 1.1.4",
+ "rustix",
"windows-sys 0.61.2",
]
@@ -5435,7 +5506,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
- "rustix 1.1.4",
+ "rustix",
"windows-sys 0.61.2",
]
@@ -5577,9 +5648,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.50.0"
+version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
+checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"bytes",
"libc",
@@ -5607,9 +5678,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.6.1"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
@@ -5642,7 +5713,7 @@ version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
- "rustls 0.23.37",
+ "rustls 0.23.39",
"tokio",
]
@@ -5748,7 +5819,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"pin-project-lite",
"slab",
"sync_wrapper",
@@ -5923,9 +5994,9 @@ dependencies = [
[[package]]
name = "typenum"
-version = "1.19.0"
+version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "typespec"
@@ -5953,8 +6024,8 @@ dependencies = [
"futures",
"getrandom 0.3.4",
"pin-project",
- "rand 0.9.2",
- "reqwest",
+ "rand 0.9.4",
+ "reqwest 0.12.28",
"serde",
"serde_json",
"time",
@@ -6089,9 +6160,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.23.0"
+version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
+checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -6188,11 +6259,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
-version = "1.0.2+wasi-0.2.9"
+version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
- "wit-bindgen",
+ "wit-bindgen 0.57.1",
]
[[package]]
@@ -6201,14 +6272,14 @@ 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",
+ "wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-bindgen"
-version = "0.2.117"
+version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
+checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [
"cfg-if",
"once_cell",
@@ -6219,9 +6290,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.67"
+version = "0.4.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
+checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -6229,9 +6300,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.117"
+version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
+checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -6239,9 +6310,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.117"
+version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
+checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -6252,9 +6323,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.117"
+version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
+checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [
"unicode-ident",
]
@@ -6276,7 +6347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"wasm-encoder",
"wasmparser",
]
@@ -6294,6 +6365,19 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
[[package]]
name = "wasmparser"
version = "0.244.0"
@@ -6302,7 +6386,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"semver",
]
@@ -6314,7 +6398,7 @@ checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
dependencies = [
"cc",
"downcast-rs",
- "rustix 1.1.4",
+ "rustix",
"smallvec",
"wayland-sys",
]
@@ -6326,7 +6410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
dependencies = [
"bitflags",
- "rustix 1.1.4",
+ "rustix",
"wayland-backend",
"wayland-scanner",
]
@@ -6378,9 +6462,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.94"
+version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
+checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -6398,9 +6482,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
-version = "1.0.6"
+version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
+checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
dependencies = [
"rustls-pki-types",
]
@@ -6801,6 +6885,12 @@ dependencies = [
"wit-bindgen-rust-macro",
]
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
@@ -6820,7 +6910,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"prettyplease",
"syn",
"wasm-metadata",
@@ -6851,7 +6941,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"log",
"serde",
"serde_derive",
@@ -6870,7 +6960,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
- "indexmap 2.13.0",
+ "indexmap 2.14.0",
"log",
"semver",
"serde",
@@ -6889,7 +6979,7 @@ dependencies = [
"libc",
"log",
"os_pipe",
- "rustix 1.1.4",
+ "rustix",
"thiserror 2.0.18",
"tree_magic_mini",
"wayland-backend",
@@ -6900,9 +6990,9 @@ dependencies = [
[[package]]
name = "writeable"
-version = "0.6.2"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "x11rb"
@@ -6911,7 +7001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
- "rustix 1.1.4",
+ "rustix",
"x11rb-protocol",
]
diff --git a/Cargo.toml b/Cargo.toml
index 5d15c52..146010b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -89,7 +89,12 @@ duct = "1.0.0"
argc = "1.23.0"
strum_macros = "0.27.2"
indoc = "2.0.6"
-rmcp = { version = "0.16.0", features = ["client", "transport-child-process"] }
+rmcp = { version = "1.5.0", features = [
+ "client",
+ "transport-child-process",
+ "transport-streamable-http-client-reqwest",
+ "reqwest-native-tls",
+] }
num_cpus = "1.17.0"
tree-sitter = "0.26.8"
tree-sitter-language = "0.1"
@@ -120,7 +125,7 @@ default-features = false
features = ["parsing", "regex-onig", "plist-load"]
[target.'cfg(target_os = "macos")'.dependencies]
-crossterm = { version = "0.28.1", features = ["use-dev-tty"] }
+crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
[target.'cfg(target_os = "linux")'.dependencies]
arboard = { version = "3.3.0", default-features = false, features = [
@@ -132,6 +137,7 @@ arboard = { version = "3.3.0", default-features = false }
[dev-dependencies]
pretty_assertions = "1.4.0"
+serial_test = "3"
[[bin]]
name = "loki"
diff --git a/README.md b/README.md
index c3d9cf5..461af13 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
# Loki: All-in-one, batteries-included LLM CLI Tool

-
[](https://crates.io/crates/loki-ai)


@@ -13,36 +12,36 @@ Agents, and More.
It is designed to include a number of useful agents, roles, macros, and more so users can get up and running with Loki
in as little time as possible.
-
+
-Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](./docs/AICHAT-MIGRATION.md) to get started.
+Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration) to get started.
## Quick Links
-* [AIChat Migration Guide](./docs/AICHAT-MIGRATION.md): Coming from AIChat? Follow the migration guide to get started.
+* [AIChat Migration Guide](https://github.com/Dark-Alex-17/loki/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
* [Installation](#install): Install Loki
* [Getting Started](#getting-started): Get started with Loki by doing first-run setup steps.
-* [REPL](./docs/REPL.md): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Loki.
- * [Custom REPL Prompt](./docs/REPL-PROMPT.md): Customize the REPL prompt to provide useful contextual information.
-* [Vault](./docs/VAULT.md): Securely store and manage sensitive information such as API keys and credentials.
-* [Shell Integrations](./docs/SHELL-INTEGRATIONS.md): Seamlessly integrate Loki with your shell environment for enhanced command-line assistance.
-* [Function Calling](./docs/function-calling/TOOLS.md#Tools): Leverage function calling capabilities to extend Loki's functionality with custom tools
- * [Creating Custom Tools](./docs/function-calling/CUSTOM-TOOLS.md): You can create your own custom tools to enhance Loki's capabilities.
- * [Create Custom Python Tools](./docs/function-calling/CUSTOM-TOOLS.md#custom-python-based-tools)
- * [Create Custom TypeScript Tools](./docs/function-calling/CUSTOM-TOOLS.md#custom-typescript-based-tools)
- * [Create Custom Bash Tools](./docs/function-calling/CUSTOM-BASH-TOOLS.md)
- * [Bash Prompt Utilities](./docs/function-calling/BASH-PROMPT-HELPERS.md)
-* [First-Class MCP Server Support](./docs/function-calling/MCP-SERVERS.md): Easily connect and interact with MCP servers for advanced functionality.
-* [Macros](./docs/MACROS.md): Automate repetitive tasks and workflows with Loki "scripts" (macros).
-* [RAG](./docs/RAG.md): Retrieval-Augmented Generation for enhanced information retrieval and generation.
-* [Sessions](/docs/SESSIONS.md): Manage and persist conversational contexts and settings across multiple interactions.
-* [Roles](./docs/ROLES.md): Customize model behavior for specific tasks or domains.
-* [Agents](/docs/AGENTS.md): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
- * [Todo System](./docs/TODO-SYSTEM.md): Built-in task tracking for improved agent reliability with smaller models.
-* [Environment Variables](./docs/ENVIRONMENT-VARIABLES.md): Override and customize your Loki configuration at runtime with environment variables.
-* [Client Configurations](./docs/clients/CLIENTS.md): Configuration instructions for various LLM providers.
- * [Authentication (API Key & OAuth)](./docs/clients/CLIENTS.md#authentication): Authenticate with API keys or OAuth for subscription-based access.
- * [Patching API Requests](./docs/clients/PATCHES.md): Learn how to patch API requests for advanced customization.
-* [Custom Themes](./docs/THEMES.md): Change the look and feel of Loki to your preferences with custom themes.
+* [REPL](https://github.com/Dark-Alex-17/loki/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Loki.
+ * [Custom REPL Prompt](https://github.com/Dark-Alex-17/loki/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
+* [Vault](https://github.com/Dark-Alex-17/loki/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
+* [Shell Integrations](https://github.com/Dark-Alex-17/loki/wiki/Shell-Integrations): Seamlessly integrate Loki with your shell environment for enhanced command-line assistance.
+* [Function Calling](https://github.com/Dark-Alex-17/loki/wiki/Tools): Leverage function calling capabilities to extend Loki's functionality with custom tools
+ * [Creating Custom Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools): You can create your own custom tools to enhance Loki's capabilities.
+ * [Create Custom Python Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools#custom-python-based-tools)
+ * [Create Custom TypeScript Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Tools#custom-typescript-based-tools)
+ * [Create Custom Bash Tools](https://github.com/Dark-Alex-17/loki/wiki/Custom-Bash-Tools)
+ * [Bash Prompt Utilities](https://github.com/Dark-Alex-17/loki/wiki/Bash-Prompt-Helpers)
+* [First-Class MCP Server Support](https://github.com/Dark-Alex-17/loki/wiki/MCP-Servers): Easily connect and interact with MCP servers for advanced functionality.
+* [Macros](https://github.com/Dark-Alex-17/loki/wiki/Macros): Automate repetitive tasks and workflows with Loki "scripts" (macros).
+* [RAG](https://github.com/Dark-Alex-17/loki/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
+* [Sessions](https://github.com/Dark-Alex-17/loki/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions.
+* [Roles](https://github.com/Dark-Alex-17/loki/wiki/Roles): Customize model behavior for specific tasks or domains.
+* [Agents](https://github.com/Dark-Alex-17/loki/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
+ * [Todo System](https://github.com/Dark-Alex-17/loki/wiki/TODO-System): Built-in task tracking for improved agent reliability with smaller models.
+* [Environment Variables](https://github.com/Dark-Alex-17/loki/wiki/Environment-Variables): Override and customize your Loki configuration at runtime with environment variables.
+* [Client Configurations](https://github.com/Dark-Alex-17/loki/wiki/Clients): Configuration instructions for various LLM providers.
+ * [Authentication (API Key & OAuth)](https://github.com/Dark-Alex-17/loki/wiki/Clients#authentication): Authenticate with API keys or OAuth for subscription-based access.
+ * [Patching API Requests](https://github.com/Dark-Alex-17/loki/wiki/Patches): Learn how to patch API requests for advanced customization.
+* [Custom Themes](https://github.com/Dark-Alex-17/loki/wiki/Themes): Change the look and feel of Loki to your preferences with custom themes.
* [History](#history): A history of how Loki came to be.
## Prerequisites
@@ -154,7 +153,7 @@ loki --list-secrets
### Authentication
Each client in your configuration needs authentication (with a few exceptions; e.g. ollama). Most clients use an API key
-(set via `api_key` in the config or through the [vault](./docs/VAULT.md)). For providers that support OAuth (e.g. Claude Pro/Max
+(set via `api_key` in the config or through the [vault](https://github.com/Dark-Alex-17/loki/wiki/Vault)). For providers that support OAuth (e.g. Claude Pro/Max
subscribers, Google Gemini), you can authenticate with your existing subscription instead:
```yaml
@@ -170,7 +169,7 @@ loki --authenticate my-claude-oauth
# Or via the REPL: .authenticate
```
-For full details, see the [authentication documentation](./docs/clients/CLIENTS.md#authentication).
+For full details, see the [authentication documentation](https://github.com/Dark-Alex-17/loki/wiki/Clients#authentication).
### Tab-Completions
You can also enable tab completions to make using Loki easier. To do so, add the following to your shell profile:
@@ -247,7 +246,7 @@ shown below:
| Setting | Description |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Loki in [REPL](./docs/REPL.md) mode.
Values can be
- `role:` to define a role
- `session:` to define a session
- `:` to define both a session and a role to use
|
+| `repl_prelude` | This setting lets you specify a default `session` or `role` to use when starting Loki in [REPL](https://github.com/Dark-Alex-17/loki/wiki/REPL) mode.
Values can be - `role:` to define a role
- `session:` to define a session
- `:` to define both a session and a role to use
|
| `cmd_prelude` | This setting lets you specify a default `session` or `role` to use when running one-off queries in Loki via the CLI.
Values can be - `role:` to define a role
- `session:` to define a session
- `:` to define both a session and a role to use
|
| `agent_session` | This setting is used to specify a default session that all agents should start into, unless otherwise specified in the agent configuration. (e.g. `temp`, `default`) |
diff --git a/assets/functions/mcp.json b/assets/functions/mcp.json
index d3794f6..0ae8f79 100644
--- a/assets/functions/mcp.json
+++ b/assets/functions/mcp.json
@@ -1,6 +1,7 @@
{
"mcpServers": {
"github": {
+ "type": "stdio",
"command": "docker",
"args": [
"run",
@@ -15,14 +16,17 @@
}
},
"atlassian": {
+ "type": "stdio",
"command": "npx",
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/mcp"]
},
"docker": {
+ "type": "stdio",
"command": "uvx",
"args": ["mcp-server-docker"]
},
"ddg-search": {
+ "type": "stdio",
"command": "uvx",
"args": ["duckduckgo-mcp-server"]
}
diff --git a/assets/functions/tools/query_jira_issues.sh b/assets/functions/tools/query_jira_issues.sh
deleted file mode 100755
index 4160be0..0000000
--- a/assets/functions/tools/query_jira_issues.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-# @meta require-tools jira
-# @describe Query for jira issues using a Jira Query Language (JQL) query
-# @option --jql-query! The Jira Query Language query to execute
-# @env LLM_OUTPUT=/dev/stdout The output path
-
-main() {
- jira issue ls -q "$argc_jql_query" --plain >> "$LLM_OUTPUT"
-}
\ No newline at end of file
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
deleted file mode 100644
index e9782d8..0000000
--- a/docs/AGENTS.md
+++ /dev/null
@@ -1,775 +0,0 @@
-# Agents
-
-Agents in Loki follow the same style as OpenAI's GPTs. They consist of 3 parts:
-
-* [Role](./ROLES.md) - Tell the LLM how to behave
-* [RAG](./RAG.md) - Pre-built knowledge bases specifically for the agent
-* [Function Calling](./function-calling/TOOLS.md#tools) ([#2](./function-calling/MCP-SERVERS.md)) - Extends the functionality of the LLM through custom functions it can call
-
-
-
-Agent configuration files are stored in the `agents` subdirectory of your Loki configuration directory. The location of
-this directory varies between systems so you can use the following command to locate yours:
-
-```shell
-loki --info | grep 'agents_dir' | awk '{print $2}'
-```
-
-If you're looking for more example agents, refer to the [built-in agents](../assets/agents).
-
-## Quick Links
-
-- [Directory Structure](#directory-structure)
-- [Metadata](#1-metadata)
-- [2. Define the Instructions](#2-define-the-instructions)
- - [Static Instructions](#static-instructions)
- - [Special Variables](#special-variables)
- - [User-Defined Variables](#user-defined-variables)
- - [Dynamic Instructions](#dynamic-instructions)
- - [Variables](#variables)
-- [3. Initializing RAG](#3-initializing-rag)
-- [4. Building Tools for Agents](#4-building-tools-for-agents)
- - [Limitations](#limitations)
- - [.env File Support](#env-file-support)
- - [Python-Based Agent Tools](#python-based-agent-tools)
- - [Bash-Based Agent Tools](#bash-based-agent-tools)
- - [TypeScript-Based Agent Tools](#typescript-based-agent-tools)
-- [5. Conversation Starters](#5-conversation-starters)
-- [6. Todo System & Auto-Continuation](#6-todo-system--auto-continuation)
-- [7. Sub-Agent Spawning System](#7-sub-agent-spawning-system)
- - [Configuration](#spawning-configuration)
- - [Spawning & Collecting Agents](#spawning--collecting-agents)
- - [Task Queue with Dependencies](#task-queue-with-dependencies)
- - [Active Task Dispatch](#active-task-dispatch)
- - [Output Summarization](#output-summarization)
- - [Teammate Messaging](#teammate-messaging)
- - [Runaway Safeguards](#runaway-safeguards)
-- [8. User Interaction Tools](#8-user-interaction-tools)
- - [Available Tools](#user-interaction-available-tools)
- - [Escalation (Sub-Agent to User)](#escalation-sub-agent-to-user)
-- [9. Auto-Injected Prompts](#9-auto-injected-prompts)
-- [Built-In Agents](#built-in-agents)
-
-
----
-
-## Directory Structure
-Agent configurations often have the following directory structure:
-
-```
-/agents
- └── my-agent
- ├── config.yaml
- ├── tools.sh
- or
- ├── tools.py
- or
- ├── tools.ts
-```
-
-This means that agent configurations often are only two files: the agent configuration file (`config.yaml`), and the
-tool definitions (`agents/my-agent/tools.sh`, `tools.py`, or `tools.ts`).
-
-To see a full example configuration file, refer to the [example agent config file](../config.agent.example.yaml).
-
-The best way to understand how an agent is built is to go step by step in the following manner:
-
----
-
-## 1. Metadata
-Agent configurations have the following settings available to customize each agent:
-
-```yaml
-# Model Configuration
-model: openai:gpt-4o # Specify the LLM to use
-temperature: null # Set default temperature parameter, range (0, 1)
-top_p: null # Set default top-p parameter, with a range of (0, 1) or (0, 2), depending on the model
-# Agent Metadata Configuration
-agent_session: null # Set a session to use when starting the agent. (e.g. temp, default); defaults to globally set agent_session
-# Agent Configuration
-name: # Name of the agent, used in the UI and logs
-description: # Description of the agent, used in the UI
-version: 1 # Version of the agent
-# Function Calling Configuration
-mcp_servers: # Optional list of MCP servers that the agent utilizes
- - github # Corresponds to the name of an MCP server in the `/functions/mcp.json` file
-global_tools: # Optional list of additional global tools to enable for the agent; i.e. not tools specific to the agent
- - web_search
- - fs
- - python
-# Todo System & Auto-Continuation (see "Todo System & Auto-Continuation" section below)
-auto_continue: false # Enable automatic continuation when incomplete todos remain
-max_auto_continues: 10 # Maximum continuation attempts before stopping
-inject_todo_instructions: true # Inject todo tool instructions into system prompt
-continuation_prompt: null # Custom prompt for continuations (optional)
-# Sub-Agent Spawning (see "Sub-Agent Spawning System" section below)
-can_spawn_agents: false # Enable spawning child agents
-max_concurrent_agents: 4 # Max simultaneous child agents
-max_agent_depth: 3 # Max nesting depth (prevents runaway)
-inject_spawn_instructions: true # Inject spawning instructions into system prompt
-summarization_model: null # Model for summarizing sub-agent output (e.g. 'openai:gpt-4o-mini')
-summarization_threshold: 4000 # Char count above which sub-agent output is summarized
-escalation_timeout: 300 # Seconds sub-agents wait for escalated user input (default: 5 min)
-```
-
-As mentioned previously: Agents utilize function calling to extend a model's capabilities. However, agents operate in
-isolated environment, so in order for an agent to use a tool or MCP server that you have defined globally, you must
-explicitly state which tools and/or MCP servers the agent uses. Otherwise, it is assumed that the agent doesn't use any
-tools outside its own custom defined tools.
-
-And if you don't define a `agents/my-agent/tools.sh`, `agents/my-agent/tools.py`, or `agents/my-agent/tools.ts`, then the agent is really just a
-`role`.
-
-You'll notice there are no settings for agent-specific tooling. This is because they are handled separately and
-automatically. See the [Building Tools for Agents](#4-building-tools-for-agents) section below for more information.
-
-To see a full example configuration file, refer to the [example agent config file](../config.agent.example.yaml).
-
-## 2. Define the Instructions
-At their heart, agents function similarly to roles in that they tell the model how to behave. Agent configuration files
-have the following settings for the instruction definitions:
-
-```yaml
-dynamic_instructions: # Whether to use dynamically generated instructions for the agent; if false, static instructions are used. False by default.
-instructions: # Static instructions for the LLM; These are ignored if dynamic instructions are used
-variables: # An array of optional variables that the agent expects and uses
-```
-
-### Static Instructions
-By default, Loki agents use statically defined instructions. Think of them as being identical to the instructions for a
-[role](./ROLES.md#instructions), because they virtually are.
-
-**Example:**
-```yaml
-instructions: |
- You are an AI agent designed to demonstrate agentic capabilities
-```
-
-Just like roles, agents support variable interpolation at runtime. There's two types of variables that can be
-interpolated into the instructions at runtime: special variables (like roles have), and user-defined variables. Just
-like roles, variables are interpolated into your instructions anywhere Loki sees the `{{variable}}` syntax.
-
-#### Special Variables
-The following special variables are provided by Loki at runtime and can be injected into your agent's instructions:
-
-| Name | Description | Example |
-|-----------------|---------------------------------------------------------------------|----------------------------|
-| `__os__` | Operating system name | `linux` |
-| `__os_family__` | Operating system family | `unix` |
-| `__arch__` | System architecture | `x86_64` |
-| `__shell__` | The current user's default shell | `bash` |
-| `__locale__` | The current user's preferred language and region settings | `en-US` |
-| `__now__` | Current timestamp in ISO 8601 format | `2025-11-07T10:15:44.268Z` |
-| `__cwd__` | The current working directory | `/tmp` |
-| `__tools__` | A list of the enabled tools (global + mcp servers + agent-specific) | |
-
-#### User-Defined Variables
-Agents also support user-defined variables that can be interpolated into the instructions, and are made available to any
-agent-specific tools you define (see [Building Tools for Agents](#4-building-tools-for-agents) for more details on how to
-create agent-specific tooling).
-
-The `variables` setting in an agent's config has the following fields:
-
-| Field | Required | Description |
-|---------------|----------|----------------------------------------------------------------------------------------------------|
-| `name` | * | The name of the variable |
-| `description` | * | The description of the field |
-| `default` | | A default value for the field. If left undefined, the user will be prompted for a value at runtime |
-
-These variables can be referenced in both the agent's instructions, and in the tool definitions via `LLM_AGENT_VAR_`.
-
-**Example:**
-```yaml
-instructions: |
- You are an agent who answers questions about a user's system.
-
-
- {{__tools__}}
-
-
-
- os: {{__os__}}
- os_family: {{__os_family__}}
- arch: {{__arch__}}
- shell: {{__shell__}}
- locale: {{__locale__}}
- now: {{__now__}}
- cwd: {{__cwd__}}
-
-
-
- username: {{username}}
-
-variables:
- - name: username # Accessible from the tool definitions via the `LLM_AGENT_VAR_USERNAME` environment variable
- description: Your user name
-```
-
-### Dynamic Instructions
-Sometimes you may find it useful to dynamically generate instructions on startup. Whether that be via a call to Loki
-itself to generate them, or by some other means. Loki supports this type of behavior using a special function defined
-in your `agents/my-agent/tools.py`, `agents/my-agent/tools.sh`, or `agents/my-agent/tools.ts`.
-
-**Example: Instructions for a JSON-reader agent that specializes on each JSON input it receives**
-`agents/json-reader/tools.py`:
-```python
-import json
-from pathlib import Path
-from genson import SchemaBuilder
-
-def _instructions():
- """Generates instructions for the agent dynamically"""
- value = input("Enter a JSON file path OR paste raw JSON: ").strip()
- if not value:
- raise SystemExit("A file path or JSON string is required.")
-
- p = Path(value)
- if p.exists() and p.is_file():
- json_file_path = str(p.resolve())
- json_text = p.read_text(encoding="utf-8")
- else:
- try:
- json.loads(value)
- except json.JSONDecodeError as e:
- raise SystemExit(f"Input is neither a file nor valid JSON.\n{e}")
- json_file_path = ""
- json_text = value
-
- try:
- data = json.loads(json_text)
- except json.JSONDecodeError as e:
- raise SystemExit(f"Provided content is not valid JSON.\n{e}")
-
- builder = SchemaBuilder()
- builder.add_object(data)
- json_schema = builder.to_schema()
- return f"""
- You are an AI agent that can view and filter JSON data with jq.
-
- ## Context
- json_file_path: {json_file_path}
- json_schema: {json.dumps(json_schema, indent=2)}
- """
-```
-
-or
-
-`agents/json-reader/tools.sh`:
-```bash
-#!/usr/bin/env bash
-set -e
-
-# @meta require-tools jq,genson
-# @env LLM_OUTPUT=/dev/stdout The output path
-
-# @cmd Generates instructions for the agent dynamically
-_instructions() {
- read -r -p "Enter a JSON file path OR paste raw JSON: " value
-
- if [[ -z "${value}" ]]; then
- echo "A file path or JSON string is required" >&2
- exit 1
- fi
- json_file_path=""
- inline_temp=""
- cleanup() {
- [[ -n "${inline_temp:-}" && -f "${inline_temp}" ]] && rm -f "${inline_temp}"
- }
- trap cleanup EXIT
-
- if [[ -f "${value}" ]]; then
- json_file_path="$(realpath "${value}")"
- if ! jq empty "${json_file_path}" >/dev/null 2>&1; then
- echo "Error: File does not contain valid JSON: ${json_file_path}" >&2
- exit 1
- fi
- else
- inline_temp="$(mktemp)"
- printf "%s" "${value}" > "${inline_temp}"
- if ! jq empty "${inline_temp}" >/dev/null 2>&1; then
- echo "Error: Input is neither a file nor valid JSON." >&2
- exit 1
- fi
- json_file_path=""
- fi
-
- source_file="${json_file_path}"
- if [[ "${json_file_path}" == "" ]]; then
- source_file="${inline_temp}"
- fi
-
- json_schema="$(genson < "${source_file}" | jq -c '.')"
- cat <> "$LLM_OUTPUT"
-You are an AI agent that can view and filter JSON data with jq.
-
-## Context
-json_file_path: ${json_file_path}
-json_schema: ${json_schema}
-EOF
-}
-```
-
-For more information on how to create custom tools for your agent and the structure of the `agent/my-agent/tools.sh`,
-`agent/my-agent/tools.py`, or `agent/my-agent/tools.ts` files, refer to the [Building Tools for Agents](#4-building-tools-for-agents) section below.
-
-#### Variables
-All the same variable interpolations supported by static instructions is also supported by dynamic instructions. For
-more information on what variables are available and how to use them, refer to the [Special Variables](#special-variables)
-and [User-Defined Variables](#user-defined-variables) sections above.
-
-## 3. Initializing RAG
-Each agent you create also has a dedicated knowledge base that adds additional context to your queries and helps the LLM
-answer queries effectively. The documents to load into RAG are defined in the `documents` array of your agent
-configuration file:
-
-```yaml
-documents:
- - https://www.ohdsi.org/data-standardization/
- - https://github.com/OHDSI/Vocabulary-v5.0/wiki/**
- - OMOPCDM_ddl.sql # Relative path to agent (i.e. file lives at '/agents/my-agent/OMOPCDM_ddl.sql')
-```
-
-These documents use the same syntax as those you'd define when constructing RAG normally. To see all the available types
-of documents that Loki supports and how to use custom document loaders, refer to the [RAG documentation](./RAG.md#supported-document-sources).
-
-Anytime your agent starts up, it will automatically be using the RAG you've defined here.
-
-## 4. Building Tools for Agents
-Building tools for agents is virtually identical to building custom tools, with one slight difference: instead of
-defining a single function that gets executed at runtime (e.g. `main` for bash tools and `run` for Python tools), agent
-tools define a number of *subcommands*.
-
-### Limitations
-You can only utilize one of: a bash-based `/agents/my-agent/tools.sh`, a Python-based
-`/agents/my-agent/tools.py`, or a TypeScript-based `/agents/my-agent/tools.ts`.
-However, if it's easier to achieve a task in one language vs the other,
-you're free to define other scripts in your agent's configuration directory and reference them from the main
-tools file. **Any scripts *not* named `tools.{py,sh,ts}` will not be picked up by Loki's compiler**, meaning they
-can be used like any other set of scripts.
-
-It's important to keep in mind the following:
-
-* **Do not give agents the same name as an executable**. Loki compiles the tools for each agent into a binary that it
- temporarily places on your path during execution. If you have a binary with the same name as your agent, then your
- shell may execute the existing binary instead of your agent's tools
-* **`LLM_ROOT_DIR` points to the agent's configuration directory**. This is where agents differ slightly from normal
- tools: The `LLM_ROOT_DIR` environment variable does *not* point to the `functions/tools` directory like it does in
- global tools. Instead, it points to the agent's configuration directory, making it easier to source scripts and other
- miscellaneous files
-
-### .env File Support
-When Loki loads an agent, it will also search the agent's configuration directory for a `.env` file. If found, all
-environment variables defined in the file will be made available to the agent's tools.
-
-### Python-Based Agent Tools
-Python-based tools are defined exactly the same as they are for custom tool definitions. The only difference is that
-instead of a single `run` function, you define as many as you like with whatever arguments you like.
-
-**Example:**
-`agents/my-agent/tools.py`
-```python
-import urllib.request
-
-def get_ip_info():
- """
- Get your IP information
- """
- with urllib.request.urlopen("https://httpbin.org/ip") as response:
- data = response.read()
- return data.decode('utf-8')
-
-def get_ip_address_from_aws():
- """
- Find your public IP address using AWS
- """
- with urllib.request.urlopen("https://checkip.amazonaws.com") as response:
- data = response.read()
- return data.decode('utf-8')
-```
-
-Loki automatically compiles these as separate functions for the LLM to call. No extra work is needed. Just make sure you
-follow all the same steps to define each function as you would when creating custom Python tools.
-
-For more information on how to build tools in Python, refer to the [custom Python tools documentation](./function-calling/CUSTOM-TOOLS.md#custom-python-based-tools)
-
-### Bash-Based Agent Tools
-Bash-based agent tools are virtually identical to custom bash tools, with only one difference. Instead of defining a
-single entrypoint via the `main` function, you actually define as many subcommands as you like.
-
-**Example:**
-`agents/my-agent/tools.sh`
-```bash
-#!/usr/bin/env bash
-
-# @env LLM_OUTPUT=/dev/stdout The output path
-# @describe Discover network information about your computer and its place in the internet
-
-# Use the `@cmd` annotation to define subcommands for your script.
-# @cmd Get your IP information
-get_ip_info() {
- curl -fsSL https://httpbin.org/ip >> "$LLM_OUTPUT"
-}
-
-# @cmd Find your public IP address using AWS
-get_ip_address_from_aws() {
- curl -fsSL https://checkip.amazonaws.com >> "$LLM_OUTPUT"
-}
-```
-To compile the script so it's executable and testable:
-```bash
-$ loki --build-tools
-```
-
-Then you can execute your script (assuming your current working directory is `agents/my-agent`):
-```bash
-$ ./tools.sh get_ip_info
-$ ./tools.sh get_ip_address_from_aws
-```
-
-All other special annotations (`@env`, `@arg`, `@option` `@flags`) apply to subcommands as well, so be sure to follow
-the same syntax ad formatting as is used to create custom bash tools globally.
-
-For more information on how to write, [build and test](function-calling/CUSTOM-BASH-TOOLS.md#execute-and-test-your-bash-tools) tools in bash, refer to the
-[custom bash tools documentation](function-calling/CUSTOM-BASH-TOOLS.md).
-
-### TypeScript-Based Agent Tools
-TypeScript-based agent tools work exactly the same as TypeScript global tools. Instead of a single `run` function,
-you define as many exported functions as you like. Non-exported functions are private helpers and are invisible to the
-LLM.
-
-**Example:**
-`agents/my-agent/tools.ts`
-```typescript
-/**
- * Get your IP information
- */
-export async function get_ip_info(): Promise {
- const resp = await fetch("https://httpbin.org/ip");
- return await resp.text();
-}
-
-/**
- * Find your public IP address using AWS
- */
-export async function get_ip_address_from_aws(): Promise {
- const resp = await fetch("https://checkip.amazonaws.com");
- return await resp.text();
-}
-
-// Non-exported helper — invisible to the LLM
-function formatResponse(data: string): string {
- return data.trim();
-}
-```
-
-Loki automatically compiles each exported function as a separate tool for the LLM to call. Just make sure you
-follow the same JSDoc and parameter conventions as you would when creating custom TypeScript tools.
-
-TypeScript agent tools also support dynamic instructions via an exported `_instructions()` function:
-
-```typescript
-import { readFileSync } from "fs";
-
-/**
- * Generates instructions for the agent dynamically
- */
-export function _instructions(): string {
- const schema = readFileSync("schema.json", "utf-8");
- return `You are an AI agent that works with the following schema:\n${schema}`;
-}
-```
-
-For more information on how to build tools in TypeScript, refer to the [custom TypeScript tools documentation](function-calling/CUSTOM-TOOLS.md#custom-typescript-based-tools).
-
-## 5. Conversation Starters
-It's often helpful to also have some conversation starters so users know what kinds of things the agent is capable of
-doing. These are available in the REPL via the `.starter` command and are selectable.
-
-They are defined using the `conversation_starters` setting in your agent's configuration file:
-
-**Example:**
-`agents/my-agent/config.yaml`:
-```yaml
-conversation_starters:
- - What is my username?
- - What is my current shell?
- - What is my ip?
- - How much disk space is left on my PC??
- - How to create an agent?
-```
-
-
-
-## 6. Todo System & Auto-Continuation
-
-Loki includes a built-in task tracking system designed to improve the reliability of agents, especially when using
-smaller language models. The Todo System helps models:
-
-- Break complex tasks into manageable steps
-- Track progress through multi-step workflows
-- Automatically continue work until all tasks are complete
-
-### Quick Configuration
-
-```yaml
-# agents/my-agent/config.yaml
-auto_continue: true # Enable auto-continuation
-max_auto_continues: 10 # Max continuation attempts
-inject_todo_instructions: true # Include the default todo instructions into prompt
-```
-
-### How It Works
-
-1. When `inject_todo_instructions` is enabled, agents receive instructions on using five built-in tools:
- - `todo__init`: Initialize a todo list with a goal
- - `todo__add`: Add a task to the list
- - `todo__done`: Mark a task complete
- - `todo__list`: View current todo state
- - `todo__clear`: Clear the entire todo list and reset the goal
-
- These instructions are a reasonable default that detail how to use Loki's To-Do System. If you wish,
- you can disable the injection of the default instructions and specify your own instructions for how
- to use the To-Do System into your main `instructions` for the agent.
-
-2. When `auto_continue` is enabled and the model stops with incomplete tasks, Loki automatically sends a
- continuation prompt with the current todo state, nudging the model to continue working.
-
-3. This continues until all tasks are done or `max_auto_continues` is reached.
-
-### When to Use
-
-- Multistep tasks where the model might lose track
-- Smaller models that need more structure
-- Workflows requiring guaranteed completion of all steps
-
-For complete documentation including all configuration options, tool details, and best practices, see the
-[Todo System Guide](./TODO-SYSTEM.md).
-
-## 7. Sub-Agent Spawning System
-
-Loki agents can spawn and manage child agents that run **in parallel** as background tasks inside the same process.
-This enables orchestrator-style agents that delegate specialized work to other agents, similar to how tools like
-Claude Code or OpenCode handle complex multi-step tasks.
-
-For a working example of an orchestrator agent that uses sub-agent spawning, see the built-in
-[sisyphus](../assets/agents/sisyphus) agent. For an example of the teammate messaging pattern with parallel sub-agents,
-see the [code-reviewer](../assets/agents/code-reviewer) agent.
-
-### Spawning Configuration
-
-| Setting | Type | Default | Description |
-|-----------------------------|---------|---------------|--------------------------------------------------------------------------------|
-| `can_spawn_agents` | boolean | `false` | Enable this agent to spawn child agents |
-| `max_concurrent_agents` | integer | `4` | Maximum number of child agents that can run simultaneously |
-| `max_agent_depth` | integer | `3` | Maximum nesting depth for sub-agents (prevents runaway spawning chains) |
-| `inject_spawn_instructions` | boolean | `true` | Inject the default spawning instructions into the agent's system prompt |
-| `summarization_model` | string | current model | Model to use for summarizing long sub-agent output (e.g. `openai:gpt-4o-mini`) |
-| `summarization_threshold` | integer | `4000` | Character count above which sub-agent output is summarized before returning |
-| `escalation_timeout` | integer | `300` | Seconds a sub-agent waits for an escalated user interaction response |
-
-**Example configuration:**
-```yaml
-# agents/my-orchestrator/config.yaml
-can_spawn_agents: true
-max_concurrent_agents: 6
-max_agent_depth: 2
-inject_spawn_instructions: true
-summarization_model: openai:gpt-4o-mini
-summarization_threshold: 3000
-escalation_timeout: 600
-```
-
-### Spawning & Collecting Agents
-
-When `can_spawn_agents` is enabled, the agent receives tools for spawning and managing child agents:
-
-| Tool | Description |
-|------------------|-------------------------------------------------------------------------|
-| `agent__spawn` | Spawn a child agent in the background. Returns an agent ID immediately. |
-| `agent__check` | Non-blocking check: is the agent done? Returns `PENDING` or the result. |
-| `agent__collect` | Blocking wait: wait for an agent to finish, return its output. |
-| `agent__list` | List all spawned agents and their status. |
-| `agent__cancel` | Cancel a running agent by ID. |
-
-The core pattern is **Spawn -> Continue -> Collect**:
-
-```
-# 1. Spawn agents in parallel (returns IDs immediately)
-agent__spawn --agent explore --prompt "Find auth middleware patterns in src/"
-agent__spawn --agent explore --prompt "Find error handling patterns in src/"
-
-# 2. Continue your own work while they run
-
-# 3. Check if done (non-blocking)
-agent__check --id agent_explore_a1b2c3d4
-
-# 4. Collect results when ready (blocking)
-agent__collect --id agent_explore_a1b2c3d4
-agent__collect --id agent_explore_e5f6g7h8
-```
-
-Any agent defined in your `/agents/` directory can be spawned as a child. Child agents:
-- Run in a fully isolated environment (separate session, config, and tools)
-- Have their output suppressed from the terminal (no spinner, no tool call logging)
-- Return their accumulated output to the parent when collected
-
-### Task Queue with Dependencies
-
-For complex workflows where tasks have ordering requirements, the spawning system includes a dependency-aware
-task queue:
-
-| Tool | Description |
-|------------------------|-----------------------------------------------------------------------------|
-| `agent__task_create` | Create a task with optional dependencies and auto-dispatch agent. |
-| `agent__task_list` | List all tasks with their status, dependencies, and assignments. |
-| `agent__task_complete` | Mark a task done. Returns newly unblocked tasks and auto-dispatches agents. |
-| `agent__task_fail` | Mark a task as failed. Dependents remain blocked. |
-
-```
-# Create tasks with dependency ordering
-agent__task_create --subject "Explore existing patterns"
-agent__task_create --subject "Implement feature" --blocked_by ["task_1"]
-agent__task_create --subject "Write tests" --blocked_by ["task_2"]
-
-# Mark tasks complete to unblock dependents
-agent__task_complete --task_id task_1
-```
-
-### Active Task Dispatch
-
-Tasks can optionally specify an agent to auto-spawn when the task becomes runnable:
-
-```
-agent__task_create \
- --subject "Implement the auth module" \
- --blocked_by ["task_1"] \
- --agent coder \
- --prompt "Implement auth module based on patterns found in task_1"
-```
-
-When `task_1` completes and the dependent task becomes unblocked, an agent is automatically spawned with the
-specified prompt. No manual intervention needed. This enables fully automated multi-step pipelines.
-
-### Output Summarization
-
-When a child agent produces long output, it can be automatically summarized before returning to the parent.
-This keeps parent context windows manageable.
-
-- If the output exceeds `summarization_threshold` characters (default: 4000), it is sent through an LLM
- summarization pass
-- The `summarization_model` setting lets you use a cheaper/faster model for summarization (e.g. `gpt-4o-mini`)
-- If `summarization_model` is not set, the parent's current model is used
-- The summarization preserves all actionable information: code snippets, file paths, error messages, and
- concrete recommendations
-
-### Teammate Messaging
-
-All agents (including children) automatically receive tools for **direct sibling-to-sibling messaging**:
-
-| Tool | Description |
-|-----------------------|-----------------------------------------------------|
-| `agent__send_message` | Send a text message to another agent's inbox by ID. |
-| `agent__check_inbox` | Drain all pending messages from your inbox. |
-
-This enables coordination patterns where child agents share cross-cutting findings:
-
-```
-# Agent A discovers something relevant to Agent B
-agent__send_message --id agent_reviewer_b1c2d3e4 --message "Found a security issue in auth.rs line 42"
-
-# Agent B checks inbox before finalizing
-agent__check_inbox
-```
-
-Messages are routed through the parent's supervisor. A parent can message its children, and children can message
-their siblings. For a working example of the teammate pattern, see the built-in
-[code-reviewer](../assets/agents/code-reviewer) agent, which spawns file-specific reviewers that share
-cross-cutting findings with each other.
-
-### Runaway Safeguards
-
-The spawning system includes built-in safeguards to prevent runaway agent chains:
-
-- **`max_concurrent_agents`:** Caps how many agents can run at once (default: 4). Spawn attempts beyond this
- limit return an error asking the agent to wait or cancel existing agents.
-- **`max_agent_depth`:** Caps nesting depth (default: 3). A child agent spawning its own child increments the
- depth counter. Attempts beyond the limit are rejected.
-- **`can_spawn_agents`:** Only agents with this flag set to `true` can spawn children. By default, spawning is
- disabled. This means child agents cannot spawn their own children unless you explicitly create them with
- `can_spawn_agents: true` in their config.
-
-## 8. User Interaction Tools
-
-Loki includes built-in tools for agents (and the REPL) to interactively prompt the user for input. These tools
-are **always available**. No configuration needed. They are automatically injected into every agent and into
-REPL mode when function calling is enabled.
-
-### User Interaction Available Tools
-
-| Tool | Description | Returns |
-|------------------|-----------------------------------------|----------------------------------|
-| `user__ask` | Present a single-select list of options | The selected option string |
-| `user__confirm` | Ask a yes/no question | `"yes"` or `"no"` |
-| `user__input` | Request free-form text input | The text entered by the user |
-| `user__checkbox` | Present a multi-select checkbox list | Array of selected option strings |
-
-**Parameters:**
-
-- `user__ask`: `--question "..." --options ["Option A", "Option B", "Option C"]`
-- `user__confirm`: `--question "..."`
-- `user__input`: `--question "..."`
-- `user__checkbox`: `--question "..." --options ["Option A", "Option B", "Option C"]`
-
-At the top level (depth 0), these tools render interactive terminal prompts directly using arrow-key navigation,
-checkboxes, and text input fields.
-
-### Escalation (Sub-Agent to User)
-
-When a **child agent** (depth > 0) calls a `user__*` tool, it cannot prompt the terminal directly. Instead,
-the request is **automatically escalated** to the root agent:
-
-1. The child agent calls `user__ask(...)` and **blocks**, waiting for a reply
-2. The root agent sees a `pending_escalations` notification in its next tool results
-3. The root agent either answers from context or prompts the user itself, then calls
- `agent__reply_escalation` to unblock the child
-4. The child receives the reply and continues
-
-The escalation timeout is configurable via `escalation_timeout` in the agent's `config.yaml` (default: 300
-seconds / 5 minutes). If the timeout expires, the child receives a fallback message asking it to use its
-best judgment.
-
-| Tool | Description |
-|---------------------------|--------------------------------------------------------------------------|
-| `agent__reply_escalation` | Reply to a pending child escalation, unblocking the waiting child agent. |
-
-This tool is automatically available to any agent with `can_spawn_agents: true`.
-
-## 9. Auto-Injected Prompts
-
-Loki automatically appends usage instructions to your agent's system prompt for each enabled built-in system.
-These instructions are injected into both **static and dynamic instructions** after your own instructions,
-ensuring agents always know how to use their available tools.
-
-| System | Injected When | Toggle |
-|--------------------|----------------------------------------------------------------|-----------------------------|
-| Todo tools | `auto_continue: true` AND `inject_todo_instructions: true` | `inject_todo_instructions` |
-| Spawning tools | `can_spawn_agents: true` AND `inject_spawn_instructions: true` | `inject_spawn_instructions` |
-| Teammate messaging | Always (all agents) | None (always injected) |
-| User interaction | Always (all agents) | None (always injected) |
-
-If you prefer to write your own instructions for a system, set the corresponding `inject_*` flag to `false`
-and include your custom instructions in the agent's `instructions` field. The built-in tools will still be
-available; only the auto-injected prompt text is suppressed.
-
-## Built-In Agents
-Loki comes packaged with some useful built-in agents:
-
-* `coder`: An agent to assist you with all your coding tasks
-* `code-reviewer`: A [CodeRabbit](https://coderabbit.ai)-style code reviewer that spawns per-file reviewers using the teammate messaging pattern
-* `demo`: An example agent to use for reference when learning to create your own agents
-* `explore`: An agent designed to help you explore and understand your codebase
-* `file-reviewer`: An agent designed to perform code-review on a single file (used by the `code-reviewer` agent)
-* `jira-helper`: An agent that assists you with all your Jira-related tasks
-* `oracle`: An agent for high-level architecture, design decisions, and complex debugging
-* `sisyphus`: A powerhouse orchestrator agent for writing complex code and acting as a natural language interface for your codebase (similar to ClaudeCode, Gemini CLI, Codex, or OpenCode). Uses sub-agent spawning to delegate to `explore`, `coder`, and `oracle`.
-* `sql`: A universal SQL agent that enables you to talk to any relational database in natural language
diff --git a/docs/AICHAT-MIGRATION.md b/docs/AICHAT-MIGRATION.md
deleted file mode 100644
index f48050f..0000000
--- a/docs/AICHAT-MIGRATION.md
+++ /dev/null
@@ -1,211 +0,0 @@
-# AIChat to Loki Migration Guide
-Loki originally started as a fork of AIChat but has since evolved into its own separate project with separate goals.
-
-As a result, there's some changes you'll need to make to your AIChat configuration to be able to use Loki.
-
-Be sure you've run `loki` at least once so that the Loki configuration directory and subdirectories exist and is
-populated with the built-in defaults.
-
-## Global Configuration File
-You should be able to copy/paste your AIChat configuration file into your Loki configuration directory. Since the
-location of the Loki configuration directory varies between systems, you can use the following command to locate your
-config directory:
-
-```shell
-loki --info | grep 'config_dir' | awk '{print $2}'
-```
-
-Then, you'll need to make the following changes:
-
-* `function_calling` -> `function_calling_support`
-* `use_tools` -> `enabled_tools`
-* `agent_prelude` -> `agent_session`
-* `compress_threshold` -> `compression_threshold`
-* `summarize_prompt` -> `summarization_prompt`
-* `summary_prompt` -> `summary_context_prompt`
-
-## Roles
-Locate your `roles` directory using the following command:
-
-```shell
-loki --info | grep 'roles_dir' | awk '{print $2}'
-```
-
-Update any roles that have `use_tools` to `enabled_tools`.
-
-## Sessions
-Locate your `sessions` directory using the following command:
-
-```shell
-loki --info | grep 'sessions_dir' | awk '{print $2}'
-```
-
-Update the following settings:
-* `use_tools` -> `enabled_tools`
-* `compress_threshold` -> `compression_threshold`
-* `summarize_prompt` -> `summarization_prompt`
-* `summary_prompt` -> `summary_context_prompt`
-
----
-
-# LLM Functions Changes
-Probably the most significant difference between AIChat and Loki is how tools are handled. So if you cloned the
-[llm-functions](https://github.com/sigoden/llm-functions) repo, you'll need to make the following changes.
-
-**Note: JavaScript functions are not supported in Loki.**
-
-The following guide assumes you're using the `llm-functions` repository as your base for custom functions, and thus
-follows that directory structure.
-
-## Agents
-Agents are now all handled in one place: the `agents` directory (`/agents`):
-
-```shell
-loki --info | grep 'agents_dir' | awk '{print $2}'
-```
-
-And instead of separate `index.yaml` and `config.yaml` files, they're now both in a single `config.yaml` file.
-
-So now for all of your agents, copy all the contents of those directories to the corresponding directory in the Loki
-`agents` directory. Then make the following changes:
-
-* Copy the contents of your `/functions/agents` directory into `/agents//tools.txt`
-* No `/agents//index.yaml`
-
-## Functions
-Loki consolidates much of the `llm-functions` repo functionality into one binary. So this means
-
-* There's no need to have `argc` installed anymore
-* No separate repository to manage
-* No `tools.txt`
-* No `functions.json`
-* No `functions/mcp` directory at all
-* No `functions/scripts`
-
-Here's how to migrate your functions over to Loki from the `llm-functions` repository.
-
-* Copy your AIChat `/functions` directory into your Loki config directory
-* Delete the following files and directories from your `/functions` directory:
- * `scripts/`
- * `agents.txt`
- * `functions.json`
- * `Argcfile.sh`
- * `README.md` (irrelevant now)
- * `LICENSE` (irrelevant now)
- * `utils/guard_operation.sh`
- * `utils/guard_path.sh`
- * `utils/patch.awk`
-* Everything in `tools.txt` now lives in the global config file under the `visible_tools` setting:
- ```text
- get_current_weather.sh
- execute_command.sh
- web_search.sh
- #execute_py_code.py
- query_jira_issues.sh
- ```
- becomes the following in your `/config.yaml`
- ```yaml
- visible_tools:
- - get_current_weather.sh
- - execute_command.sh
- - web_search.sh
- # - web_search.sh
- - query_jira_issues.sh
- ```
-* If you've defined a `functions/mcp.json` file, you can leave it alone.
-* Similarly to agents, if you have any bash `tools.sh` that depend on the utility scripts in the `llm-functions`
- repository, they've been replaced by built-in utility scripts. So use the following to replace any matching lines in
- your `tools.sh` files:
- ```bash
- ##################
- ## Scripts file ##
- ##################
- ROOT_DIR="${LLM_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
- # replace with
- source "$LLM_PROMPT_UTILS_FILE"
-
- #######################
- ## guard_path script ##
- #######################
- "$ROOT_DIR/utils/guard_path.sh"
- # replace with
- guard_path
-
- ############################
- ## guard_operation script ##
- ############################
- "$ROOT_DIR/utils/guard_operation.sh"
- # replace with
- guard_operation
-
- ######################
- ## patch.awk script ##
- ######################
- awk -f "$ROOT_DIR/utils/patch.awk"
- # replace with
- patch_file
- ```
-
-Refer to the [custom bash tools docs](./function-calling/CUSTOM-BASH-TOOLS.md) to learn how to compile and test bash
-tools in Loki without needing to use `argc`.
diff --git a/docs/ENVIRONMENT-VARIABLES.md b/docs/ENVIRONMENT-VARIABLES.md
deleted file mode 100644
index a9a0a3f..0000000
--- a/docs/ENVIRONMENT-VARIABLES.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Environment Variables
-
-Loki is designed to be highly dynamic and customizable. As a result, Loki utilizes a number of environment variables
-that can be used to modify its behavior at runtime without needing to modify the existing configuration files.
-
-Loki also supports defining environment variables via a `.env` file in the Loki configuration directory. This directory
-varies between systems, so you can find the location of your configuration directory using the following command:
-
-```shell
-loki --info | grep 'config_dir' | awk '{print $2}'
-```
-
-## Quick Links
-
-- [Global Configuration Related Variables](#global-configuration-related-variables)
-- [Client Related Variables](#client-related-variables)
-- [Files and Directory Related Variables](#files-and-directory-related-variables)
-- [Agent Related Variables](#agent-related-variables)
-- [Logging Related Variables](#logging-related-variables)
-- [Miscellaneous Variables](#miscellaneous-variables)
-
-
----
-
-## Global Configuration Related Variables
-All configuration items in the global config file have environment variables that can be overridden at runtime. To see
-all configuration options and more thorough descriptions, refer to the [example config file](../config.example.yaml).
-
-Below are the most commonly used configuration settings and their corresponding environment variables:
-
-| Setting | Environment Variable |
-|----------------------------|---------------------------------|
-| `model` | `LOKI_MODEL` |
-| `temperature` | `LOKI_TEMPERATURE` |
-| `top_p` | `LOKI_TOP_P` |
-| `stream` | `LOKI_STREAM` |
-| `save` | `LOKI_SAVE` |
-| `editor` | `LOKI_EDITOR` |
-| `wrap` | `LOKI_WRAP` |
-| `wrap_code` | `LOKI_WRAP_CODE` |
-| `save_session` | `LOKI_SAVE_SESSION` |
-| `compression_threshold` | `LOKI_COMPRESSION_THRESHOLD` |
-| `function_calling_support` | `LOKI_FUNCTION_CALLING_SUPPORT` |
-| `enabled_tools` | `LOKI_ENABLED_TOOLS` |
-| `mcp_server_support` | `LOKI_MCP_SERVER_SUPPORT` |
-| `enabled_mcp_servers` | `LOKI_ENABLED_MCP_SERVERS` |
-| `rag_embedding_model` | `LOKI_RAG_EMBEDDING_MODEL` |
-| `rag_reranker_model` | `LOKI_RAG_RERANKER_MODEL` |
-| `rag_top_k` | `LOKI_RAG_TOP_K` |
-| `rag_chunk_size` | `LOKI_RAG_CHUNK_SIZE` |
-| `rag_chunk_overlap` | `LOKI_RAG_CHUNK_OVERLAP` |
-| `highlight` | `LOKI_HIGHLIGHT` |
-| `theme` | `LOKI_THEME` |
-| `serve_addr` | `LOKI_SERVE_ADDR` |
-| `user_agent` | `LOKI_USER_AGENT` |
-| `save_shell_history` | `LOKI_SAVE_SHELL_HISTORY` |
-| `sync_models_url` | `LOKI_SYNC_MODELS_URL` |
-
-
-## Client Related Variables
-The following environment variables are available for clients in Loki:
-
-| Environment Variable | Description |
-|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `{client}_API_KEY` | For clients that require an API key, you can define the keys either through environment variables or
using the [vault](./VAULT.md). The variables are named after the client to which they apply;
e.g. `OPENAI_API_KEY`, `GEMINI_API_KEY`, etc. |
-| `LOKI_PLATFORM` | Combine with `{client}_API_KEY` to run Loki without a configuration file.
This variable is ignored if a configuration file exists. |
-| `LOKI_PATCH_{client}_CHAT_COMPLETIONS` | Patch chat completion requests to models on the corresponding client; Can modify the URL, body,
or headers. |
-| `LOKI_SHELL` | Specify the shell that Loki should be using when executing commands |
-
-## Files and Directory Related Variables
-You can also customize the files and directories that Loki loads its configuration files from:
-
-| Environment Variable | Description | Default Value |
-|----------------------|------------------------------------------------------------------------|---------------------------------|
-| `LOKI_CONFIG_DIR` | Customize the location of the Loki configuration directory. | `/loki` |
-| `LOKI_ENV_FILE` | Customize the location of the `.env` file to load at startup. | `/.env` |
-| `LOKI_CONFIG_FILE` | Customize the location of the global `config.yaml` configuration file. | `/config.yaml` |
-| `LOKI_ROLES_DIR` | Customize the location of the `roles` directory. | `/roles` |
-| `LOKI_SESSIONS_DIR` | Customize the location of the `sessions` directory. | `/sessions` |
-| `LOKI_RAGS_DIR` | Customize the location of the `rags` directory. | `/rags` |
-| `LOKI_FUNCTIONS_DIR` | Customize the location of the `functions` directory. | `/functions` |
-
-## Agent Related Variables
-You can also customize the location of full agent configurations using the following environment variables:
-
-| Environment Variable | Description |
-|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
-| `_CONFIG_FILE` | Customize the location of the agent's configuration file; e.g. `SQL_CONFIG_FILE` |
-| `_MODEL` | Customize the `model` used for the agent; e.g `SQL_MODEL` |
-| `_TEMPERATURE` | Customize the `temperature` used for the agent; e.g. `SQL_TEMPERATURE` |
-| `_TOP_P` | Customize the `top_p` used for the agent; e.g. `SQL_TOP_P` |
-| `_GLOBAL_TOOLS` | Customize the `global_tools` that are enabled for the agent (a JSON string array); e.g. `SQL_GLOBAL_TOOLS` |
-| `_MCP_SERVERS` | Customize the `mcp_servers` that are enabled for the agent (a JSON string array); e.g. `SQL_MCP_SERVERS` |
-| `_AGENT_SESSION` | Customize the `agent_session` used with the agent; e.g. `SQL_SESSION` |
-| `_INSTRUCTIONS` | Customize the `instructions` for the agent; e.g. `SQL_INSTRUCTIONS` |
-| `_VARIABLES` | Customize the `variables` used for the agent (in JSON format of `[{"key1": "value1", "key2": "value2"}]`);
e.g. `SQL_VARIABLES` |
-
-## Logging Related Variables
-The following variables can be used to change the log level of Loki or the location of the log file:
-
-| Environment Variable | Description | Default Value |
-|----------------------|---------------------------------------------|----------------------------------|
-| `LOKI_LOG_LEVEL` | Customize the log level of Loki | `INFO` |
-| `LOKI_LOG_FILE` | Customize the location of the Loki log file | `/loki/loki.log` |
-
-**Pro-Tip:** You can always tail the Loki logs using the `--tail-logs` flag. If you need to disable color output, you
-can also pass the `--disable-log-colors` flag as well.
-
-## Miscellaneous Variables
-| Environment Variable | Description | Default Value |
-|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
-| `AUTO_CONFIRM` | Bypass all `guard_*` checks in the bash prompt helpers; useful for agent composition and routing | |
-| `LLM_TOOL_DATA_FILE` | Set automatically by Loki on Windows. Points to a temporary file containing the JSON tool call data.
Tool scripts (`run-tool.sh`, `run-agent.sh`, etc.) read from this file instead of command-line args
to avoid JSON escaping issues when data passes through `cmd.exe` → bash. **Not intended to be set by users.** | |
\ No newline at end of file
diff --git a/docs/MACROS.md b/docs/MACROS.md
deleted file mode 100644
index a90aafe..0000000
--- a/docs/MACROS.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# Macros
-Macros are essentially Loki "scripts"; that is, a predefined sequence of REPL commands that automate repetitive tasks or
-workflows. Macros run in isolated environments, ensuring that the macros don't inherit any pre-existing role, session,
-RAG, or agent state, and they will not affect your current context.
-
-This isolation ensures that your workspace remains clean and unaffected by macro operations.
-
-
-
-For more information on Loki's REPL, refer to the [REPL](./REPL.md) documentation.
-
-## Quick Links
-
-- [Macro Definition](#macro-definition)
- - [Step Definitions](#step-definitions)
- - [Macro Variables](#macro-variables)
-- [Built-In Macros](#built-in-macros)
-
-
----
-
-## Macro Definition
-Macros are defined as YAML files in the `macros` subdirectory of your Loki configuration directory. The Loki configuration
-directory can vary between systems, so to find the location of your macros config directory, you can use the following
-command:
-
-```shell
-loki --info | grep 'macros_dir' | awk '{print $2}'
-```
-
-Macro definitions are broken into two parts: the `steps` of the macro, and an optional `variables` section that lets
-users pass in variables to alter the behavior of the macro at runtime.
-
-### Step Definitions
-The step definitions for a macro are straightforward: They are simply the exact commands you would otherwise type in the
-REPL.
-
-**Example: Macro to generate a git commit message**
-`macros/generate-commit-message.yaml`
-```yaml
-steps:
- - .file `git diff` -- generate git commit message
-```
-Usage:
-```shell
-$ loki --macro generate-commit-message
->> .file `git diff` -- generate a git commit message
-Add documentation on macros
-```
-
-For a full example configuration, refer to the [example macro configuration file](../config.macro.example.yaml) in the root of this project.
-
-### Macro Variables
-Sometimes it's useful to be able to modify the behavior of a macro at runtime. This is achieved with the `variables`
-array of the macro definition.
-
-To pass variables to a macro, since they are just Loki scripts, the syntax is the same as it is for any other scripting
-language: You just pass them alongside your invocation.
-
-**Example:**
-```shell
-$ loki --macro example-variable-macro first_argument second_argument
-```
-
-Each variable in the `variables` array has the following properties:
-* `name` (Required): the name of the variable, which can be referenced in the actual steps of the macro using the
- `{{name}}` syntax.
-* `default` (Optional): A default value for the variable if no value is specified. If no default value is defined, and
- no value is provided for the variable at runtime, Loki will error out.
-* `rest` (Optional, Boolean): When set to `true`, this variable will collect all remaining arguments passed to the
- macro. This behavior is only applicable when the variable is the last variable in the list. By default, this is
- `false`.
-
-The `variables` array is order-dependent; that is to say that all arguments passed to the macro are positional. So be
-careful about the ordering if that is important to your macro's invocation.
-
-**Example: Simple variable example to invoke an agent**
-`macros/invoke-agent.yaml`
-```yaml
-variables:
- - name: agent # No default value means this must be defined at runtime
- - name: args
- rest: true # All remaining arguments to the macro are collected into this variable
- default: What can you do? # This is used if no value is passed at runtime
-steps:
- - .agent {{agent}}
- - '{{args}}'
-```
-Usage:
-```shell
-$ loki --macro invoke-agent sql
-# or
-$ loki --macro invoke-agent sql What tables are available?
-```
-
-For a full example configuration, refer to the [example macro configuration file](../config.macro.example.yaml) in the root of this project.
-
-## Built-In Macros
-Loki comes packaged with some useful built-in macros. These are also good examples if you're looking for more examples
-on how to make your own macros, so be sure to check out the [built-in macro definitions](../assets/macros) if you're
-looking for more examples.
-
-* `generate-commit-message` - Generate a Git commit message based on the staged changes in the current directory
diff --git a/docs/RAG.md b/docs/RAG.md
deleted file mode 100644
index ea3fe9e..0000000
--- a/docs/RAG.md
+++ /dev/null
@@ -1,307 +0,0 @@
-# RAG
-Retrieval Augmented Generation (RAG) is a method of minimizing LLM hallucinations and extending the model's context
-without consuming a significant portion of the context length. It uses documents and other additional resources that you
-provide to give the model more context for all of your prompts.
-
-Loki has a built-in vector database and full-text search engine to support RAG knowledge bases for your queries.
-
-The generated knowledge bases are stored in the `rag` subdirectory of your Loki configuration directory. The location of
-this directory varies by system, so you can use the following command to find your RAG directory:
-
-```shell
-loki --info | grep 'rags_dir' | awk '{print $2}'
-```
-
-## Quick Links
-
-- [Usage](#usage)
- - [Persistent RAG](#persistent-rag)
- - [Ephemeral RAG](#ephemeral-rag)
-- [How It Works](#how-it-works)
- - [1. Build](#1-build)
- - [2. Lookup](#2-lookup)
- - [2a. Reranking (Optional)](#2a-reranking-optional)
- - [3. Prompt](#3-prompt)
-- [Supported Document Sources](#supported-document-sources)
-- [Document Loaders](#document-loaders)
- - [Document Loader Usage](#document-loader-usage)
-- [Advanced Customizations](#advanced-customizations)
- - [Embedding Model](#embedding-model)
- - [Reranker](#reranker)
- - [Chunk Size](#chunk-size)
- - [Trade-Offs](#chunk-size-trade-offs)
- - [Chunk Overlap](#chunk-overlap)
- - [Top K](#top-k)
- - [Trade-Offs](#top-k-trade-offs)
- - [RAG Template](#rag-template)
-
-
----
-
-## Usage
-There's two ways to use RAG in Loki: A persistent RAG that can be loaded on-demand for queries, and an ephemeral one for
-adding RAG to a single specific query.
-
-### Persistent RAG
-In the REPL, persistent RAG is initialized via the `.rag` command:
-
-
-
-The generated RAG is then saved to the `rag` subdirectory of the Loki configuration, and can then be loaded whenever you
-want that knowledge base via either `.rag ` or `loki --rag `.
-
-### Ephemeral RAG
-Short-lived RAG that is only used for a single session or query is loaded using `.file`/`--file`.
-
-You can use it to either execute a prompt from a file, or for temporary RAG. The difference is the usage of the `--`
-separator. If you only specify a filename and no `--` separator, Loki will know to read the file contents and pass them
-as a query to the model. Otherwise, the `--` separator is read to indicate that this is the end of the list of documents
-to load into the ephemeral RAG, and what follows is the query to pass to the model.
-
-```shell
-.file prompt.md # Read the file as a prompt
-.file %% -- translate the last reply to italian
-.file `git diff` -- generate a commit message
-```
-
-
-
-Once the session ends, this RAG will no longer be accessible and is only visible to the current session.
-
-#### The `%%` Document Type
-In addition to the usual documents that can be specified for persistent RAG, ephemeral RAG has a special `%%` value.
-This value references the content of the last reply. So you can use it like this:
-
-```shell
-.file %% -- translate the last reply to italian
-```
-
-The `--` indicates that this is the end of your documents and the beginning of your prompt.
-
-#### The `cmd` Document Type
-Loki also lets you use command outputs for ephemeral RAG input. Simply enclose the command in backticks:
-
-```shell
-.file `git diff` -- generate a commit message
-```
-
-The `--` indicates that this is the end of your documents and the beginning of your prompt.
-
-## How It Works
-#### 1. Build
-When you define RAG, Loki will first "build" the RAG. This means that Loki will consume the documents you specified and
-generate [embeddings](https://huggingface.co/spaces/hesamation/primer-llm-embedding) for that text. This essentially just means that Loki translates the document into a language
-the LLM can understand.
-
-These embeddings are then stored in an in-memory vector database.
-
-#### 2. Lookup
-Loki sits between you and the model. So when you submit a prompt to the model, before Loki ever sends it, it will first
-convert your prompt into embeddings (LLM language), and look for relevant snippets of text in the vector database.
-
-Loki then passes the top `n`-snippets of text that it finds in the vector database as additional context to the model
-before your prompt.
-
-#### 2a. Reranking (Optional)
-The lookup for relevant snippets of texts uses embeddings to find text that is semantically similar to your prompt, and
-returns the top `n`-results. This often works fairly well, however these top results aren't always the most relevant for
-answering the specific query.
-
-Reranking improves these initial results (say, the top 20-100 text snippets) and re-scores them using a more
-sophisticated model. The reranker model will rank documents by their actual usefulness for answering the query to ensure
-the most relevant context is passed to the model alongside your query.
-
-This reranking model can be customized for each RAG you build in Loki. See the [Custom Reranker](#reranker) section
-below for more details on how to customize this.
-
-#### 3. Prompt
-Finally, the text snippets that were looked up in RAG are passed to the model as additional context to your prompt,
-giving the model query-specific context to answer your question.
-
-## Supported Document Sources
-Loki supports a number of document sources that can be used for RAG:
-
-| Source | Example | Comments |
-|--------------------------|-----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Files | `/tmp/dir1/file1;/tmp/dir1/file2` | |
-| Directory | `/tmp/dir` | Picks up all files in a directory and all its subdirectories |
-| Directory (extensions} | `/tmp/dir2/**/*.{md,txt}` | Finds all files in all subdirectories with the specified extensions |
-| Recursive Filename | `/tmp/*/LOKI.md` | The following files will be picked up:
- `/tmp/dir1/LOKI.md`
- `/tmp/dir2/subdir1/LOKI.md`
- `/tmp/dir2/subdir2/LOKI.md`
|
-| URL | `https://www.ohdsi.org/data-standardization/` | Downloads and loads the specified webpage into the
knowledge base |
-| Recursive URL (Websites) | `https://github.com/OHDSI/Vocabulary-v5.0/wiki/**` | Crawls all pages under the given URL and loads them
into the knowledge base |
-| Document Loader (custom) | `jina:https://cloud.google.com/bigquery/docs/reference/standard-sql/` | Use a custom document loader to parse the given document |
-
-## Document Loaders
-Loki only has built-in support for loading text files. But that functionality can be extended to read all kinds of files
-into your knowledge bases. These custom loaders are used by both RAG and for documents specified using the
-`.file`/`--file` flags.
-
-In the global configuration file, you can specify loaders for specific document types using the `document_loaders`
-setting. Each loader is defined by specifying a name and then a command that Loki will execute to load the document.
-
-The following variables are interpolated at runtime by Loki and can be used as placeholders in your command definitions:
-* `$1` (Required) - The input file
-* `$2` (Optional) - The output file. If omitted, `stdout` is used as the output destination
-
-**Note:** It is your responsibility to ensure that any tools used to parse documents into text that Loki can read are
-installed on your system and are available on your `$PATH`. Loki does not have any built-in way of installing
-dependencies for document loaders for you.
-
-The following are some example loaders:
-```yaml
-document_loaders:
- pdf: 'pdftotext $1 -' # Use pdftotext to convert a PDF file to text
- # (see https://poppler.freedesktop.org for details on how to install pdftotext)
- docx: 'pandoc --to plain $1' # Use pandoc to convert a .docx file to text
- # (see https://pandoc.org for details on how to install pandoc)
- jina: 'curl -fsSL https://r.jina.ai/$1 -H "Authorization: Bearer {{JINA_API_KEY}}' # Use Jina to translate a website into text;
- # Requires a Jina API key to be added to the Loki vault
- git: > # Use yek to load a git repository into the knowledgebase (https://github.com/bodo-run/yek)
- sh -c "yek $1 --json | jq 'map({ path: .filename, contents: .content })'"
-```
-
-### Document Loader Usage
-Once you have your loaders defined, you can specify when Loki should use them by prefixing any RAG file/directory/URI
-with the name of the loader.
-
-**Example: Load a git repo into RAG**
-
-
-**Example: Use pdf loader for ephemeral RAG**
-```shell
-$ loki --file pdf:some-file.pdf
-```
-
-## Advanced Customizations
-For those familiar with RAG, Loki exposes a handful of advanced global settings that can be used to tweak your default
-RAG configurations.
-
-### Embedding Model
-When Loki queries your RAG knowledge bases, it needs to first convert your query into embeddings. By default, Loki uses
-the same embedding model that was used to create the knowledge base in the first place.
-
-This can be customized to any other embedding model available in your configured clients by setting the
-`rag_embedding_model` setting in your global Loki configuration file:
-
-```yaml
-rag_embedding_model: null # Specifies the embedding model used for context retrieval
-```
-
-### Reranker
-By default, Loki uses [Reciprocal Rank Fusion (RRF)](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion) to merge vector and keyword search results.
-
-You can change the default reranker model to any other reranking model in your configured clients. To change the default
-reranker model, simply change the value of the `rag_reranker_model` setting in your global configuration file:
-
-```yaml
-rag_reranker_model: null # By default,
-```
-
-### Chunk Size
-In the context of RAG, the chunk size is the maximum length of each text chunk (measured in characters) that is created
-when splitting documents. In Loki, this defaults to `2000` characters.
-
-You can specify a different global default by setting the `rag_chunk_size` property in your global configuration file:
-
-```yaml
-rag_chunk_size: null # Defines the size of chunks for document processing in characters
-```
-
-#### Chunk Size Trade-Offs
-Keep in mind the following trade-offs when changing the chunk size:
-
-* **Smaller chunks (e.g. 256 characters):** More precise retrieval, better semantic focus, but may lack context or split
- important information
-* **Larger chunks (e.g. 1024 characters):** More context preserved, fewer chunks to manage, but less precise matching
- and more noise in retrieved document
-
-### Chunk Overlap
-Chunk overlap in RAG is the number of characters that overlap between consecutive chunks to maintain continuity.
-
----
-
-**Example:** If the following sentence is cut off at the end of one chunk
-
-`I was doing fine until someone brought up`
-
-You'll ideally want that full sentence to be picked up at the beginning of the next chunk to make sure the full meaning
-is captured. So in this example, if your chunk overlap is 42 characters, then the start of the next chunk would look
-like this:
-
-`I was doing fine until someone brought up the game. `
-
----
-
-Often, this value is 10%-20% of the chunk size.
-
-By default, in Loki, this value is 5% the chunk size. You can override this and specify the default chunk overlap (in
-characters) that Loki should use as a global default by setting the `rag_chunk_overlap` property in the global Loki
-configuration file:
-
-```yaml
-rag_chunk_overlap: null # Defines the overlap between chunks
-```
-
-### Top K
-In RAG, `top_k` represents the top `k`-chunks to return from the vector database query. Think of it like if you search
-something on Google and only care about the top 10 results, that's what you'll use for your context.
-
-In Loki, the default value for this is `5`. You can customize this global default by setting the `rag_top_k` property in
-your global configuration file:
-
-```yaml
-rag_top_k: 5 # Specifies the number of documents to retrieve for answering queries
-```
-
-#### Top K Trade-Offs
-When customizing this value, keep in mind the following trade-offs so you get the best performance:
-
-* **Lower top_k (e.g. 3):** Faster, more focused context, lower cost, but risks missing relevant information
-* **Higher top_k (e.g. 10):** More comprehensive coverage, but more noise, higher latency, increased token costs, and
- potential context window constraints
-
-### RAG Template
-When you use RAG in Loki, after Loki performs the lookup for relevant chunks of text to add as context to your query, it
-will add the retrieved text chunks as context to your query before sending it to the model. The format of this context
-is determined by the `rag_template` setting in your global Loki configuration file.
-
-This template utilizes three placeholders:
-* `__INPUT__`: The user's actual query
-* `__CONTEXT__`: The context retrieved from RAG
-* `__SOURCES__`: A numbered list of the source file paths or URLs that the retrieved context came from
-
-These placeholders are replaced with the corresponding values into the template and make up what's actually passed to
-the model at query-time. The `__SOURCES__` placeholder enables the model to cite which documents its answer is based on,
-which is especially useful when building knowledge-base assistants that need to provide verifiable references.
-
-The default template that Loki uses is the following:
-
-```text
-Answer the query based on the context while respecting the rules. (user query, some textual context and rules, all inside xml tags)
-
-
-__CONTEXT__
-
-
-
-__SOURCES__
-
-
-
-- If you don't know, just say so.
-- If you are not sure, ask for clarification.
-- Answer in the same language as the user query.
-- If the context appears unreadable or of poor quality, tell the user then answer as best as you can.
-- If the answer is not in the context but you think you know the answer, explain that to the user then answer with your own knowledge.
-- Answer directly and without using xml tags.
-- When using information from the context, cite the relevant source from the section.
-
-
-
-__INPUT__
-
-```
-
-You can customize this template by specifying the `rag_template` setting in your global Loki configuration file. Your
-template *must* include both the `__INPUT__` and `__CONTEXT__` placeholders in order for it to be valid. The
-`__SOURCES__` placeholder is optional. If it is omitted, source references will not be included in the prompt.
diff --git a/docs/REPL-PROMPT.md b/docs/REPL-PROMPT.md
deleted file mode 100644
index 01acbb9..0000000
--- a/docs/REPL-PROMPT.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# Customize REPL Prompt
-
-The prompt you see when you start the Loki REPL can be customized to your liking. This is achieved via the `left_prompt`
-and `right_prompt` settings in the global Loki configuration file:
-
-```yaml
-left_prompt: '{color.red}{model}){color.green}{?session {?agent {agent}>}{session}{?role /}}{!session {?agent {agent}>}}{role}{?rag @{rag}}{color.cyan}{?session )}{!session >}{color.reset} '
-right_prompt: '{color.purple}{?session {?consume_tokens {consume_tokens}({consume_percent}%)}{!consume_tokens {consume_tokens}}}{color.reset}'
-```
-
-The location of the global configuration file differs between systems, so you can use the following command to find your
-global configuration file's location:
-
-```shell
-loki --info | grep 'config_file' | awk '{print $2}'
-```
-
-## Quick Links
-
-- [Syntax](#syntax)
-- [Variables](#variables)
-
-
-## Syntax
-The syntax for the prompts consists of plain text and templates contained in `{...}`. The plain text is
-printed exactly as given.
-
-The syntax for the templates `{...}` is as follows:
-
-* `{variable}` - Replaced with the value of `variable`
-* `{?variable }` - Evaluate the `` when `variable` is evaluated to `true`
-* `{!variable }` - Evaluate the `` when `variable` is evaluated to `false`
-
-Where a `` is another expression consisting of plain text and/or more special computations inside `{...}`.
-
-Variables are evaluated to also be "truthy"; that is, if a variable is undefined, it is considered to be the exact same
-as if that variable's value was `false`.
-
-**Example 1: Simple Boolean Usage**
-For the prompt `{?variable yay}{!variable boo}`, if `variable=true`, then the output will be
-```
-yay
-```
-
-And if `variable=false`:
-```
-boo
-```
-
-**Example 2: Nested Expressions**
-For the prompt `{?variable {!variable2 yay}>}`, and assuming
-* `variable=true`
-* `variable2=false`
-the output will be
-```
-yay>
-```
-
-If `variable2=true`, the output will be empty.
-
-If `variable=false`, the output will be empty.
-
-## Variables
-The following variables and output modifiers are available to you when you're creating your prompts:
-
-```yaml
-# Model Variables
-model: openai:gpt-4 # The active model's full name
-client_name: openai # The name of the client serving the active model
-model_name: gpt-4 # The aliased name of the active model
-max_input_tokens: 4096 # The maximum number of input tokens for the active model
-
-# Configuration Variables
-temperature: 1.0 # The temperature for the active model
-top_p: 0.9 # The top_p for the active model
-dry_run: true # Whether the given command is flagged to be a dry run
-stream: false # Whether streaming responses are enabled
-save: true # Whether shell history is saved
-wrap: 120 # The number of characters to allow before wrapping around output to the next line
-
-# Role Variables
-role: code # The active role
-
-# Session Variables
-session: temp # The name of the active session
-dirty: false # Whether the session settings have been updated but not persisted
-consume_tokens: 200 # The number of tokens consumed
-consume_percent: 1% # The percentage of tokens consumed to the maximum input tokens
-user_messages_len: 0 # The total number of sent user messages
-
-# RAG Variables
-rag: temp # The name of the active RAG
-
-# Agent Variables
-agent: todo-sh # The name of the active agent
-
-# ANSI COLORS
-color.reset:
-color.black:
-color.dark_gray:
-color.red:
-color.light_red:
-color.green:
-color.light_green:
-color.yellow:
-color.light_yellow:
-color.blue:
-color.light_blue:
-color.purple:
-color.light_purple:
-color.magenta:
-color.light_magenta:
-color.cyan:
-color.light_cyan:
-color.white:
-color.light_gray:
-```
diff --git a/docs/REPL.md b/docs/REPL.md
deleted file mode 100644
index 2726883..0000000
--- a/docs/REPL.md
+++ /dev/null
@@ -1,260 +0,0 @@
-# Loki REPL Guide
-In addition to being a CLI, Loki also has a built-in REPL (Read-Execute-Print-Loop). This enables users to quickly try
-out prompts, commands, configurations, and everything in between without having to modify the same command every time.
-
-You can enter the REPL by simply typing `loki` without any follow-up flags or arguments.
-## Quick Links
-
-- [Features](#features)
-- [REPL Commands](#repl-commands)
- - [`.model` - Change the current LLM](#model---change-the-current-llm)
- - [`.role` - Role management](#role---role-management)
- - [`.prompt` - Set a temporary role using a prompt](#prompt---set-a-temporary-role-using-a-prompt)
- - [`.session` - Session management](#session---session-management)
- - [`.agent` - Chat with an AI agent](#agent---chat-with-an-ai-agent)
- - [`.rag` - Chat with documents](#rag---chat-with-documents)
- - [`.macro` - Execute a macro](#macro---execute-a-macro)
- - [`.file` - Read files and use them as input](#file---read-files-and-use-them-as-input)
- - [`.vault` - Manage the Loki vault](#vault---manage-the-loki-vault)
- - [`.continue` - Continue the previous response](#continue---continue-the-previous-response)
- - [`.regenerate` - Regenerate the last response](#regenerate---regenerate-the-last-response)
- - [`.copy` - Copy the last response to your clipboard](#copy---copy-the-last-response-to-your-clipboard)
- - [`.set` - Adjust runtime settings](#set---adjust-runtime-settings)
- - [`.edit` - Modify configuration files](#edit---modify-configuration-files)
- - [`.delete` - Delete configurations from Loki](#delete---delete-configurations-from-loki)
- - [`.info` - Display information about the current mode](#info---display-information-about-the-current-mode)
- - [`.authenticate` - Authenticate the current model client via OAuth](#authenticate---authenticate-the-current-model-client-via-oauth)
- - [`.exit` - Exit an agent/role/session/rag or the Loki REPL itself](#exit---exit-an-agentrolesessionrag-or-the-loki-repl-itself)
- - [`.help` - Show the help guide](#help---show-the-help-guide)
-
-
----
-
-## Features
-The REPL has features that are intended to make your Loki experience as easy and as enjoyable as possible! This includes
-things like
-
-* **Tab Autocompletion:** Every command in the REPL (i.e. everything that starts with a `.`) has fuzzy search auto
- completions.
- * `.` to complete REPL commands
- * `.model ` to complete chat models
- * `.set ` to complete configuration keys
- * `.set key ` to complete configuration values
-* **Multi-Line Prompts:** You can also type prompts that span more than one line to help organize your thoughts. This
- can be done in the following ways:
- * `Ctrl-o` to open the current input buffer in your preferred editor (either the value of `editor` or `$EDITOR`)
- * You can paste multi-line text
- * You can type `:::` to start multi-line editing, and use `:::` to finish it.
- * And finally, you can use hotkeys like `{ctrl/shift/alt}+enter` or `ctrl-j` to insert a new line directly in the
- REPL.
-* **History Search** Press `ctrl+r` to search the REPL history, and navigate it with `↑↓`
-* **Configurable Keybindings:** You can switch between `emacs` style keybindings or `vi` style keybindings
-* [**Custom REPL Prompt:**](./REPL-PROMPT.md) You can even customize the REPL prompt to display information about the
- current context in the prompt
-* **Built-in user interaction tools:** When function calling is enabled in the REPL, the `user__ask`, `user__confirm`,
- `user__input`, and `user__checkbox` tools are always available for interactive prompts. These are not injected in the
- one-shot CLI mode.
-
----
-
-## REPL Commands
-All REPL commands begin with a `.` to indicate that they're not part of a prompt. The following list details the
-commands available in Loki:
-
-### `.model` - Change the current LLM
-When browsing models in the REPL, use the following legend to understand the purpose of each column in the model table:
-```
-openai:gpt-4o 128000 / 4096 | 5 / 15 👁 ⚒
-| | | | | | └─ supports function calling
-| | | | | └─ support vision (multi-modal)
-| | | | └─ output price ($/1M)
-| | | └─ input price ($/1M)
-| | |
-| | └─ max output tokens
-| └─ max input tokens
-└─ model id
-```
-
-
-For more information about how to add models to Loki, refer to the [clients documentation](./clients/CLIENTS.md).
-
-### `.role` - Role management
-Loki offers the following commands to manage your roles:
-
-| Command | Description |
-|--------------|-------------------------------------------------------------------------|
-| `.role` | Create or switch to a role |
-| `.info role` | Show information about the active role |
-| `.edit role` | Open the active role's configuration file in your preferred text editor |
-| `.save role` | Save the active role and its configurations to a configuration file |
-| `.exit role` | Exit the active role |
-
-
-
-For more information about roles in Loki and how to build them, refer to the [roles documentation](./ROLES.md).
-
-### `.prompt` - Set a temporary role using a prompt
-If you need to create a temporary role that you want to discard after use, you use `.prompt`. `.prompt`-based roles
-cannot be persisted to a file and saved.
-
-
-
-### `.session` - Session management
-Use the following commands to manage sessions in Loki:
-
-| Command | Description |
-|---------------------|---------------------------------------------------------------------------------------------|
-| `.session` | Start or switch to a session |
-| `.empty session` | Clear all messages for the active session |
-| `.compress session` | Compress the session messages using the `summarization_prompt` setting in the global config |
-| `.info session` | Display information about the active session |
-| `.edit session` | Open the active session's configuration in your preferred text editor |
-| `.save session` | Save the active session to a `session` configuration file |
-| `.exit session` | Exit the active session |
-
-
-
-For more information on sessions and how to use them in Loki, refer to the [sessions documentation](./SESSIONS.md).
-
-### `.agent` - Chat with an AI agent
-Loki lets you build OpenAI GPT-style agents. The following commands let you interact with and manage your agents in
-Loki:
-
-| Command | Description |
-|----------------------|-----------------------------------------------------------------------------------------------|
-| `.agent` | Use an agent |
-| `.starter` | Display and use conversation starters for the active agent |
-| `.clear todo` | Clear the todo list and stop auto-continuation (requires `auto_continue: true` on the agent) |
-| `.edit agent-config` | Open the agent configuration in your preferred text editor |
-| `.info agent` | Display information about the active agent |
-| `.exit agent` | Leave the active agent |
-
-
-
-For more information on agents in Loki and how to create them, refer to the [agents documentation](./AGENTS.md).
-
-### `.rag` - Chat with documents
-RAG (Retrieval Augmented Generation) enables you to load documents into the LLM so you can ask questions about it or
-complete tasks using the documents as additional context.
-
-| Command | Description |
-|------------------|------------------------------------------------------------------------------|
-| `.rag` | Initialize or access a RAG |
-| `.edit rag-docs` | Add or remove documents from the active RAG using your preferred text editor |
-| `.rebuild rag` | Rebuild the active RAG to accommodate document changes |
-| `.sources rag` | Show a works-cited of the sources used in the last query |
-| `.info rag` | Display information about the active RAG |
-| `.exit rag` | Exit the active RAG |
-
-
-
-For more information about RAG in Loki and how to utilize it, refer to the [rag documentation](./RAG.md).
-
-### `.macro` - Execute a macro
-Macros in Loki are like "scripts" of commands that can be run in isolated environments; that means they do not use any
-active settings and use the same settings they had when written. They are created/executed using the `.macro `
-command.
-
-
-
-For more information on macros in Loki and how to create them, refer to the [macros documentation](./MACROS.md).
-
-### `.file` - Read files and use them as input
-Loki lets you specify any number of documents that you can load and use as ephemeral RAG to chat with the LLM. To see
-what files or values you can pass to it, simply run the command `.file` with no arguments:
-
-```shell
-openai:gpt-4o)> .file
-Usage: .file ... [-- ...]
-```
-
-
-
-For more information about ephemeral RAG, refer to the [ephemeral RAG documentation](./RAG.md#ephemeral-rag).
-
-### `.vault` - Manage the Loki vault
-The Loki vault lets users store sensitive secrets and credentials securely so that there's no plaintext secrets
-anywhere in your configurations.
-
-
-
-For more information about the Loki vault, refer to the [vault documentation](./VAULT.md).
-
-### `.continue` - Continue the previous response
-When you have a response that exceeds the context length, you can use the `.continue` command to continue the generation
-of the last response.
-
-
-
-### `.regenerate` - Regenerate the last response
-If ever your response is interrupted, or you want to try generating it again, you can use the `.regenerate` command to do
-this without having to retype your query:
-
-
-
-### `.copy` - Copy the last response to your clipboard
-If you're trying to copy the last response (like copying some code), you can use the `.copy` command to copy the entire
-last response to your system clipboard:
-
-
-
-### `.set` - Adjust runtime settings
-You can use `.set` to adjust select settings at runtime. This is useful when you're experimenting with settings and want
-to know how they'll affect Loki. To persist the changes you make, be sure to update them in the global configuration
-file.
-
-
-
-### `.edit` - Modify configuration files
-The `.edit` command lets you modify configuration files for the current mode of the REPL. It will open the selected
-configuration in your preferred text editor. It lets you modify the following configurations:
-
-* `.edit config` - Modify the global configuration
-* `.edit role` - Modify the active role's configuration
-* `.edit session` - Modify the active session's configuration
-* `.edit agent-config` - Modify the active agent's configuration
-* `.edit rag-docs` - Add or remove documents from the active RAG
-
-### `.delete` - Delete configurations from Loki
-The `.delete` command allows you to delete entities in Loki without having to directly run `rm -rf` on the configuration
-directory or file corresponding to the target entity. You can use it to delete the following entities:
-
-* `.delete role` - Delete select roles
-* `.delete session` - Delete select sessions
-* `.delete macro` - Delete select macros
-* `.delete rag` - Delete select RAGs
-* `.delete agent-data` - Delete select agent's configurations and all tools
-
-### `.info` - Display information about the current mode
-The `.info` command provides useful information about different modes that Loki may be operating in. It's helpful if you
-want a quick understanding of the system info, a role's configuration, an agent's configuration, etc.
-
-The following entities are supported:
-
-| Command | Description |
-|-----------------|-------------------------------------------------------------|
-| `.info` | Display system information (identical to the `--info` flag) |
-| `.info role` | Display information about the active role |
-| `.info session` | Display information about the active session |
-| `.info agent` | Display information about the active agent |
-| `.info rag` | Display information about the active RAG |
-
-### `.authenticate` - Authenticate the current model client via OAuth
-The `.authenticate` command will start the OAuth flow for the current model client if
-* The client supports OAuth (See the [clients documentation](./clients/CLIENTS.md#providers-that-support-oauth) for supported clients)
-* The client is configured in your Loki configuration to use OAuth via the `auth: oauth` property
-
-### `.exit` - Exit an agent/role/session/rag or the Loki REPL itself
-The `.exit` command is used to move between modes in the Loki REPL.
-
-| Command | Description |
-|-----------------|-------------------------|
-| `.exit role` | Exit the active role |
-| `.exit session` | Exit the active session |
-| `.exit agent` | Exit the active agent |
-| `.exit rag` | Exit the active RAG |
-| `.exit` | Exit the Loki REPL |
-
-### `.help` - Show the help guide
-Just like with any shell or REPL, you sometimes need a little help and want to know what commands are available to you.
-That's when you use the `.help` command.
diff --git a/docs/ROLES.md b/docs/ROLES.md
deleted file mode 100644
index 2adebcf..0000000
--- a/docs/ROLES.md
+++ /dev/null
@@ -1,266 +0,0 @@
-# Roles
-When customizing the behavior or LLMs, we use roles to "constrain" the responses or behavior of the LLM to whatever
-purpose we desire.
-
-Think of them kind of like a baby: That baby can grow up to do anything! Be a resume builder, teacher, engineer, etc.
-
-The only difference is that with roles, we're explicitly telling the LLM what we want it to be. Also: the LLM is already
-grown up so we don't have to wait!
-
-
-
-## Quick Links
-
-- [Role Definition](#role-definition)
- - [Metadata Header](#metadata-header)
- - [Instructions](#instructions)
- - [Special Case: Metadata Header Only](#special-case-metadata-header-only)
-- [Prompt Types](#prompt-types)
- - [Embedded Prompts](#embedded-prompts)
- - [System Prompts](#system-prompts)
- - [Few-Shot Prompt](#few-shot-prompt)
-- [Built-In Roles](#built-in-roles)
-
-
----
-
-## Role Definition
-Roles in Loki are Markdown files that live in the `roles` directory of your Loki configuration. Loki configuration
-locations vary between systems, so you can use the following command to find the location of your roles configuration
-directory:
-
-```shell
-loki --info | grep 'roles_dir' | awk '{print $2}'
-```
-
-All role configuration files have two parts: The metadata header, and the instructions.
-
-**Example:** An expert resume builder role that specializes in helping users build and refine their resumes.
-```markdown
----
-# This is the metadata header
-name: resume-builder
-model: openai:gpt-4o
-temperature: 0.2
-top_p: 0
-enabled_tools: fs_ls,fs_cat
-enabled_mcp_servers: github
----
-
-You are an expert resume builder.
-```
-
-To see a full example configuration for a role, refer to the [example role configuration](../config.role.example.md)
-file in the root of the repo.
-
-### Metadata Header
-The metadata header in all role configuration files is completely optional. It lets you define role-specific settings
-for each role that make the model work the way you want for your role. This includes things like forcing your role to
-always use a specific model, set of tools, and tailoring the hyperparameters of the model for your role.
-
-The header consists of a YAML-formatted list of settings that let you customize the model behavior for your role. These
-settings sit between `---` separators in your role configuration so Loki knows they're not part of the instructions you
-want to feed the model.
-
-The following table lists the available configuration settings and their default values (if undefined):
-
-| Setting | Default | Description |
-|-----------------------|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
-| `name` | The name of the role markdown file | The name of the role |
-| `model` | Default configured model or currently in-use model (REPL mode) | The preferred model to use with this role |
-| `temperature` | Default `temperature` for the preferred model | Controls the creativity and randomness of the model's responses |
-| `top_p` | Default `top_p` for the preferred model | Alternative way to control the model's output diversity, affecting the
probability distribution of tokens |
-| `enabled_tools` | Global setting for `enabled_tools` | The tools that this role utilizes |
-| `enabled_mcp_servers` | Global setting for `enabled_mcp_servers` | The MCP servers that this role utilizes |
-| `prompt` | `null` | See [Prompt Types](#prompt-types) for detailed usage |
-
-### Instructions
-The instructions for a role is what you use to tell the model how you want it to behave. This typically consists of one
-or two sentences, but can be more. To see some examples, look at the [built-in roles](../assets/roles) to see how they are defined.
-
-**Pro-Tip:** The struggle to create good instructions for a role (or any other kind of instructions for your model) is
-so common, that Loki comes with a role to help you write instructions for roles! Simply invoke the role to start
-creating a role with the `create-prompt` role:
-
-```shell
-loki -r create-prompt
-```
-
-### Special Case: Metadata Header Only
-When instructions are defined, the metadata header is optional. However sometimes we want a way to enable specific
-functions or MCP servers when prompting different models. In this situation, you need only specify the metadata header
-to just enable these settings as you like.
-
-**Example: Role that enables all filesystem tools**
-`roles/filesystem-functions.md`
-```markdown
----
-enabled_tools: fs_ls,fs_cat,fs_mkdir,fs_patch,fs_write
----
-```
-
-**Example: Role that enables the GitHub MCP server with the ollama:deepseek-r1 model**
-`roles/github.md`
-```markdown
----
-model: ollama:deepseek-r1
-enabled_mcp_servers: github
----
-```
-
-For more examples of this special use case of roles, you can look at the role configuration files for the following
-built-in roles:
-
-* [explain-shell](../assets/roles/explain-shell.md) - Explains cryptic shell commands in natural language
-* [functions](../assets/roles/functions.md) - Enables all available functions (i.e. all globally `visible_functions`)
-* [mcp-servers](../assets/roles/mcp-servers.md) - Enables all available MCP servers
-
-## Special Variables
-Loki has a set of built-in special variables that it will inject into your role's instructions if it finds them in the
-`{{variable_name}}` syntax. The available special variables are listed below:
-
-| Name | Description | Example |
-|-----------------|-----------------------------------------------------------|----------------------------|
-| `__os__` | Operating system name | `linux` |
-| `__os_family__` | Operating system family | `unix` |
-| `__arch__` | System architecture | `x86_64` |
-| `__shell__` | The current user's default shell | `bash` |
-| `__locale__` | The current user's preferred language and region settings | `en-US` |
-| `__now__` | Current timestamp in ISO 8601 format | `2025-11-07T10:15:44.268Z` |
-| `__cwd__` | The current working directory | `/tmp` |
-
-## Prompt Types
-In Loki, you can also create roles with pre-configured prompts so you can template prompts for your use cases. This is
-the purpose of the `prompt` field in the role's metadata header.
-
-There's three types of prompts you can create:
-
-### Embedded Prompts
-Embedded prompts let you create templated prompts for any input given to it. They are ideal for concise, input-driven
-replies from the model. The input that users pass to Loki are injected into your prompt via a `__INPUT__` placeholder in
-your prompt.
-
-**Example: Role to convert the given input to TOML**
-`roles/convert-to-toml.md`
-```markdown
----
-prompt: convert __INPUT__ to TOML
----
-Convert the given input to TOML format. Exclude any markdown formatting or code blocks and only output code.
-```
-Usage:
-```shell
-$ loki -r json-to-toml '{"test":"hi me"}'
-test = "hi me"
-```
-
-Without the instructions (i.e. the prompt after the metadata header), this role would simply generate the following
-message for the model:
-
-```json
-[
- {"role": "user", "content": "convert {\"test\":\"hi me\"} to TOML"}
-]
-```
-
-### System Prompts
-System prompts let you set the general context of the LLMs behavior. This is no different than other system prompts you
-define in ChatGPT, Claude, Open WebUI, etc.
-
-They are essentially Embedded Prompts without an `__INPUT__` placeholder.
-
-**Example: Role to convert all input words to emoji**
-`roles/emoji.md`
-```markdown
----
-prompt: convert my words to emojis
----
-Convert all given input words into emojis
-```
-Usage:
-```shell
-$ loki -r emoji music joy
-🎵 😊
-```
-
-Without the instructions (i.e. the prompt after the metadata header), this role would simply generate the following
-messages for the model:
-
-```json
-[
- {"role": "system", "content": "convert my words to emojis"},
- {"role": "user", "content": "music joy"}
-]
-```
-
-### Few-Shot Prompt
-[Few-Shot prompting](https://www.promptingguide.ai/techniques/fewshot) is a technique to enable in-context learning for LLMs by providing examples in the prompt to steer
-the model to better performance. In Loki, this is done as an extension of System Prompts.
-
-**Example: Role to output code only**
-`roles/code-generator.md`
-~~~markdown
----
-prompt: |-
- Output code only without comments or explanations.
- ### INPUT:
- async sleep in js
- ### OUTPUT:
- ```javascript
- async function timeout(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- ```
----
-Output code only in response to the user's request
-~~~
-Usage:
-~~~shell
-$ loki -r code-generator python add two numbers
-```python
-# Function to add two numbers
-def add_numbers(num1, num2):
- return num1 + num2
-
-# Example usage
-number1 = 5
-number2 = 7
-
-result = add_numbers(number1, number2)
-print(f"The sum of {number1} and {number2} is {result}.")
-```
-~~~
-
-Without the instructions (i.e. the prompt after the metadata header), this role would simply generate the following
-messages for the model:
-
-```json
-[
- {"role": "system", "content": "Output code only without comments or explanations."},
- {"role": "user", "content": "async sleep in js"},
- {"role": "assistant", "content": "```javascript\nasync function timeout(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n```"},
- {"role": "user", "content": "python add two numbers"}
-]
-```
-
-## Built-In Roles
-Loki comes packaged with some useful built-in roles. These are also good examples if you're looking for more examples on
-how to make your own roles, so be sure to check out the [built-in role definitions](../assets/roles) if you're looking
-for more examples.
-
-* `code`: Generates code (used by `loki -c`)
-* `create-prompt`: Creates a prompt based on the user's input
-* `create-title`: Creates 3-6 word titles based on the user's input
-* `explain-shell`: Explains shell commands
-* `functions`: Enable all globally-visible functions
-* `github`: Interact with GitHub using natural language
-* `mcp-servers`: Enables all MCP servers
-* `repo-analyzer`: Ask questions about the code repository in the current working directory
-* `shell`: Convert natural language into shell commands (used by `loki -e`)
-* `slack`: Interact with Slack using natural language
-
-## Temporary Roles
-Loki also enables you to create temporary roles that will be discarded once you're finished with them. This is done via
-the `.prompt/--prompt` command:
-
-
\ No newline at end of file
diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md
deleted file mode 100644
index 7f683ec..0000000
--- a/docs/SESSIONS.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# Sessions
-By default, Loki does not send back all previous messages in a conversation to the model. This means that each time you
-query a model, it's essentially a one-off. However, Loki does support chat-like conversations with LLMs via its
-`sessions` mechanism.
-
-Sessions in Loki enable the familiar conversational interactions with LLMs. This means you can reference previous
-answers and ask follow-up questions and the model will know what you're referring to.
-
-Sessions can be temporary, or can be saved so you can continue conversations at a later time.
-
-Saved sessions are stored in the `sessions` subdirectory of the Loki configuration directory. The location of the
-`sessions` directory varies by system, so you can use the following command to find the `sessions` directory if you need
-it:
-
-```shell
-loki --info | grep 'sessions_dir' | awk '{print $2}'
-```
-
-## Usage
-When you use a session in Loki, you can either persist it or discard it once you're done. Sessions you discard are then
-just considered 'temporary' sessions.
-
-
-
-Sessions you persist and then load again later will inherit the same configuration as was used during the last usage of
-that session. That is to say, if you had certain tools or MCP servers enabled when you were last in that session, they
-will be available again when you continue that session.
-
-## Configuration
-Session behavior can be configured from the global Loki configuration file. The location of this file varies between
-systems so you can use the following command to locate it on your system:
-
-```shell
-loki --info | grep 'config_file' | awk '{print $2}'
-```
-
-The following settings are available to customize the default behavior of sessions globally:
-
-| Setting | Description |
-|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `save_session` | Controls the persistence of the session.
- If `true`, then any time you're in a session, changes will auto-save unless explicitly defined otherwise.
- If `false`, then any time you're in a session, changes will not auto-save unless explicitly specified otherwise.
- If `null`, Loki will always prompt the user for what to do.
|
-| `compression_threshold` | Defines the token count threshold at which Loki will compress the session to save on the context length |
-| `summarization_prompt` | This is the prompt that is used to compress the session up to a given point when compression is triggered |
-| `summary_context_prompt` | This is the prompt that's used to add the summarized conversation generated by the `summarization_prompt` as context to the model |
diff --git a/docs/SHELL-INTEGRATIONS.md b/docs/SHELL-INTEGRATIONS.md
deleted file mode 100644
index b5353cd..0000000
--- a/docs/SHELL-INTEGRATIONS.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Loki Shell Integrations
-Loki supports the following integrations with a handful of shell environments to enhance user experience and streamline workflows.
-
-## Tab Completions
-### Dynamic
-Dynamic tab completions are supported by Loki to assist users in quickly completing commands, options, and arguments.
-You can enable it by using the corresponding command for your shell. To enable dynamic tab completions for every
-shell session (i.e. persistently), add the corresponding command to your shell's configuration file as indicated:
-
-```shell
-# Bash
-# (add to: `~/.bashrc`)
-source <(COMPLETE=bash loki)
-
-# Zsh
-# (add to: `~/.zshrc`)
-source <(COMPLETE=zsh loki)
-
-# Fish
-# (add to: `~/.config/fish/config.fish`)
-source <(COMPLETE=fish loki | psub)
-
-# Elvish
-# (add to: `~/.elvish/rc.elv`)
-eval (E:COMPLETE=elvish loki | slurp)
-
-# PowerShell
-# (add to: `$PROFILE`)
-$env:COMPLETE = "powershell"
-loki | Out-String | Invoke-Expression
-```
-
-At the time of writing, `nushell` is not yet fully supported for dynamic tab completions due to limitations
-in the [`clap`](https://crates.io/crates/clap) crate. However, `nushell` support is being actively developed, and will
-be added in a future release.
-
-Progress on this feature can be tracked in the following issue: [Clap Issue #5840](https://github.com/clap-rs/clap/issues/5840).
-
-### Static
-Static tab completions (i.e. pre-generated completion scripts that are not context aware) can also be generated using the
-`--completions` flag. You can enable static tab completions by using the corresponding commands for your shell. These commands
-will enable them for every shell session (i.e. persistently):
-
-```shell
-# Bash
-echo 'source <(loki --completions bash)' >> ~/.bashrc
-
-# Zsh
-echo 'source <(loki --completions zsh)' >> ~/.zshrc
-
-# Fish
-echo 'loki --completions fish | source' >> ~/.config/fish/config.fish
-
-# Elvish
-echo 'eval (loki --completions elvish | slurp)' >> ~/.elvish/rc.elv
-
-# Nushell
-[[ -d ~/.config/nushell/completions ]] || mkdir -p ~/.config/nushell/completions
-loki --completions nushell | save -f ~/.config/nushell/completions/loki.nu
-echo 'use ~/.config/nushell/completions/cli.nu *' >> ~/.config/nushell/config.nu
-
-# PowerShell
-Add-content $PROFILE "loki --completions powershell | Out-String | Invoke-Expression"
-```
-
-## Shell Assistant
-Loki has an `-e,--execute` flag that allows users to run natural language commands directly from the CLI. It accepts
-natural language input and translates it into executable shell commands.
-
-
-
-## Intelligent Command Completions
-Loki also provides shell scripts that bind `Alt-e` to `loki -e ""`, allowing users to generate
-commands from natural text directly without invoking the CLI.
-
-For example:
-
-```shell
-$ find all typescript files with more than 100 lines
-# Gets replaced with
-$ find . -name '*.ts' -type f -exec awk 'NR>100{exit 1}' {} \; -print
-```
-
-To use the CLI helper, add the content of the appropriate integration script for your shell to your shell configuration file:
-* [Bash Integration](../scripts/shell-integration/integration.bash) (add to: `~/.bashrc`)
-* [Zsh Integration](../scripts/shell-integration/integration.zsh) (add to: `~/.zshrc`)
-* [Elvish Integration](../scripts/shell-integration/integration.elv) (add to: `~/.elvish/rc.elv`)
-* [Fish Integration](../scripts/shell-integration/integration.fish) (add to: `~/.config/fish/config.fish`)
-* [Nushell Integration](../scripts/shell-integration/integration.nu) (add to: `~/.config/nushell/config.nu`)
-* [PowerShell Integration](../scripts/shell-integration/integration.ps1) (add to: `$PROFILE`)
-
-## Explain Commands
-In addition to the Shell Assistant, Loki has a built-in role that explains shell commands to you to decipher their
-language. So if Loki generates a command that you're unsure of what it does, simply pass it to the `explain-shell` role:
-
-
-
-## Code Generation
-Users can also directly generate code snippets from natural language prompts using the `-c,--code` flag.
-
-
-
-**Pro Tip:** Pipe the output of the code generation directly into `tee` to ensure the generated code is properly extracted
-from any generated Markdown (i.e. remove any triple backticks).
diff --git a/docs/THEMES.md b/docs/THEMES.md
deleted file mode 100644
index 5d625a3..0000000
--- a/docs/THEMES.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Theming Loki
-Loki supports customizing the theme via a `.tmTheme` file.
-
-## Setup
-To install a custom theme, download the `.tmTheme` file to the Loki configuration directory and name it `dark.tmTheme`
-or `light.tmTheme`. The location of the Loki configuration directory varies between systems, so you can use the
-following command to locate it on your system:
-
-```shell
-loki --info | grep 'config_dir' | awk '{print $2}'
-```
-
-## Themes
-### 1337-Scheme
-https://raw.githubusercontent.com/MarkMichos/1337-Scheme/ca6a329cfda8307449d405b70f8fab34b8fd23b5/1337.tmTheme
-
-
-### Coldark
-https://raw.githubusercontent.com/ArmandPhilippot/coldark-bat/e44750b2a9629dd12d8ed3ad9fd50c77232170b9/Coldark-Dark.tmTheme
-
-
-### Dracula
-https://raw.githubusercontent.com/dracula/sublime/c2de0acf5af67042393cf70de68013153c043656/Dracula.tmTheme
-
-
-### GitHub
-https://raw.githubusercontent.com/AlexanderEkdahl/github-sublime-theme/508740b2430c3c3a9e785fc93ee1d7c6f233af53/GitHub.tmTheme
-
-
-### gruvbox
-#### Dark
-https://raw.githubusercontent.com/subnut/gruvbox-tmTheme/64c47250e54298b91e2cf8d401320009aba9f991/gruvbox-dark.tmTheme
-
-
-#### Light
-https://raw.githubusercontent.com/subnut/gruvbox-tmTheme/64c47250e54298b91e2cf8d401320009aba9f991/gruvbox-light.tmTheme
-
-
-### OneHalf
-#### Dark
-https://raw.githubusercontent.com/sonph/onehalf/141c775ace6b71992305f144a8ab68e9a8ca4a25/sublimetext/OneHalfDark.tmTheme
-
-
-#### Light
-https://raw.githubusercontent.com/sonph/onehalf/141c775ace6b71992305f144a8ab68e9a8ca4a25/sublimetext/OneHalfLight.tmTheme
-
-
-### Solarized
-#### Dark
-https://raw.githubusercontent.com/braver/Solarized/87e01090cggjf5fb821a234265b3138426ae84900e7/Solarized%20(dark).tmTheme
-
-
-#### Light
-https://raw.githubusercontent.com/braver/Solarized/87e01090cf5fb821a234265b3138426ae84900e7/Solarized%20(light).tmTheme
-
-
-### Sublime Snazzy
-https://raw.githubusercontent.com/greggb/sublime-snazzy/70343201f1d7539adbba3c79e2fe81c2559a0431/Sublime%20Snazzy.tmTheme
-
-
-### TwoDark
-https://raw.githubusercontent.com/erremauro/TwoDark/8e0f6fa5b59d196658a22288f519fd8320de4c87/TwoDark.tmTheme
-
-
-### Visual Studio Dark+
-https://raw.githubusercontent.com/vidann1/visual-studio-dark-plus/01ee1e8e0dc578f3b4e8c0dbb6aa0279b4a26a40/Visual%20Studio%20Dark%2B.tmTheme
-
-
-### Zenburn
-https://raw.githubusercontent.com/colinta/zenburn/86d4ee7a1f884851a1d21d66249687f527fced32/zenburn.tmTheme
-
\ No newline at end of file
diff --git a/docs/TODO-SYSTEM.md b/docs/TODO-SYSTEM.md
deleted file mode 100644
index 18a06eb..0000000
--- a/docs/TODO-SYSTEM.md
+++ /dev/null
@@ -1,250 +0,0 @@
-# Todo System
-
-Loki's Todo System is a built-in task tracking feature designed to improve the reliability and effectiveness of LLM agents,
-especially smaller models. It provides structured task management that helps models:
-
-- Break complex tasks into manageable steps
-- Track progress through multistep workflows
-- Automatically continue work until all tasks are complete
-- Avoid forgetting steps or losing context
-
-
-
-## Quick Links
-
-- [Why Use the Todo System?](#why-use-the-todo-system)
-- [How It Works](#how-it-works)
-- [Configuration Options](#configuration-options)
-- [Available Tools](#available-tools)
-- [Auto-Continuation](#auto-continuation)
-- [Best Practices](#best-practices)
-- [Example Workflow](#example-workflow)
-- [Troubleshooting](#troubleshooting)
-
-
-## Why Use the Todo System?
-Smaller language models often struggle with:
-- **Context drift**: Forgetting earlier steps in a multi-step task
-- **Incomplete execution**: Stopping before all work is done
-- **Lack of structure**: Jumping between tasks without clear organization
-
-The Loki Todo System addresses these issues by giving the model explicit tools to plan, track, and verify task completion.
-The system automatically prompts the model to continue when incomplete tasks remain, ensuring work gets finished.
-
-## How It Works
-1. **Planning Phase**: The model initializes a todo list with a goal and adds individual tasks
-2. **Execution Phase**: The model works through tasks, marking each done immediately after completion
-3. **Continuation Phase**: If incomplete tasks remain, the system automatically prompts the model to continue
-4. **Completion**: When all tasks are marked done, the workflow ends naturally
-
-The todo state is preserved across the conversation (and any compressions), and injected into continuation prompts,
-keeping the model focused on remaining work.
-
-## Configuration Options
-The Todo System is configured per-agent in `/agents//config.yaml`:
-
-| Setting | Type | Default | Description |
-|----------------------------|---------|-------------|---------------------------------------------------------------------------------|
-| `auto_continue` | boolean | `false` | Enable the To-Do system for automatic continuation when incomplete todos remain |
-| `max_auto_continues` | integer | `10` | Maximum number of automatic continuations before stopping |
-| `inject_todo_instructions` | boolean | `true` | Inject the default todo tool usage instructions into the agent's system prompt |
-| `continuation_prompt` | string | (see below) | Custom prompt used when auto-continuing |
-
-### Example Configuration
-```yaml
-# agents/my-agent/config.yaml
-model: openai:gpt-4o
-auto_continue: true # Enable auto-continuation
-max_auto_continues: 15 # Allow up to 15 automatic continuations
-inject_todo_instructions: true # Include todo instructions in system prompt
-continuation_prompt: | # Optional: customize the continuation prompt
- [CONTINUE]
- You have unfinished tasks. Proceed with the next pending item.
- Do not explain; just execute.
-```
-
-### Default Continuation Prompt
-If `continuation_prompt` is not specified, the following default is used:
-
-```
-[SYSTEM REMINDER - TODO CONTINUATION]
-You have incomplete tasks in your todo list. Continue with the next pending item.
-Call tools immediately. Do not explain what you will do.
-```
-
-## Available Tools
-When `inject_todo_instructions` is enabled (the default), agents have access to four built-in todo management tools:
-
-### `todo__init`
-Initialize a new todo list with a goal. Clears any existing todos.
-
-**Parameters:**
-- `goal` (string, required): The overall goal to achieve when all todos are completed
-
-**Example:**
-```json
-{"goal": "Refactor the authentication module"}
-```
-
-### `todo__add`
-Add a new todo item to the list.
-
-**Parameters:**
-- `task` (string, required): Description of the todo task
-
-**Example:**
-```json
-{"task": "Extract password validation into separate function"}
-```
-
-**Returns:** The assigned task ID
-
-### `todo__done`
-Mark a todo item as done by its ID.
-
-**Parameters:**
-- `id` (integer, required): The ID of the todo item to mark as done
-
-**Example:**
-```json
-{"id": 1}
-```
-
-### `todo__list`
-Display the current todo list with status of each item.
-
-**Parameters:** None
-
-**Returns:** The full todo list with goal, progress, and item statuses
-
-### `todo__clear`
-Clear the entire todo list and reset the goal. Use when the current task has been canceled or invalidated.
-
-**Parameters:** None
-
-**Returns:** Confirmation that the todo list was cleared
-
-### REPL Command: `.clear todo`
-You can also clear the todo list manually from the REPL by typing `.clear todo`. This is useful when:
-- You gave a custom response that changes or cancels the current task
-- The agent is stuck in auto-continuation with stale todos
-- You want to start fresh without leaving and re-entering the agent
-
-**Note:** This command is only available when an agent with `auto_continue: true` is active. If the todo
-system isn't enabled for the current agent, the command will display an error message.
-
-## Auto-Continuation
-When `auto_continue` is enabled, Loki automatically sends a continuation prompt if:
-
-1. The agent's response completes (model stops generating)
-2. There are incomplete tasks in the todo list
-3. The continuation count hasn't exceeded `max_auto_continues`
-4. The response isn't identical to the previous continuation (prevents loops)
-
-### What Gets Injected
-Each continuation prompt includes:
-- The continuation prompt text (default or custom)
-- The current todo list state showing:
- - The goal
- - Progress (e.g., "3/5 completed")
- - Each task with status (✓ done, ○ pending)
-
-**Example continuation context:**
-```
-[SYSTEM REMINDER - TODO CONTINUATION]
-You have incomplete tasks in your todo list. Continue with the next pending item.
-Call tools immediately. Do not explain what you will do.
-
-Goal: Refactor the authentication module
-Progress: 2/4 completed
- ✓ 1. Extract password validation into separate function
- ✓ 2. Add unit tests for password validation
- ○ 3. Update login handler to use new validation
- ○ 4. Update registration handler to use new validation
-```
-
-### Visual Feedback
-During auto-continuation, you'll see a message in your terminal:
-```
-📋 Auto-continuing (3/10): 2 incomplete todo(s) remain
-```
-
-## Best Practices
-
-### For Agent Developers
-1. **Enable for complex workflows**: Use `auto_continue: true` for agents that handle multistep tasks
-2. **Set reasonable limits**: Adjust `max_auto_continues` based on typical task complexity
-3. **Customize the prompt**: If your agent needs specific continuation behavior, set a custom `continuation_prompt`
-
-### For Model Behavior
-The injected instructions tell models to:
-
-1. **Always create a todo list before starting work**: This ensures planning happens upfront
-2. **Mark each task done immediately after finishing**: Don't batch completions
-3. **Add all planned steps before starting**: Complete planning before execution
-
-### When to Use vs. Skip
-**Use the Todo System when:**
-- Tasks have 3+ distinct steps
-- The model might lose track of progress
-- You want guaranteed completion of all steps
-- Working with smaller/less capable models
-
-**Skip the Todo System when:**
-- Single-step, simple tasks
-- Interactive Q&A sessions
-- The overhead of task tracking isn't worth it
-
-## Example Workflow
-Here's how a typical todo-driven workflow looks:
-
-**User prompt:** "Add input validation to the user registration form"
-
-**Model actions:**
-```
-1. todo__init(goal="Add input validation to user registration form")
-2. todo__add(task="Analyze current registration form fields")
-3. todo__add(task="Create validation rules for email field")
-4. todo__add(task="Create validation rules for password field")
-5. todo__add(task="Implement client-side validation")
-6. todo__add(task="Add server-side validation")
-7. todo__add(task="Write tests for validation logic")
-```
-
-**Model executes first task, then:**
-```
-8. todo__done(id=1)
-9. [Proceeds with task 2...]
-10. todo__done(id=2)
-...
-```
-
-**If model stops with incomplete tasks:**
-- System automatically sends continuation prompt
-- Model sees remaining tasks and continues
-- Repeats until all tasks are done or max continuations reached
-
-## Troubleshooting
-
-### Model Not Using Todo Tools
-- Verify `inject_todo_instructions: true` in your agent config
-- Check that the agent is properly loaded (not just a role)
-- Some models may need explicit prompting to use the tools
-
-### Too Many Continuations
-- Lower `max_auto_continues` to a reasonable limit
-- Check if the model is creating new tasks without completing old ones
-- Ensure tasks are appropriately scoped (not too granular)
-
-### Continuation Loop
-The system detects when a model's response is identical to its previous continuation response and stops
-automatically. If you're seeing loops:
-- The model may be stuck; check if a task is impossible to complete
-- Consider adjusting the `continuation_prompt` to be more directive
-
----
-
-## Additional Docs
-- [Agents](./AGENTS.md) - Full agent configuration guide
-- [Function Calling](./function-calling/TOOLS.md) - How tools work in Loki
-- [Sessions](./SESSIONS.md) - How conversation state is managed
diff --git a/docs/VAULT.md b/docs/VAULT.md
deleted file mode 100644
index 9bb6374..0000000
--- a/docs/VAULT.md
+++ /dev/null
@@ -1,161 +0,0 @@
-# The Loki Vault
-The Loki vault lets users store sensitive secrets and credentials securely so that there's no plaintext secrets
-anywhere in your configurations.
-
-It's based on the [G-Man library](https://github.com/Dark-Alex-17/gman) (which also comes in a binary format) which
-functions as a universal secret management tool.
-
-
-
-## Quick Links
-
-- [Usage](#usage)
- - [CLI Usage](#cli-usage)
- - [REPL Usage](#repl-usage)
-- [Motivation](#motivation)
-- [How it works](#how-it-works)
-- [Supported Files](#supported-files)
-- [Environment Variable Secret Injection in Agents](#environment-variable-secret-injection-in-agents)
-
-
----
-
-## Usage
-The Loki vault can be used in one of two ways: via the CLI or via the REPL for interactive usage.
-
-### CLI Usage
-The vault is utilized from the CLI with the following flags:
-
-```bash
---add-secret Add a secret to the Loki vault
---get-secret Decrypt a secret from the Loki vault and print the plaintext
---update-secret Update an existing secret in the Loki vault
---delete-secret Delete a secret from the Loki vault
---list-secrets List all secrets stored in the Loki vault
-```
-(The above is also documented in `loki --help`)
-
-Loki will guide you through manipulating your secrets to make usage easier.
-
-### REPL Usage
-The vault can be access from within the Loki REPL using the `.vault` commands:
-
-
-
-
-The manipulation of your vault is guided in the same way as the CLI usage, ensuring ease of use.
-
-## Motivation
-Loki is intended to be highly configurable and adaptable to many different use cases. This means that users of Loki
-should be able to share configurations for agents, tools, roles, etc. with other users or even entire teams.
-
-My objective is to encourage this, and to make it so that users can easily version their configurations using version
-control. Good VCS hygiene dictates that one *never* commits secrets or sensitive information to a repository.
-
-Since a number of files and configurations in Loki may contain sensitive information, the vault exists to solve this problem.
-
-Users can either share the vault password with a team, making it so a single configuration can be pulled from VCS and used
-by said team. Alternatively, each user can maintain their own vault password and expect other users to replace secret values
-with their user-specific secrets.
-
-## How it works
-When you first start Loki, if you don't already have a vault password file, it will prompt you to create one. This file
-houses the password that is used to encrypt and decrypt secrets within Loki. This file exists so that you are not prompted
-for a password every time Loki attempts to decrypt a secret.
-
-When you encrypt a secret, it uses the local provider for `gman` to securely store those secrets in the Loki vault file.
-This file is typically located at your Loki configuration directory under `vault.yml`. If you open this file, you'll see a
-bunch of gibberish. This is because all secrets are encrypted using the password you provided, meaning only you can decrypt them.
-
-Secrets are specified in Loki configurations using the same variable templating as the [Jinja templating engine](https://jinja.palletsprojects.com/en/stable/):
-
-```
-{{some_variable}}
-```
-
-So whenever you want Loki to use a secret from the vault, you simply specify the secret name in this format in the applicable
-file.
-
-**Example:**
-Suppose my vault has a secret called `GITHUB_TOKEN` in it, and I want to use that in the MCP configuration. Then, I simply replace
-the expected value in my `mcp.json` with the templated secret:
-
-```json
-{
- "mcpServers": {
- "atlassian": {
- "command": "npx",
- "args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
- },
- "github": {
- "command": "docker",
- "args": [
- "run",
- "-i",
- "--rm",
- "-e",
- "GITHUB_PERSONAL_ACCESS_TOKEN",
- "ghcr.io/github/github-mcp-server"
- ],
- "env": {
- "GITHUB_PERSONAL_ACCESS_TOKEN": "{{GITHUB_TOKEN}}"
- }
- }
- }
-}
-```
-
-At runtime, Loki will detect the templated secret and replace it with the decrypted value from the vault before executing.
-
-## Supported Files
-At the time of writing, the following files support Loki secret injection:
-
-| File Type | Description | Limitations |
-|-------------------------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
-| `config.yaml` | The main Loki configuration file | Cannot use secret injection on the `vault_password_file` field |
-| `functions/mcp.json` | The MCP server configuration file | |
-| `/tools.` | Tool files for agents | Specific configuration and only supported for Agents, not all global tools ([see below](#environment-variable-secret-injection-in-agents)) |
-
-
-Note that all paths are relative to the Loki configuration directory. The directory varies by system, so you can find yours by
-running
-
-```shell
-loki --info | grep config_dir | awk '{print $2}'
-```
-
-## Environment Variable Secret Injection in Agents
-Secrets from the Loki vault can be injected into agent `tools.sh/tools.py` as environment variables. This is done as
-follows:
-
-1. Ensure a secret named `MY_USERNAME` is in your Loki vault.
-2. Set the name of the secret as the default value for a variable
- `/config.yaml`
- ```yaml
- name: Username
- description: An AI agent that demonstrates agent capabilities
- instructions: |
- You are a AI agent designed to demonstrate agent capabilities.
- variables:
- - name: username
- description: Your user name
- # Configure the secret you want to inject using the same templating mentioned above; i.e. wrap the
- # case-sensitive name in '{{}}'
- default: '{{MY_USERNAME}}'
- ```
-3. Reference the variable in your `/tools.` file using the familiar variable injection name; that is,
- since the name of the variable is `username`, the environment variable that will be provided to the tool call will
- be named `LLM_AGENT_VAR_USERNAME`
- `tools.sh`
- ```bash
- #!/usr/bin/env bash
- # @env LLM_OUTPUT=/dev/stdout The output path
-
- # @cmd Get my username
- get_my_username() {
- echo "$LLM_AGENT_VAR_USERNAME" >> "$LLM_OUTPUT"
- }
- ```
-
-For more information about variable usage within agents, refer to the [Variables section](./AGENTS.md#user-defined-variables) of the [Agents README](./AGENTS.md)
-
diff --git a/docs/clients/CLIENTS.md b/docs/clients/CLIENTS.md
deleted file mode 100644
index 5e98a5b..0000000
--- a/docs/clients/CLIENTS.md
+++ /dev/null
@@ -1,185 +0,0 @@
-# Model Clients
-
-Loki supports a large number of model providers (referred to as `clients` since Loki is a client of these providers). In
-order to use them, you must configure each one in the `clients` array in the global Loki configuration file.
-
-The location of the global Loki configuration file varies between systems, so you can use the following command to
-locate your configuration file:
-
-```shell
-loki --info | grep 'config_file' | awk '{print $2}'
-```
-
-## Quick Links
-
-- [Supported Clients](#supported-clients)
-- [Client Configuration](#client-configuration)
-- [Authentication](#authentication)
-- [Extra Settings](#extra-settings)
-
-
----
-
-## Supported Clients
-Loki supports the following model client types:
-
-* Azure AI Foundry
-* AWS Bedrock
-* Anthropic Claude
-* Cohere
-* Google Gemini
-* OpenAI
-* OpenAI-Compatible
-* GCP Vertex AI
-
-In addition to the settings detailed below, each client may have additional settings specific to the provider. Check the
-[example global configuration file](../../config.example.yaml) to verify that your client has all the necessary fields
-defined.
-
-## Client Configuration
-Each client in Loki has the same configuration settings available to them, with only special authentication fields added
-for specific clients as necessary. They are each placed under the `clients` array in your global configuration file:
-
-```yaml
-clients:
- - name: client1
- # ... client configuration ...
- - name: client2
- # ... client configuration ...
-```
-
-### Metadata
-The client metadata uniquely identifies the client in Loki so you can reference it across your configurations. The
-available settings are listed below:
-
-| Setting | Description |
-|----------|------------------------------------------------------------------------------------------------------------|
-| `name` | The name of the client (e.g. `openai`, `gemini`, etc.) |
-| `auth` | Authentication method: `oauth` for OAuth, or omit to use `api_key` (see [Authentication](#authentication)) |
-| `models` | See the [model settings](#model-settings) documentation below |
-| `patch` | See the [client patch configuration](./PATCHES.md#client-configuration-patches) documentation |
-| `extra` | See the [extra settings](#extra-settings) documentation below |
-
-Be sure to also check provider-specific configurations for any extra fields that are added for authentication purposes.
-
-### Model Settings
-The `models` array lists the available models from the model client. Each one has the following settings:
-
-| Setting | Required | Model Type | Description |
-|-----------------------------|----------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `name` | * | `all` | The name of the model |
-| `real_name` | | `all` | You can define model aliases via the `name` field. However, Loki still needs to know the real name
of the model so it can query it. For example: If you have `name: gpt-alias`, then you must
also define `real_name: gpt-oss:latest` |
-| `type` | * | `all` | The type of model. Loki supports only 3 types of models: - `chat`
- `embedding`
- `reranker`
|
-| `input_price` | | `all` | The cost in USD per 1M tokens for each input sequence; Loki will keep track of usage costs if this is defined |
-| `output_price` | | `all` | The cost in USD per 1M tokens of the model output; Loki will keep track of usage costs if this is defined |
-| `patch` | | `all` | See the [model-specific patch configuration](./PATCHES.md#model-specific-patches) documentation |
-| `max_input_tokens` | | `all` | The maximum number of input tokens for the model |
-| `max_output_tokens` | | `chat` | The maximum number of output tokens for the model |
-| `require_max_tokens` | | `chat` | Whether to enforce the `max_output_tokens` constraint. |
-| `supports_vision` | | `chat` | Indicates if the model supports multimodal queries that would require vision (i.e. image recognition) |
-| `supports_function_calling` | | `chat` | Indicates if the model supports function calling |
-| `no_stream` | | `chat` | Enable or disable streaming API responses |
-| `no_system_message` | | `chat` | Controls whether the model supports system messages |
-| `system_prompt_prefix` | | `chat` | An additional prefix prompt to add to all system prompts to ensure consistent behavior across all interactions |
-| `max_tokens_per_chunk` | | `embedding` | The maximum chunk size supported by the embedding model |
-| `default_chunk_size` | | `embedding` | The default chunk size to use with the given model |
-| `max_batch_size` | | `embedding` | The maximum batch size that the given embedding model supports |
-
-## Authentication
-
-Loki clients support two authentication methods: **API keys** and **OAuth**. Each client entry in your configuration
-must use one or the other.
-
-### API Key Authentication
-
-Most clients authenticate using an API key. Simply set the `api_key` field directly or inject it from the
-[Loki vault](../VAULT.md):
-
-```yaml
-clients:
- - type: claude
- api_key: '{{ANTHROPIC_API_KEY}}'
-```
-
-API keys can also be provided via environment variables named `{CLIENT_NAME}_API_KEY` (e.g. `OPENAI_API_KEY`,
-`GEMINI_API_KEY`). See the [environment variables documentation](../ENVIRONMENT-VARIABLES.md#client-related-variables)
-for details.
-
-### OAuth Authentication
-
-For [providers that support OAuth](#providers-that-support-oauth), you can authenticate using your existing subscription instead of an API key. This uses
-the OAuth 2.0 PKCE flow.
-
-**Step 1: Configure the client**
-
-Add a client entry with `auth: oauth` and no `api_key`:
-
-```yaml
-clients:
- - type: claude
- name: my-claude-oauth
- auth: oauth
-```
-
-**Step 2: Authenticate**
-
-Run the `--authenticate` flag with the client name:
-
-```sh
-loki --authenticate my-claude-oauth
-```
-
-Or if you have only one OAuth-configured client, you can omit the name:
-
-```sh
-loki --authenticate
-```
-
-Alternatively, you can use the REPL command `.authenticate`.
-
-This opens your browser for the OAuth authorization flow. Depending on the provider, Loki will either start a
-temporary localhost server to capture the callback automatically (e.g. Gemini) or ask you to paste the authorization
-code back into the terminal (e.g. Claude). Loki stores the tokens in `~/.cache/loki/oauth` and automatically refreshes
-them when they expire.
-
-#### Gemini OAuth Note
-Loki uses the following scopes for OAuth with Gemini:
-* https://www.googleapis.com/auth/generative-language.peruserquota
-* https://www.googleapis.com/auth/userinfo.email
-* https://www.googleapis.com/auth/generative-language.retriever (Sensitive)
-
-Since the `generative-language.retriever` scope is a sensitive scope, Google needs to verify Loki, which requires full
-branding (logo, official website, privacy policy, terms of service, etc.). The Loki app is open-source and is designed
-to be used as a simple CLI. As such, there's no terms of service or privacy policy associated with it, and thus Google
-cannot verify Loki.
-
-So, when you kick off OAuth with Gemini, you may see a page similar to the following:
-
-
-Simply click the `Advanced` link and click `Go to Loki (unsafe)` to continue the OAuth flow.
-
-
-
-
-**Step 3: Use normally**
-
-Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically:
-
-```sh
-loki -m my-claude-oauth:claude-sonnet-4-20250514 "Hello!"
-```
-
-> **Note:** You can have multiple clients for the same provider. For example: you can have one with an API key and
-> another with OAuth. Use the `name` field to distinguish them.
-
-### Providers That Support OAuth
-* Claude
-* Gemini
-
-## Extra Settings
-Loki also lets you customize some extra settings for interacting with APIs:
-
-| Setting | Description |
-|-------------------|-------------------------------------------------------|
-| `proxy` | Set a proxy to use |
-| `connect_timeout` | Set the timeout in seconds for connections to the API |
diff --git a/docs/clients/PATCHES.md b/docs/clients/PATCHES.md
deleted file mode 100644
index 23246b9..0000000
--- a/docs/clients/PATCHES.md
+++ /dev/null
@@ -1,368 +0,0 @@
-# Request Patching in Loki
-Loki provides two mechanisms for modifying API requests sent to LLM providers: **Model-Specific Patches** and
-**Client Configuration Patches**. These allow you to customize request parameters, headers, and URLs to work around
-provider quirks or add custom behavior.
-
-## Quick Links
-- [Model-Specific Patches](#model-specific-patches)
-- [Client Configuration Patches](#client-configuration-patches)
-- [Comparison](#comparison)
-- [Common Use Cases](#common-use-cases)
-- [Environment Variable Patches](#environment-variable-patches)
-- [Tips](#tips)
-- [Debugging Patches](#debugging-patches)
-
----
-
-## Model-Specific Patches
-
-### Overview
-Model-specific patches are applied **unconditionally** to a single model. They are useful for handling model-specific
-quirks or requirements.
-
-### When to Use
-- A specific model requires certain parameters to be set or removed
-- A model needs different default values than other models from the same provider
-- You need to add special configuration for one model only
-
-### Structure
-
-```yaml
-models:
- - name: model-name
- type: chat
- # ... other model properties ...
- patch:
- url: "https://custom-endpoint.com" # Optional: override the API endpoint
- body: # Optional: modify request body
- : # Add or modify parameters
- : null # Remove parameters (set to null)
- headers: # Optional: modify request headers
- : # Add or modify headers
- : null # Remove headers (set to null)
-```
-
-### Examples
-
-#### Example 1: Removing Parameters
-OpenAI's o1 models don't support `temperature`, `top_p`, or `max_tokens` parameters. The `patch` removes them:
-
-```yaml
-- name: o4-mini
- type: chat
- max_input_tokens: 200000
- max_output_tokens: 100000
- supports_function_calling: true
- patch:
- body:
- max_tokens: null # Remove max_tokens from request
- temperature: null # Remove temperature from request
- top_p: null # Remove top_p from request
-```
-
-#### Example 2: Setting Required Parameters
-Some models require specific parameters to be set:
-
-```yaml
-- name: o4-mini-high
- type: chat
- patch:
- body:
- reasoning_effort: high # Always set reasoning_effort to "high"
- max_tokens: null
- temperature: null
-```
-
-#### Example 3: Custom Endpoint
-If a model needs a different API endpoint:
-
-```yaml
-- name: custom-model
- type: chat
- patch:
- url: "https://special-endpoint.example.com/v1/chat"
-```
-
-#### Example 4: Adding Headers
-Add authentication or custom headers:
-
-```yaml
-- name: special-model
- type: chat
- patch:
- headers:
- X-Custom-Header: "special-value"
- X-API-Version: "2024-01"
-```
-
-### How It Works
-1. When you use a model, Loki loads its configuration
-2. If the model has a `patch` field, it's **always applied** to every request
-3. The patch modifies the request URL, body, or headers before sending to the API
-4. Parameters set to `null` are **removed** from the request
-
----
-
-## Client Configuration Patches
-
-### Overview
-Client configuration patches allow you to apply customizations to **multiple models** based on
-**regex pattern matching**. They're defined in your `config.yaml` file and can target specific API types (`chat`,
-`embeddings`, or `rerank`).
-
-### When to Use
-- You want to apply the same settings to multiple models from a provider
-- You need different configurations for different groups of models
-- You want to override the default client model settings
-- You need environment-specific customizations
-
-### Structure
-
-```yaml
-clients:
- - type: # e.g., gemini, openai, claude
- # ... client configuration ...
- patch:
- chat_completions: # For chat models
- '': # Regex to match model names
- url: "..." # Optional: override endpoint
- body: # Optional: modify request body
- :
- headers: # Optional: modify headers
- :
- embeddings: # For embedding models
- '':
- # ... same structure ...
- rerank: # For reranker models
- '':
- # ... same structure ...
-```
-
-### Pattern Matching
-- Patterns are **regular expressions** that match against the model name
-- Use `.*` to match all models
-- Use specific patterns like `gpt-4.*` to match model families
-- Use `model1|model2` to match multiple specific models
-
-### Examples
-
-#### Example 1: Disable Safety Filters for Gemini Models
-Apply to all Gemini chat models:
-
-```yaml
-clients:
- - type: gemini
- api_key: "{{GEMINI_API_KEY}}"
- patch:
- chat_completions:
- '.*': # Matches all Gemini models
- body:
- safetySettings:
- - category: HARM_CATEGORY_HARASSMENT
- threshold: BLOCK_NONE
- - category: HARM_CATEGORY_HATE_SPEECH
- threshold: BLOCK_NONE
- - category: HARM_CATEGORY_SEXUALLY_EXPLICIT
- threshold: BLOCK_NONE
- - category: HARM_CATEGORY_DANGEROUS_CONTENT
- threshold: BLOCK_NONE
-```
-
-#### Example 2: Apply Settings to Specific Model Family
-Only apply to GPT-4 models (not GPT-3.5):
-
-```yaml
-clients:
- - type: openai
- api_key: "{{OPENAI_API_KEY}}"
- patch:
- chat_completions:
- 'gpt-4.*': # Matches gpt-4, gpt-4-turbo, gpt-4o, etc.
- body:
- frequency_penalty: 0.2
- presence_penalty: 0.1
-```
-
-#### Example 3: Different Settings for Different Models
-Apply different patches based on model name:
-
-```yaml
-clients:
- - type: openai
- api_key: "{{OPENAI_API_KEY}}"
- patch:
- chat_completions:
- 'gpt-4o': # Specific model
- body:
- temperature: 0.7
- 'gpt-3.5.*': # Model family
- body:
- temperature: 0.9
- max_tokens: 2000
-```
-
-#### Example 4: Modify Embedding Requests
-Apply to embedding models:
-
-```yaml
-clients:
- - type: openai
- api_key: "{{OPENAI_API_KEY}}"
- patch:
- embeddings:
- 'text-embedding-.*': # All text-embedding models
- body:
- dimensions: 1536
- encoding_format: "float"
-```
-
-#### Example 5: Custom Headers for Specific Models
-Add headers only for certain models:
-
-```yaml
-clients:
- - type: openai-compatible
- api_base: "https://api.example.com/v1"
- patch:
- chat_completions:
- 'custom-model-.*':
- headers:
- X-Custom-Auth: "bearer-token"
- X-Model-Version: "latest"
-```
-
-#### Example 6: Override Endpoint for Specific Models
-Use different endpoints for different model groups:
-
-```yaml
-clients:
- - type: openai-compatible
- api_base: "https://default-endpoint.com/v1"
- patch:
- chat_completions:
- 'premium-.*': # Premium models use different endpoint
- url: "https://premium-endpoint.com/v1/chat/completions"
-```
-
-### How It Works
-1. When making a request, Loki checks if the client has a `patch` configuration
-2. It looks at the appropriate API type (`chat_completions`, `embeddings`, or `rerank`)
-3. For each pattern in that section, it checks if the regex matches the model name
-4. If a match is found, that patch is applied to the request
-5. Only the **first matching pattern** is applied (patterns are processed in order)
-
----
-
-## Comparison
-
-| Feature | Model-Specific Patch | Client Configuration Patch |
-|-----------------------|-----------------------|-------------------------------------|
-| **Scope** | Single model only | Multiple models via regex |
-| **Matching** | Exact model name | Regular expression pattern |
-| **Application** | Always applied | Only if pattern matches |
-| **API Type** | All APIs | Separate for chat/embeddings/rerank |
-| **Override** | Cannot be overridden | Can override model patch |
-| **Use Case** | Model-specific quirks | User preferences & customization |
-| **Application Order** | Applied first | Applied second (can override) |
-
-### Patch Application Order
-When both patches are present, they're applied in this order:
-
-1. **Model-Specific Patch**
-2. **Client Configuration Patch**
-
-This means client configuration patches can override model-specific patches if they modify the same parameters.
-
-## Common Use Cases
-
-### Removing Unsupported Parameters
-Some models don't support standard parameters like `temperature` or `max_tokens`:
-
-**Model Patch**:
-```yaml
-patch:
- body:
- temperature: null
- max_tokens: null
-```
-
-### Adding Provider-Specific Parameters
-Providers often have unique parameters:
-
-**Client Patch**:
-```yaml
-patch:
- chat_completions:
- '.*':
- body:
- safetySettings: [...] # Gemini
- thinking_budget: 10000 # DeepSeek
- response_format: # OpenAI
- type: json_object
-```
-
-### Changing Endpoints
-Use custom or regional endpoints:
-
-**Client Patch**:
-```yaml
-patch:
- chat_completions:
- '.*':
- url: "https://eu-endpoint.example.com/v1/chat"
-```
-
-### Setting Default Values
-Provide defaults for specific models or model families:
-
-**Client Patch**:
-```yaml
-patch:
- chat_completions:
- 'claude-3-.*':
- body:
- max_tokens: 4096
- temperature: 0.7
-```
-
-### Custom Authentication
-Add special authentication headers:
-
-**Client Patch**:
-```yaml
-patch:
- chat_completions:
- '.*':
- headers:
- Authorization: "Bearer {{custom_token}}"
- X-Organization-ID: "org-123"
-```
-
-## Environment Variable Patches
-You can also apply patches via environment variables for temporary overrides:
-
-```bash
-export LLM_PATCH_OPENAI_CHAT_COMPLETIONS='{"gpt-4.*":{"body":{"temperature":0.5}}}'
-```
-
-This takes precedence over client configuration patches but not model-specific patches.
-
-## Tips
-1. **Use model patches** for permanent, model-specific requirements
-2. **Use client patches** for personal preferences or environment-specific settings
-3. **Test regex patterns** carefully
-4. **Set to `null`** to remove parameters, don't just omit them
-5. **Check each model provider's docs** for available parameters and their formats
-6. **Be specific** with patterns to avoid unintended matches
-7. **Remember order matters** - first matching pattern wins for client patches
-8. **Patches merge** - both types can be applied, with client patches overriding model patches
-
-## Debugging Patches
-To see what request is actually being sent, enable debug logging:
-
-```bash
-export RUST_LOG=loki=debug
-loki "your prompt here"
-```
-
-This will show the final request body after all patches are applied.
diff --git a/docs/function-calling/BASH-PROMPT-HELPERS.md b/docs/function-calling/BASH-PROMPT-HELPERS.md
deleted file mode 100644
index bf0c891..0000000
--- a/docs/function-calling/BASH-PROMPT-HELPERS.md
+++ /dev/null
@@ -1,279 +0,0 @@
-# Bash Prompt Helpers
-
-When creating bash based tools, it's often helpful to prompt the user for input or confirmation.
-
-Loki comes pre-packaged with a handful of prompt helpers for your bash-based tools. These helpers
-can be used to prompt the user for various types of input, such as yes/no confirmations,
-text input, and selections from a list.
-
-The utility script is located at `functions/utils/prompt-utils.sh` within your Loki `functions` directory.
-
-The Loki `functions` directory varies between machines, so you can find its location on your system by running the following command in your terminal:
-
-```shell
-loki --info | grep functions_dir | awk '{print $2}'
-```
-
-## Quick Links
-
-- [Import The Prompt Utils Into Your Tools Script](#import-the-prompt-utils-into-your-tools-script)
-- [Included Utility Functions](#included-utility-functions)
- - [input](#input)
- - [confirm](#confirm)
- - [list](#list)
- - [checkbox](#checkbox)
- - [password](#password)
- - [editor](#editor)
- - [with_validate](#with_validate)
- - [validate_present](#validate_present)
- - [detect_os](#detect_os)
- - [get_opener](#get_opener)
- - [open_link](#open_link)
- - [guard_operation](#guard_operation)
- - [guard_path](#guard_path)
- - [patch_file](#patch_file)
- - [error](#error)
- - [warn](#warn)
- - [info](#info)
- - [debug](#debug)
- - [trace](#trace)
- - [Colored Output](#colored-output)
-
-
----
-
-## Import The Prompt Utils Into Your Tools Script
-In order to use the bash prompt helpers in your bash scripts, you need to source the provided `prompt-utils.sh` script.
-This script is pre-packaged with Loki and is located [here](../../assets/functions/utils/prompt-utils.sh).
-
-When sourcing the file in your bash script, you use the `LLM_PROMPT_UTILS_FILE` environment variable that automatically
-populates the `functions/utils/prompt-utils.sh` path for you.
-
-Thus, to properly source and enable all the bash prompt helpers in your Bash tools, add the following prelude to your
-scripts:
-
-```bash
-source "$LLM_PROMPT_UTILS_FILE"
-```
-
-## Included Utility Functions
-Below are the built-in bash prompt helpers that can be used to enhance user interaction with your tool scripts.
-
-### input
-Prompt for text input
-
-
-
-**Example With Validation:**
-```bash
-text=$(with_validation 'input "Please enter something:"' validate_present 2>/dev/tty)
-```
-
-**Example Without Validation:**
-```bash
-text=$(input "Please enter something:" 2>/dev/tty)
-```
-
-### confirm
-Show a confirm dialog with options for yes/no
-
-
-
-**Example:**
-```bash
-confirmed=$(confirm "Do the thing?" 2>/dev/tty)
-if [[ $confirmed == "0" ]]; then echo "No"; else echo "Yes"; fi
-```
-
-### list
-Renders a text based list of options that can be selected by the user using up, down, and enter
-keys that then returns the chosen option.
-
-
-
-**Example:**
-```bash
-options=("one" "two" "three" "four")
-choice=$(list "Select an item" "${options[@]}" 2>/dev/tty)
-echo "Your choice: ${options[$choice]}"
-```
-
-### checkbox
-Render a text based list of options, where multiple options can be selected by the user using down, up,
-and enter keys that then returns the chosen options.
-
-
-
-**Example:**
-```bash
-options=("one" "two" "three" "four")
-checked=$(checkbox "Select one or more items" "${options[@]}" 2>/dev/tty)
-echo "Your choices: ${checked}"
-```
-
-### password
-Show a password prompt displaying stars for each character typed.
-
-
-
-**Example With Validation:**
-```bash
-validate_password() {
- if [[ ${#1} -lt 10 ]]; then
- echo "Password must be at least 10 characters"
- exit 1
- fi
-}
-pass=$(with_validate 'password "Enter your password"' validate_password 2>/dev/tty)
-```
-
-**Example Without Validation:**
-```bash
-pass="$(password "Enter your password:" 2>/dev/tty)"
-```
-
-### editor
-Open the default editor (`$EDITOR`); if none is set, default back to `vi`
-
-**Example:**
-```bash
-text=$(editor "Please enter something in the editor" 2>/dev/tty)
-echo -e "You wrote:\n${text}"
-```
-
-### with_validate
-Evaluate the given prompt command with validation. This prompts the user for input until the
-validation functions returns 0.
-
-
-
-**Example:**
-```bash
-# Using the built-in 'validate_present' validator
-text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present 2>/dev/tty)
-
-# Using a custom validator; e.g. for password
-validate_password() {
- if [[ ${#1} -lt 10 ]]; then
- echo "Password needs to be at least 10 characters"
- exit 1
- fi
-}
-pass=$(with_validate 'password "Enter random password"' validate_password 2>/dev/tty)
-```
-
-### validate_present
-Validate that the prompt returned a value.
-
-
-
-**Example:**
-```bash
-text=$(with_validate 'input "Please enter something and confirm with enter"' validate_present 2>/dev/tty)
-```
-
-### detect_os
-Detect the current OS.
-
-Returns one of the following:
-
-* `solaris`
-* `macos`
-* `linux`
-* `bsd`
-* `windows`
-* `unknown`
-
-**Example:**
-```bash
-detect_os
-```
-
-### get_opener
-Determines the Os-specific file opening command (i.e. the command to open anything)
-
-**Example:**
-```bash
-# Returns 'xdg-open'
-get_opener
-```
-
-### open_link
-Opens the given link in the default browser
-
-**Example:**
-```bash
-open_link https://www.google.com
-```
-
-### guard_operation
-Prompt for permission to run an operation.
-
-Can be disabled by setting the environment variable `AUTO_CONFIRM`.
-
-**Example:**
-```bash
-guard_operation "Execute SQL?"
-_run_sql
-```
-
-### guard_path
-Prompt for permission to perform path operations.
-
-Can be disabled by setting the environment variable `AUTO_CONFIRM`.
-
-**Example:***
-```bash
-guard_path "$target_path" "Remove '$target_path'?"
-rm -rf "$target_path"
-```
-
-### patch_file
-Patch a file and show a diff using the default diff viewer. Uses git diff syntax.
-
-**Example:**
-```bash
-new_contents="$(patch_file "$path" file.patch)"
-```
-
-### error
-Log an error
-
-
-
-### warn
-Log a warning
-
-
-
-### info
-Log info
-
-
-
-### debug
-Log a debug message
-
-
-
-### trace
-Log a trace message
-
-
-
-### Colored Output
-The following commands allow users to output text in specific colors.
-
-* `red`
-* `green`
-* `gold`
-* `blue`
-* `magenta`
-* `cyan`
-* `white`
-
-**Example:**
-```bash
-red "This will be red"
-yellow "This will be yellow"
-```
diff --git a/docs/function-calling/CUSTOM-BASH-TOOLS.md b/docs/function-calling/CUSTOM-BASH-TOOLS.md
deleted file mode 100644
index 868d9c4..0000000
--- a/docs/function-calling/CUSTOM-BASH-TOOLS.md
+++ /dev/null
@@ -1,309 +0,0 @@
-# Custom Bash-Based Tools
-Loki supports tools written in Bash. However, they must be written in a special format with special annotations in order
-for Loki to be able to properly parse and utilize them. This formatting ensures that each Bash script is
-self-describing, and formatted in such a way that Loki can anticipate how to execute it and what parameters to pass to
-it. This standardization also lets Loki compile the script into a JSON schema that can be used to inform the LLM about
-how to use the tool.
-
-Each Bash-based tool must follow a specific structure in order for Loki to be able to properly compile and execute it:
-
-* The tool must be a Bash script with a `.sh` file extension.
-* The script must have the following comments:
- * `# @describe ...` comment at the top that describes the tool.
- * `# @env LLM_OUTPUT=/dev/stdout The output path` comment to describe the `LLM_OUTPUT` environment variable. This
- syntax in particular assigns `/dev/stdout` as the default value for `LLM_OUTPUT`, so that if it's not set by Loki,
- the script will still function properly.
- * `# @option --option An example option` comments to define each option that the tool accepts.
- * Use `--flag` syntax for boolean flags.
- * Use `--option ` syntax for options that accept a value.
- * Use `--option ` syntax for options that accept multiple values (i.e. arrays).
-* The script must have a `main` function
-* The `main` function must redirect the return value to the `>> "$LLM_OUTPUT"` environment variable.
- * This is necessary because Loki relies on the `$LLM_OUTPUT` environment variable to capture the output of the tool.
-
-Essentially, you can think of the Bash-based tool script as just a normal Bash script that uses special comments to
-define a CLI.
-* The `# @env LLM_OUTPUT=/dev/stdout` comment to define the `$LLM_OUTPUT` environment variable (good practice)
-* A `# @describe`
-* And a `main` function that writes to `$LLM_OUTPUT`
-
-The following section explains how you can add parameters to your bash functions and how to test out your scripts.
-
-## Quick Links:
-
-- [Loki Bash Tools Syntax](#loki-bash-tools-syntax)
- - [Metadata](#metadata)
- - [Environment Variables](#environment-variables)
- - [Arguments](#arguments)
- - [Flags](#flags)
- - [Options](#options)
- - [Subcommands (Agents only)](#subcommands-agents-only)
-- [Execute and Test Your Bash Tools](#execute-and-test-your-bash-tools)
- - [Example](#example)
-- [Prompt Helpers](#prompt-helpers)
-
-
----
-
-## Loki Bash Tools Syntax
-Loki Bash tools work via `@___` annotations that describe specific functionality of a script. The following reference
-explains the general syntax of these annotations and how to use them to create a CLI that Loki can recognize.
-
-Refer to the [Execute and Test Your Bash Tools](#execute-and-test-your-bash-tools) section to learn how to test out your Bash tools
-without needing to go through Loki itself.
-
-It's important to note that any functions prefixed with `_` are not sent to the LLM, so they will be invisible to the
-LLM at runtime.
-
-### Metadata:
-You can define different metadata about your script to help Loki understand its dependencies and purpose.
-
-```bash
-# Use the `@meta require-tools` annotation to specify any external tools that your script depends on.
-# @meta require-tools jq,yq
-
-# Use the `@describe` annotation to describe the purpose of the script.
-# @describe A tool to interact with things
-```
-
-### Environment Variables:
-```bash
-###########################
-## Environment Variables ##
-###########################
-
-# Use `@env` to define environment variables that the script uses.
-# @env LLM_OUTPUT=/dev/stdout The output path, with a default value of '/dev/stdout' if not set.
-# @env OPTIONAL An optional environment variable
-# @env REQUIRED! A required environment variable
-# @env DEFAULT_VALUE=default An environment variable with a default value if unset.
-# @env DEFAULT_FROM_FN=`_default_env_fn` An environment variable with a default value calculated from a function if unset.
-# @env CHOICE[even|odd] An environment variable that, if set, must be set to either `even` or `odd`
-# @env CHOICE_WITH_DEFAULT[=even|odd] An environment variable that, if set, must be set to either `even` or `odd`, and defaults to `even` when unset
-# @env CHOICE_FROM_FN[`_choice_env_fn`] An environment variable that, if set, must be set to one of the values returned by the `_choice_fn` function.
-
-# Example variable usage:
-export CHOICE=even
-# ./script.sh
-main() {
- [[ $CHOICE == "even" ]] || { echo "The value of the 'CHOICE' env var is not 'even'" >> "$LLM_OUTPUT" && exit 1 }
-}
-
-# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
-_default_env_fn() {
- echo "calculated default env value"
-}
-
-# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
-_choice_env_fn() {
- echo even
- echo odd
-}
-```
-
-### Arguments:
-When referencing an argument defined via the `@arg` annotation, you can access its value using the `argc_` variable that
-is created at runtime.
-
-```bash
-###############
-## Arguments ##
-###############
-
-# Use `@arg` To define positional arguments for your script.
-# To reference an argument within your script, use the `argc_` variable.
-# @arg optional Optional argument
-# @arg required! Required argument
-# @arg multi_value* An argument that accepts multiple values (e.g. './script.sh one two three')
-# @arg multi_value_required+ An argument that is required and accepts multiple values
-# @arg value_notated An argument that explicitly specifies the name for documentation (e.g. Usage: ./script.sh [VALUE])
-# @arg default=default An argument with a default value if unset
-# @arg default_from_fn=`_default_arg_fn` An argument with a default value calculated from a function if unset
-# @arg choice[even|odd] An argument that, if set, must be set to either `even` or `odd`
-# @arg required_choice+[even|odd] An required argument that must be set to either `even` or `odd`
-# @arg default_choice[=even|odd] An argument that if unset defaults to 'even', but if set must be either `even` or `odd`
-# @arg multi_value_choice*[even|odd] An argument that, if set, must be set to either `even` or `odd`, and accepts multiple values
-# @arg choice_fn[`_choice_arg_fn`] An argument that, if set, must be set to one of the values returned by the `_choice_arg_fn` function.
-# @arg choice_fn_no_valid[?`_choice_arg_fn`] An argument that, if set, can be set to one of the values returned by the `_choice_arg_fn` function,
-# but does not validate the value.
-# @arg multi_choice_fn*[`_choice_arg_fn`] An argument that, if set, must be set to one of the values returned by the `_choice_arg_fn` function,
-# and accepts multiple values.
-# @arg multi_choice_comma_fn*,[`_choice_arg_fn`] An argument that, if set, must be set to one of the values returned by the `_choice_arg_fn` function,
-# and accepts multiple values in the form of a comma-separated list
-# @arg capture_arg~ An argument that captures all remaining args passed to the script
-
-# Example usage 1: ./script.sh something_required
-main() {
- [[ $argc_required == "something_required" ]] || { echo "The value of the 'required' arg is not 'something_required'" >> "$LLM_OUTPUT" && exit 1 }
-}
-
-# Example usage 2: ./script.sh this is a test
-main() {
- [[ "${argc_multi_value[*]}" == "this is a test" ]] || { echo "The value of the 'multi_value' arg is not 'this is a test'" >> "$LLM_OUTPUT" && exit 1 }
-}
-
-
-# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
-_default_arg_fn() {
- echo "default arg value"
-}
-
-# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
-_choice_arg_fn() {
- echo even
- echo odd
-}
-```
-
-### Flags:
-To access the value of a flag defined via the `@flag` annotation, you can check the value of the `argc_` variable.
-
-```bash
-###########
-## Flags ##
-###########
-
-# Use `@flag` to define boolean flags for your script
-# To reference a flag within your script, use the `argc_` variable.
-# @flag --bool A boolean flag with only a long option
-# @flag -b --bool A boolean flag with a short and long option
-# @flag -b A boolean flag with only a short option
-# @flag --multi* A boolean flag that can be used multiple times (e.g. '--multi --multi' will return '2')
-
-# Example usage 1: ./script.sh --bool
-main() {
- [[ $argc_bool == "1" ]] || { echo "The value of the 'bool' flag is not '1'" >> "$LLM_OUTPUT" && exit 1 }
-}
-
-# Example usage 2: ./script.sh --multi --multi
-main() {
- [[ $argc_multi == "2" ]] || { echo "The value of the 'multi' flag is not 2" >> "$LLM_OUTPUT" && exit 1 }
-}
-```
-
-### Options:
-To access the value of an option defined via the `@option` annotation, you can check the value of the `argc_` variable.
-
-```bash
-#############
-## Options ##
-#############
-
-# Use `@option` to define flags that accept values
-# To reference an option within your script, use the `argc_` variable.
-# @option --option An option that accepts a value with only a long flag
-# @option -o --option An option that accepts a value with both a short and long flag
-# @option -o An option that accepts a value with only a short flag
-# @option --required A required option that accepts a value
-# @option --multi* An option that accepts multiple values
-# @option --required-multi+ An option that accepts multiple values and is required
-# @option --multi-comma*, An option that accepts multiple values in the form of a comma-separated list
-# @option --value An option that explicitly specifies the name for documentation (e.g. Usage: ./script.sh --value [VALUE])
-# @option --two-args An option that accepts two arguments and explicitly names them for documentation
-# (e.g. Usage: ./script.sh --two-args [SRC] [DEST])
-# @option --unlimited-args An option that accepts an unlimited number of arguments and explicitly names them for documentation
-# (e.g. Usage: ./script.sh --unlimited-args [SRC] [DEST ...])
-# @option --default=default An option that has a default value if unset
-# @option --default-from-fn=`_default_opt_fn` An option that has a default value calculated from a function if unset
-# @option --choice[even|odd] An option that, if set, must be set to either `even` or `odd`
-# @option --choice-default[=even|odd] An option that, if unset, defaults to `even`, but if set must be either `even` or `odd`
-# @option --choice-multi*[even|odd] An option that, if set, must be set to either `even` or `odd`, and can be specified multiple times
-# (e.g. ./script.sh --choice-multi even --choice-multi odd)
-# @option --required-choice-multi+[even|odd] A required option that, must be set to either `even` or `odd`, and can be specified multiple times
-# @option --choice-fn[`_choice_opt_fn`] An option that, if set, must be set to one of the values returned by the `_choice_opt_fn` function.`
-# @option --choice-fn-no-valid[?`_choice_opt_fn`] An option that, if set, can be set to one of the values returned by the `_choice_opt_fn` function, with no validation
-# @option --choice-multi-fn*[`_choice_opt_fn`] An option that, if set, must be set to one of the values returned by the `_choice_opt_fn` function,
-# and can be specified multiple times
-# @option --choice-multi-comma*,[`_choice_opt_fn`] An option that, if set, must be set to one of the values returned by the `_choice_opt_fn` function,
-# and is specified as a comma-separated list
-# @option --capture~ An option that captures all remaining arguments passed to the script
-
-# Example usage 1: ./script.sh --option some_value
-main() {
- [[ $argc_option == "some_value" ]] || { echo "The value of the 'option' option is not 'some_value'" >> "$LLM_OUTPUT" && exit 1 }
-}
-
-# Example usage 2: ./script.sh --multi value1 --multi value2
-main() {
- [[ "${argc_multi[*]}" == "value1 value2" ]] || { echo "The value of the 'multi' option is not 'value1 value2'" >> "$LLM_OUTPUT" && exit 1 }
-}
-
-
-# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
-_default_opt_fn() {
- echo "calculated default option value"
-}
-
-# Loki does not pass functions prefixed with `_` to the LLM, so these are essentially `private` functions
-_choice_opt_fn() {
- echo even
- echo odd
-}
-```
-
-### Subcommands (Agents only):
-By default, if no `@cmd` annotations are defined, the script's `main` function is treated as the default command.
-However, for agents, there can be many functions defined in one file, and thus it is useful to create subcommands
-to organize your agent's tools.
-
-```bash
-#################
-## Subcommands ##
-#################
-
-# Use the `@cmd` annotation to define subcommands for your script.
-# @cmd List all files
-list() {
- ls -la >> "$LLM_OUTPUT"
-}
-
-# @cmd Output the contents of the specified file
-# @arg file! The file to output
-cat() {
- cat "$argc_file" >> "$LLM_OUTPUT"
-}
-
-# Example usage 1: ./script.sh cat myfile.txt
-```
-
-## Execute and Test Your Bash Tools
-Your bash tools are just normal bash scripts stored in the `functions/tools` directory. So you can execute and test them
-directly by first having Loki compile them so all this syntactic sugar means something.
-
-This is achieved via the `loki --build-tools` command.
-
-### Example
-Suppose we want to execute the `functions/tools/get_current_time.sh` script for testing.
-
-We'd first make sure the script is visible in all contexts by ensuring it's in the `visible_tools` array in your global
-`config.yaml` file. This ensures Loki builds the tool so it's ready to use in any context.
-
-You can find the location of your global `config.yaml` file with the following command:
-
-```shell
-loki --info | grep 'config_file' | awk '{print $2}'
-```
-
-Then, we can instruct Loki to build the script so we can test it out:
-
-```shell
-loki --build-tools
-```
-
-This will add additional boilerplate to the top of the script so that it can be executed directly.
-
-Finally, we can now execute the script:
-
-```bash
-$ ./get_current_time.sh
-Fri Oct 24 05:55:04 PM MDT 2025
-```
-
-## Prompt Helpers
-It's often useful to create interactive prompts for our bash tools so that our tools can get input from
-users.
-
-To accommodate this, Loki provides a set of prompt helper functions that can be referenced and used within your Bash
-tools.
-
-For more information, refer to the [Bash Prompt Helpers documentation](BASH-PROMPT-HELPERS.md).
diff --git a/docs/function-calling/CUSTOM-TOOLS.md b/docs/function-calling/CUSTOM-TOOLS.md
deleted file mode 100644
index 1de68cc..0000000
--- a/docs/function-calling/CUSTOM-TOOLS.md
+++ /dev/null
@@ -1,282 +0,0 @@
-# Custom Tools
-Loki is designed to be as flexible and as customizable as possible. One of the key
-features that enables this flexibility is the ability to create and integrate custom tools
-into your Loki setup. This document provides a guide on how to create and use custom tools within Loki.
-
-## Quick Links
-
-- [Supported Languages](#supported-languages)
-- [Creating a Custom Tool](#creating-a-custom-tool)
- - [Environment Variables](#environment-variables)
- - [Custom Bash-Based Tools](#custom-bash-based-tools)
- - [Custom Python-Based Tools](#custom-python-based-tools)
- - [Custom TypeScript-Based Tools](#custom-typescript-based-tools)
-- [Custom Runtime](#custom-runtime)
-
-
----
-
-## Supported Languages
-Loki supports custom tools written in the following programming languages:
-
-* Python
-* Bash
-* TypeScript
-
-## Creating a Custom Tool
-All tools are created as scripts in either Python, Bash, or TypeScript. They should be placed in the `functions/tools` directory.
-The location of the `functions` directory varies between systems, so you can use the following command to locate
-your `functions` directory:
-
-```shell
-loki --info | grep functions_dir | awk '{print $2}'
-```
-
-Once you've created your custom tool, remember to add it to the `visible_tools` array in your global `config.yaml` file
-to enable it globally. See the [Tools](TOOLS.md#enablingdisabling-global-tools) documentation for more information on how Loki utilizes the
-`visible_tools` array.
-
-### Environment Variables
-All tools have access to the following environment variables that provide context about the current execution environment:
-
-| Variable | Description |
-|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
-| `LLM_OUTPUT` | Indicates where the output of the tool should go.
In certain situations, this may be set to a temporary file instead of `/dev/stdout`. |
-| `LLM_ROOT_DIR` | The root `config_dir` directory for Loki
(i.e. `dirname $(loki --info \| grep config_file \| awk '{print $2}')`) |
-| `LLM_TOOL_NAME` | The name of the tool being executed |
-| `LLM_TOOL_CACHE_DIR` | A directory specific to the tool for storing cache or temporary files |
-
-Loki also searches the tools directory on startup for a `.env` file. If found, all tools in `functions/tools/` will have
-the environment variables defined in the `.env` file available to them.
-
-### Custom Bash-Based Tools
-To create a Bash-based tool, refer to the [custom bash tools documentation](CUSTOM-BASH-TOOLS.md).
-
-### Custom Python-Based Tools
-Loki supports tools written in Python.
-
-Each Python-based tool must follow a specific structure in order for Loki to be able to properly compile and
-execute it:
-
-* The tool must be a Python script with a `.py` file extension.
-* The tool must have a `def run` function that serves as the entry point for the tool.
-* The `run` function must accept parameters that define the inputs for the tool.
- * Always use type hints to specify the data type of each parameter.
- * Use `Optional[...]` to indicate optional parameters
-* The `run` function must return a `str`.
- * For Python, this is automatically written to the `LLM_OUTPUT` environment variable, so there's no need to explicitly
- write to the environment variable within the function.
-* The function must also have a docstring that describes the tool and its parameters.
- * Each parameter in the `run` function should be documented in the docstring using the `Args:` section. They should use the following format:
- * `: ` Where
- * ``: The name of the parameter
- * ``: The description of the parameter
- * These are *very* important because these descriptions are what's passed to the LLM as the description of the tool,
- letting the LLM know what the tool does and how to use it.
-
-It's important to note that any functions prefixed with `_` are not sent to the LLM, so they will be invisible to the LLM
-at runtime.
-
-Below is the [`demo_py.py`](../../assets/functions/tools/demo_py.py) tool definition that comes pre-packaged with
-Loki and demonstrates how to create a Python-based tool:
-
-```python
-import os
-from typing import List, Literal, Optional
-
-
-def run(
- string: str,
- string_enum: Literal["foo", "bar"],
- boolean: bool,
- integer: int,
- number: float,
- array: List[str],
- string_optional: Optional[str] = None,
- integer_with_default: int = 42,
- boolean_with_default: bool = True,
- number_with_default: float = 3.14,
- string_with_default: str = "hello",
- array_optional: Optional[List[str]] = None,
-):
- """Demonstrates all supported Python parameter types and variations.
- Args:
- string: A required string property
- string_enum: A required string property constrained to specific values
- boolean: A required boolean property
- integer: A required integer property
- number: A required number (float) property
- array: A required string array property
- string_optional: An optional string property (Optional[str] with None default)
- integer_with_default: An optional integer with a non-None default value
- boolean_with_default: An optional boolean with a default value
- number_with_default: An optional number with a default value
- string_with_default: An optional string with a default value
- array_optional: An optional string array property
- """
- output = f"""string: {string}
-string_enum: {string_enum}
-boolean: {boolean}
-integer: {integer}
-number: {number}
-array: {array}
-string_optional: {string_optional}
-integer_with_default: {integer_with_default}
-boolean_with_default: {boolean_with_default}
-number_with_default: {number_with_default}
-string_with_default: {string_with_default}
-array_optional: {array_optional}"""
-
- for key, value in os.environ.items():
- if key.startswith("LLM_"):
- output = f"{output}\n{key}: {value}"
-
- return output
-```
-
-### Custom TypeScript-Based Tools
-Loki supports tools written in TypeScript. TypeScript tools require [Node.js](https://nodejs.org/) and
-[tsx](https://tsx.is/) (`npx tsx` is used as the default runtime).
-
-Each TypeScript-based tool must follow a specific structure in order for Loki to properly compile and execute it:
-
-* The tool must be a TypeScript file with a `.ts` file extension.
-* The tool must have an `export function run(...)` that serves as the entry point for the tool.
- * Non-exported functions are ignored by the compiler and can be used as private helpers.
-* The `run` function must accept flat parameters that define the inputs for the tool.
- * Always use type annotations to specify the data type of each parameter.
- * Use `param?: type` or `type | null` to indicate optional parameters.
- * Use `param: type = value` for parameters with default values.
-* The `run` function must return a `string` (or `Promise` for async functions).
- * For TypeScript, the return value is automatically written to the `LLM_OUTPUT` environment variable, so there's
- no need to explicitly write to the environment variable within the function.
-* The function must have a JSDoc comment that describes the tool and its parameters.
- * Each parameter should be documented using `@param name - description` tags.
- * These descriptions are passed to the LLM as the tool description, letting the LLM know what the tool does and
- how to use it.
-* Async functions (`export async function run(...)`) are fully supported and handled transparently.
-
-**Supported Parameter Types:**
-
-| TypeScript Type | JSON Schema | Notes |
-|-------------------|--------------------------------------------------|-----------------------------|
-| `string` | `{"type": "string"}` | Required string |
-| `number` | `{"type": "number"}` | Required number |
-| `boolean` | `{"type": "boolean"}` | Required boolean |
-| `string[]` | `{"type": "array", "items": {"type": "string"}}` | Array (bracket syntax) |
-| `Array` | `{"type": "array", "items": {"type": "string"}}` | Array (generic syntax) |
-| `"foo" \| "bar"` | `{"type": "string", "enum": ["foo", "bar"]}` | String enum (literal union) |
-| `param?: string` | `{"type": "string"}` (not required) | Optional via question mark |
-| `string \| null` | `{"type": "string"}` (not required) | Optional via null union |
-| `param = "value"` | `{"type": "string"}` (not required) | Optional via default value |
-
-**Unsupported Patterns (will produce a compile error):**
-
-* Rest parameters (`...args: string[]`)
-* Destructured object parameters (`{ a, b }: { a: string, b: string }`)
-* Arrow functions (`const run = (x: string) => ...`)
-* Function expressions (`const run = function(x: string) { ... }`)
-
-Only `export function` declarations are recognized. Non-exported functions are invisible to the compiler.
-
-Below is the [`demo_ts.ts`](../../assets/functions/tools/demo_ts.ts) tool definition that comes pre-packaged with
-Loki and demonstrates how to create a TypeScript-based tool:
-
-```typescript
-/**
- * Demonstrates all supported TypeScript parameter types and variations.
- *
- * @param string - A required string property
- * @param string_enum - A required string property constrained to specific values
- * @param boolean - A required boolean property
- * @param number - A required number property
- * @param array_bracket - A required string array using bracket syntax
- * @param array_generic - A required string array using generic syntax
- * @param string_optional - An optional string using the question mark syntax
- * @param string_nullable - An optional string using the union-with-null syntax
- * @param number_with_default - An optional number with a default value
- * @param boolean_with_default - An optional boolean with a default value
- * @param string_with_default - An optional string with a default value
- * @param array_optional - An optional string array using the question mark syntax
- */
-export function run(
- string: string,
- string_enum: "foo" | "bar",
- boolean: boolean,
- number: number,
- array_bracket: string[],
- array_generic: Array,
- string_optional?: string,
- string_nullable: string | null = null,
- number_with_default: number = 42,
- boolean_with_default: boolean = true,
- string_with_default: string = "hello",
- array_optional?: string[],
-): string {
- const parts = [
- `string: ${string}`,
- `string_enum: ${string_enum}`,
- `boolean: ${boolean}`,
- `number: ${number}`,
- `array_bracket: ${JSON.stringify(array_bracket)}`,
- `array_generic: ${JSON.stringify(array_generic)}`,
- `string_optional: ${string_optional}`,
- `string_nullable: ${string_nullable}`,
- `number_with_default: ${number_with_default}`,
- `boolean_with_default: ${boolean_with_default}`,
- `string_with_default: ${string_with_default}`,
- `array_optional: ${JSON.stringify(array_optional)}`,
- ];
-
- for (const [key, value] of Object.entries(process.env)) {
- if (key.startsWith("LLM_")) {
- parts.push(`${key}: ${value}`);
- }
- }
-
- return parts.join("\n");
-}
-```
-
-## Custom Runtime
-By default, Loki uses the following runtimes to execute tools:
-
-| Language | Default Runtime | Requirement |
-|------------|-----------------|--------------------------------|
-| Python | `python` | Python 3 on `$PATH` |
-| TypeScript | `npx tsx` | Node.js + tsx (`npm i -g tsx`) |
-| Bash | `bash` | Bash on `$PATH` |
-
-You can override the runtime for Python and TypeScript tools using a **shebang line** (`#!`) at the top of your
-script. Loki reads the first line of each tool file; if it starts with `#!`, the specified interpreter is used instead
-of the default.
-
-**Examples:**
-
-```python
-#!/usr/bin/env python3.11
-# This Python tool will be executed with python3.11 instead of the default `python`
-
-def run(name: str):
- """Greet someone.
- Args:
- name: The name to greet
- """
- return f"Hello, {name}!"
-```
-
-```typescript
-#!/usr/bin/env bun
-// This TypeScript tool will be executed with Bun instead of the default `npx tsx`
-
-/**
- * Greet someone.
- * @param name - The name to greet
- */
-export function run(name: string): string {
- return `Hello, ${name}!`;
-}
-```
-
-This is useful for pinning a specific Python version, using an alternative TypeScript runtime like
-[Bun](https://bun.sh/) or [Deno](https://deno.com/), or working with virtual environments.
diff --git a/docs/function-calling/MCP-SERVERS.md b/docs/function-calling/MCP-SERVERS.md
deleted file mode 100644
index 56a3121..0000000
--- a/docs/function-calling/MCP-SERVERS.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# MCP Servers
-[MCP servers](https://modelcontextprotocol.io/docs/getting-started/intro) are essentially APIs designed specifically for LLMs that work like a remote repository of
-tools for the model to access and extend its capabilities.
-
-So think of it like this: Instead of having to write all your own custom tools to interact with different
-services, those services can expose their functionality through an MCP server.
-
-Loki has first-class support for MCP servers.
-
-As mentioned in the [Loki Vault documentation](../VAULT.md), Loki can inject sensitive
-configuration data into your MCP configuration file to ensure that secrets are not hard-coded.
-
-## Quick Links
-
-- [Important Note](#important-note)
-- [MCP Server Configuration](#mcp-server-configuration)
- - [Secret Injection](#secret-injection)
-- [Default MCP Servers](#default-mcp-servers)
-- [Loki Configuration](#loki-configuration)
- - [Global Configuration](#global-configuration)
- - [Role Configuration](#role-configuration)
- - [Agent Configuration](#agent-configuration)
-
-
----
-
-## Important Note
-Be careful how many MCP servers you enable at one time, regardless of the context. When there is a significant
-number of configured MCP servers, enabling too many MCP servers may overwhelm the context length of a model,
-and quickly exceed token limits.
-
-## MCP Server Configuration
-Loki stores the MCP server configuration file, `functions/mcp.json`, in the `functions` directory. You can find
-this directory using the following command:
-
-```shell
-loki --info | grep functions_dir | awk '{print $2}'
-```
-
-The syntax for the `functions/mcp.json` file is identical to the syntax for MCP server configurations for Claude Desktop.
-So any time you're looking to add a new server, look at the docs for it and find the configuration example for
-Claude desktop. You should be able to use the exact same configuration in your `functions/mcp.json` file.
-
-### Secret Injection
-As mentioned in the [Loki Vault documentation](../VAULT.md), you can use Loki Vault to inject secrets into your MCP configuration file.
-
-In fact, this is why you need to set up your vault before using Loki at all: the built-in MCP configuration
-requires you set up some secrets to use it.
-
-For more information about how to set up your vault and inject secrets, please refer to the [Loki Vault documentation](../VAULT.md).
-
-## Default MCP Servers
-Loki ships with a `functions/mcp.json` file that includes some useful MCP servers:
-
-* [github](https://github.com/github/github-mcp-server) - Interact with GitHub repositories, issues, pull requests, and more.
-* [docker](https://github.com/ckreiling/mcp-server-docker) - Manage your local Docker containers with natural language
-* [slack](https://github.com/korotovsky/slack-mcp-server) - Interact with Slack
-* [ddg-search](https://github.com/nickclyde/duckduckgo-mcp-server) - Perform web searches with the DuckDuckGo search engine
-
-## Loki Configuration
-MCP servers, like tools, can be used in a handful of contexts:
-* Inside a session
-* Inside a role
-* Inside an agent
-* Globally (i.e. outside a session, role, or agent)
-
-Each of these has a different configuration and interaction with the global configuration.
-
-***Note:** The names of each MCP server referenced in the below configuration properties directly corresponds
-to the names given in the `functions/mcp.json` configuration file. So if you change the name of an MCP server
-from `slack` to `lucem-slack`, then you need to also update your Loki configuration accordingly.
-
-### Global Configuration
-The global configuration is essentially what settings you want to have on by default when
-you just invoke `loki`. (Don't worry about agents, roles, or sessions yet. We'll get to them in a bit).
-
-The following settings are available in the global configuration for MCP servers:
-
-```yaml
-mcp_server_support: true # Enables or disables MCP server support (globally).
-mapping_mcp_servers: # Alias for an MCP server or set of servers
- git: github,gitmcp
-enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack')
-```
-
-A special note about `enabled_mcp_servers`: a user can set this to `all` to enable all configured MCP servers in the
-`functions/mcp.json` configuration.
-
-(See the [Configuration Example](../../config.example.yaml) file for an example global configuration with all options.)
-
-When running in REPL-mode, the `mcp_server_support` and `enabled_mcp_servers` settings can be overridden using the
-`.set` command:
-
-
-
-### Role Configuration
-When you create a role, you have the following MCP-related configuration options available to you:
-
-```yaml
-enabled_mcp_servers: github # Which MCP servers the role uses.
-```
-
-The values for `mapping_mcp_servers` are inherited from the `[global configuration](#global-configuration)`.
-
-For more information about roles, refer to the [Roles](../ROLES.md) documentation.
-
-### Agent Configuration
-When you create an agent, you have the following MCP-related configuration options available to you:
-
-```yaml
-mcp_servers: # Which MCP servers the agent uses
- - github
- - docker
-```
-
-The values for `mapping_mcp_servers` are inherited from the [global configuration](#global-configuration).
-
-For more information about agents, refer to the [Agents](../AGENTS.md) documentation.
-
-For a full example configuration for an agent, see the [Agent Configuration Example](../../config.agent.example.yaml) file.
diff --git a/docs/function-calling/TOOLS.md b/docs/function-calling/TOOLS.md
deleted file mode 100644
index 80bf392..0000000
--- a/docs/function-calling/TOOLS.md
+++ /dev/null
@@ -1,192 +0,0 @@
-# Tools
-Loki supports function calling with various tools built-in to enhance LLM capabilities. All built-in tools for Loki
-are located in the [`functions/tools`](../../assets/functions/tools) directory. These tools are also stored in your Loki `functions`
-directory, which is also where you'd go to add more tools.
-
-**Pro Tip:** The Loki functions directory can be found by running the following command:
-```bash
-loki --info | grep functions_dir | awk '{print $2}'
-```
-
-# Quick Links
-
-- [Built-In Tools](#built-in-tools)
-- [Configuration](#configuration)
- - [Global Configuration](#global-configuration)
- - [Enabling/Disabling Global Tools](#enablingdisabling-global-tools)
- - [Role Configuration](#role-configuration)
- - [Agent Configuration](#agent-configuration)
-- [Tool Error Handling](#tool-error-handling)
- - [Native/Shell Tool Errors](#nativeshell-tool-errors)
- - [MCP Errors](#mcp-tool-errors)
- - [Why Tool Error Handling Is Important](#why-this-matters)
-
-
----
-
-## Built-In Tools
-The following tools are built-in to Loki by default, and their default enabled/disabled status is indicated. More about how tools can
-be enabled/disabled can be found in the [Configuration](#configuration) section below.
-
-| Tool | Description | Enabled/Disabled |
-|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|
-| [`demo_py.py`](../../assets/functions/tools/demo_py.py) | Demonstrates how to create a tool using Python and how to use comments. | 🔴 |
-| [`demo_sh.sh`](../../assets/functions/tools/demo_sh.sh) | Demonstrate how to create a tool using Bash and how to use comment tags. | 🔴 |
-| [`demo_ts.ts`](../../assets/functions/tools/demo_ts.ts) | Demonstrates how to create a tool using TypeScript and how to use JSDoc comments. | 🔴 |
-| [`execute_command.sh`](../../assets/functions/tools/execute_command.sh) | Execute the shell command. | 🟢 |
-| [`execute_py_code.py`](../../assets/functions/tools/execute_py_code.py) | Execute the given Python code. | 🔴 |
-| [`execute_sql_code.sh`](../../assets/functions/tools/execute_sql_code.sh) | Execute SQL code. | 🔴 |
-| [`fetch_url_via_curl.sh`](../../assets/functions/tools/fetch_url_via_curl.sh) | Extract the content from a given URL using cURL. | 🔴 |
-| [`fetch_url_via_jina.sh`](../../assets/functions/tools/fetch_url_via_jina.sh) | Extract the content from a given URL using Jina. | 🔴 |
-| [`fs_cat.sh`](../../assets/functions/tools/fs_cat.sh) | Read the contents of a file at the specified path. | 🟢 |
-| [`fs_read.sh`](../../assets/functions/tools/fs_read.sh) | Controlled reading of the contents of a file at the specified path with line numbers, offset, and limit to read specific sections. | 🟢 |
-| [`fs_glob.sh`](../../assets/functions/tools/fs_glob.sh) | Find files by glob pattern. Returns matching file paths sorted by modification time. | 🟢 |
-| [`fs_grep.sh`](../../assets/functions/tools/fs_grep.sh) | Search file contents using regular expressions. Returns matching file paths and lines. | 🟢 |
-| [`fs_ls.sh`](../../assets/functions/tools/fs_ls.sh) | List all files and directories at the specified path. | 🟢 |
-| [`fs_mkdir.sh`](../../assets/functions/tools/fs_mkdir.sh) | Create a new directory at the specified path. | 🔴 |
-| [`fs_patch.sh`](../../assets/functions/tools/fs_patch.sh) | Apply a patch to a file at the specified path.
This can be used to edit a file without having to rewrite the whole file. | 🔴 |
-| [`fs_rm.sh`](../../assets/functions/tools/fs_rm.sh) | Remove a file or directory at the specified path. | 🔴 |
-| [`fs_write.sh`](../../assets/functions/tools/fs_write.sh) | Write the full file contents to a file at the specified path. | 🟢 |
-| [`get_current_time.sh`](../../assets/functions/tools/get_current_time.sh) | Get the current time. | 🟢 |
-| [`get_current_weather.py`](../../assets/functions/tools/get_current_weather.py) | Get the current weather in a given location (Python implementation) | 🔴 |
-| [`get_current_weather.sh`](../../assets/functions/tools/get_current_weather.sh) | Get the current weather in a given location. | 🟢 |
-| [`get_current_weather.ts`](../../assets/functions/tools/get_current_weather.ts) | Get the current weather in a given location (TypeScript implementation) | 🔴 |
-| [`query_jira_issues.sh`](../../assets/functions/tools/query_jira_issues.sh) | Query for jira issues using a Jira Query Language (JQL) query. | 🟢 |
-| [`search_arxiv.sh`](../../assets/functions/tools/search_arxiv.sh) | Search arXiv using the given search query and return the top papers. | 🔴 |
-| [`search_wikipedia.sh`](../../assets/functions/tools/search_wikipedia.sh) | Search Wikipedia using the given search query.
Use it to get detailed information about a public figure, interpretation of a
complex scientific concept or in-depth connectivity of a significant historical
event, etc. | 🔴 |
-| [`search_wolframalpha.sh`](../../assets/functions/tools/search_wolframalpha.sh) | Get an answer to a question using Wolfram Alpha. The input query should be
in English. Use it to answer user questions that require computation, detailed
facts, data analysis, or complex queries. | 🔴 |
-| [`send_mail.sh`](../../assets/functions/tools/send_mail.sh) | Send an email. | 🔴 |
-| [`send_twilio.sh`](../../assets/functions/tools/send_twilio.sh) | Send SMS or Twilio Messaging Channels messages using the Twilio API. | 🔴 |
-| [`web_search_loki.sh`](../../assets/functions/tools/web_search_loki.sh) | Perform a web search to get up-to-date information or additional context.
Use this when you need current information or feel a search could provide
a better answer. | 🔴 |
-| [`web_search_perplexity.sh`](../../assets/functions/tools/web_search_perplexity.sh) | Perform a web search using the Perplexity API to get up-to-date
information or additional context. Use this when you need current
information or feel a search could provide a better answer. | 🔴 |
-| [`web_search_tavily.sh`](../../assets/functions/tools/web_search_tavily.sh) | Perform a web search using the Tavily API to get up-to-date
information or additional context. Use this when you need current
information or feel a search could provide a better answer. | 🔴 |
-
-Details on what configuration, if any, is necessary for each tool can be found inside the tool file definition itself.
-
-## Configuration
-Tools can be used in a handful of contexts:
-* Inside a session
-* Inside a role
-* Inside an agent
-* Globally (i.e. outside a session, role, or agent)
-
-Each of these has a different configuration and interaction with the global configuration.
-
-**Note:** For each configuration property listed below, the functions that are mentioned *only*
-correspond to the tool scripts located in your Loki `functions/tools` directory.
-
-### Global Configuration
-The global configuration is essentially what settings you want to have on by default when
-you just invoke `loki`. (Don't worry about agents, roles, or sessions yet. We'll get to them in a bit).
-
-The following settings are available in the global configuration for tools:
-
-```yaml
-function_calling_support: true # Enables or disables function calling in any context
-mapping_tools: # Alias for a tool or toolset
- fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write'
-enabled_tools: null # Which tools to use by default. (e.g. 'fs,web_search_loki')
-visible_tools: # Which tools are visible to be compiled (and are thus able to be defined in 'enabled_tools')
- # - demo_py.py
- - execute_command.sh
-```
-
-A special not about `enabled_tools`: a user can set this to `all` to enable all available tools listed in the
-`visible_tools` section of your Loki `config.yaml` file.
-See the [Enabling/Disabling Global Tools](#enablingdisabling-global-tools) section below for more information on how tools
-are globally enabled/disabled globally.
-
-(See the [Configuration Example](../../config.example.yaml) file for an example global configuration with all options.)
-
-When running in REPL-mode, the `function_calling_support` and `enabled_tools` settings can be overridden using the
-`.set` command:
-
-
-
-You'll notice that mentioned above, some tools are disabled while others are enabled. How is that determined?
-
-### Enabling/Disabling Global Tools
-The configured tools are enabled/disabled by looking at the values in the `visible_tools` array in your `config.yaml`
-file. This file is located in the root of the Loki `config` directory. The location of the Loki config varies by system,
-so your config file can be found using the following command:
-
-```bash
-loki --info | grep 'config_file' | awk '{print $2}'
-```
-
-Each line in the `visible_tools` array lists a tool.
-
-If that line is commented out, then that tool is not included in the global tool set, and cannot be used in any context;
-This means it will not be built, and even if enabled under `enabled_tools`, it still will not be available in any
-context.
-
-### Role Configuration
-When you create a role, you have the following global tool-related configuration options available to you:
-
-```yaml
-enabled_tools: query_jira_issues # Which tools the role uses.
-```
-
-The values for `mapping_tools` are inherited from the [global configuration](#global-configuration).
-
-For more information about roles, refer to the [Roles](../ROLES.md) documentation.
-
-### Agent Configuration
-When you create an agent, you have the following global tool-related configuration options available to you:
-
-```yaml
-global_tools: # Which global tools the agent uses
- - query_jira_issues.sh
- - fs_cat.sh
- - fs_ls.sh
-```
-
-The values for `mapping_tools` are inherited from the [global configuration](#global-configuration).
-
-For more information about agents, refer to the [Agents](../AGENTS.md) documentation.
-
-For a full example configuration for an agent, see the [Agent Configuration Example](../../config.agent.example.yaml) file.
-
----
-
-## Tool Error Handling
-When tools fail, Loki captures error information and passes it back to the model so it can diagnose issues and
-potentially retry or adjust its approach.
-
-### Native/Shell Tool Errors
-When a shell-based tool exits with a non-zero exit code, the model receives:
-
-```json
-{
- "tool_call_error": "Tool call 'my_tool' exited with code 1",
- "stderr": "Error: file not found: config.json"
-}
-```
-
-The `stderr` field contains the actual error output from the tool, giving the model context about what went wrong.
-If the tool produces no stderr output, only the `tool_call_error` field is included.
-
-**Note:** Tool stdout streams to your terminal in real-time so you can see progress. Only stderr is captured for
-error reporting.
-
-### MCP Tool Errors
-When an MCP (Model Context Protocol) tool invocation fails due to connection issues, timeouts, or server errors,
-the model receives:
-
-```json
-{
- "tool_call_error": "MCP tool invocation failed: connection refused"
-}
-```
-
-This allows the model to understand that an external service failed and take appropriate action (retry, use an
-alternative approach, or inform the user).
-
-### Why This Matters
-Without proper error propagation, models would only know that "something went wrong" without understanding *what*
-went wrong. By including stderr output and detailed error messages, models can:
-
-- Diagnose the root cause of failures
-- Suggest fixes (e.g., "the file doesn't exist, should I create it?")
-- Retry with corrected parameters
-- Fall back to alternative approaches when appropriate
diff --git a/docs/images/agents/conversation-starters.gif b/docs/images/agents/conversation-starters.gif
deleted file mode 100644
index 594a663..0000000
Binary files a/docs/images/agents/conversation-starters.gif and /dev/null differ
diff --git a/docs/images/agents/sql.gif b/docs/images/agents/sql.gif
deleted file mode 100644
index b7fbd38..0000000
Binary files a/docs/images/agents/sql.gif and /dev/null differ
diff --git a/docs/images/agents/todo-system.png b/docs/images/agents/todo-system.png
deleted file mode 100644
index e66212a..0000000
Binary files a/docs/images/agents/todo-system.png and /dev/null differ
diff --git a/docs/images/macros/macros-example.gif b/docs/images/macros/macros-example.gif
deleted file mode 100644
index 005beb0..0000000
Binary files a/docs/images/macros/macros-example.gif and /dev/null differ
diff --git a/docs/images/mcp/global-settings-overrides-repl.png b/docs/images/mcp/global-settings-overrides-repl.png
deleted file mode 100644
index 00adbcf..0000000
Binary files a/docs/images/mcp/global-settings-overrides-repl.png and /dev/null differ
diff --git a/docs/images/rag/ephemeral-rag.gif b/docs/images/rag/ephemeral-rag.gif
deleted file mode 100644
index fb31d62..0000000
Binary files a/docs/images/rag/ephemeral-rag.gif and /dev/null differ
diff --git a/docs/images/rag/git-loader.png b/docs/images/rag/git-loader.png
deleted file mode 100644
index b6905f8..0000000
Binary files a/docs/images/rag/git-loader.png and /dev/null differ
diff --git a/docs/images/rag/persistent-rag.gif b/docs/images/rag/persistent-rag.gif
deleted file mode 100644
index 6459312..0000000
Binary files a/docs/images/rag/persistent-rag.gif and /dev/null differ
diff --git a/docs/images/repl/continue.gif b/docs/images/repl/continue.gif
deleted file mode 100644
index 3fae887..0000000
Binary files a/docs/images/repl/continue.gif and /dev/null differ
diff --git a/docs/images/repl/copy.gif b/docs/images/repl/copy.gif
deleted file mode 100644
index ad86371..0000000
Binary files a/docs/images/repl/copy.gif and /dev/null differ
diff --git a/docs/images/repl/model.gif b/docs/images/repl/model.gif
deleted file mode 100644
index f64bfd7..0000000
Binary files a/docs/images/repl/model.gif and /dev/null differ
diff --git a/docs/images/repl/regenerate.gif b/docs/images/repl/regenerate.gif
deleted file mode 100644
index ad798cc..0000000
Binary files a/docs/images/repl/regenerate.gif and /dev/null differ
diff --git a/docs/images/repl/set.gif b/docs/images/repl/set.gif
deleted file mode 100644
index 3882417..0000000
Binary files a/docs/images/repl/set.gif and /dev/null differ
diff --git a/docs/images/roles/code.gif b/docs/images/roles/code.gif
deleted file mode 100644
index 18c1d69..0000000
Binary files a/docs/images/roles/code.gif and /dev/null differ
diff --git a/docs/images/roles/prompt-role.gif b/docs/images/roles/prompt-role.gif
deleted file mode 100644
index 54be216..0000000
Binary files a/docs/images/roles/prompt-role.gif and /dev/null differ
diff --git a/docs/images/sessions/sessions-example.gif b/docs/images/sessions/sessions-example.gif
deleted file mode 100644
index b4eef8a..0000000
Binary files a/docs/images/sessions/sessions-example.gif and /dev/null differ
diff --git a/docs/images/shell_integrations/assistant.gif b/docs/images/shell_integrations/assistant.gif
deleted file mode 100644
index 511730d..0000000
Binary files a/docs/images/shell_integrations/assistant.gif and /dev/null differ
diff --git a/docs/images/shell_integrations/code-generation.gif b/docs/images/shell_integrations/code-generation.gif
deleted file mode 100644
index a757bff..0000000
Binary files a/docs/images/shell_integrations/code-generation.gif and /dev/null differ
diff --git a/docs/images/shell_integrations/explain-shell.png b/docs/images/shell_integrations/explain-shell.png
deleted file mode 100644
index 84690e4..0000000
Binary files a/docs/images/shell_integrations/explain-shell.png and /dev/null differ
diff --git a/docs/images/themes/1337-scheme.png b/docs/images/themes/1337-scheme.png
deleted file mode 100644
index 6ad2e87..0000000
Binary files a/docs/images/themes/1337-scheme.png and /dev/null differ
diff --git a/docs/images/themes/coldark.png b/docs/images/themes/coldark.png
deleted file mode 100644
index 2501fc5..0000000
Binary files a/docs/images/themes/coldark.png and /dev/null differ
diff --git a/docs/images/themes/dracula.png b/docs/images/themes/dracula.png
deleted file mode 100644
index 3077c05..0000000
Binary files a/docs/images/themes/dracula.png and /dev/null differ
diff --git a/docs/images/themes/github.png b/docs/images/themes/github.png
deleted file mode 100644
index e6f98c6..0000000
Binary files a/docs/images/themes/github.png and /dev/null differ
diff --git a/docs/images/themes/gruvbox-dark.png b/docs/images/themes/gruvbox-dark.png
deleted file mode 100644
index fbaf262..0000000
Binary files a/docs/images/themes/gruvbox-dark.png and /dev/null differ
diff --git a/docs/images/themes/gruvbox-light.png b/docs/images/themes/gruvbox-light.png
deleted file mode 100644
index 616b201..0000000
Binary files a/docs/images/themes/gruvbox-light.png and /dev/null differ
diff --git a/docs/images/themes/onehalf-dark.png b/docs/images/themes/onehalf-dark.png
deleted file mode 100644
index 9146282..0000000
Binary files a/docs/images/themes/onehalf-dark.png and /dev/null differ
diff --git a/docs/images/themes/onehalf-light.png b/docs/images/themes/onehalf-light.png
deleted file mode 100644
index 86ed15b..0000000
Binary files a/docs/images/themes/onehalf-light.png and /dev/null differ
diff --git a/docs/images/themes/solarized-dark.png b/docs/images/themes/solarized-dark.png
deleted file mode 100644
index 4458464..0000000
Binary files a/docs/images/themes/solarized-dark.png and /dev/null differ
diff --git a/docs/images/themes/solarized-light.png b/docs/images/themes/solarized-light.png
deleted file mode 100644
index 1c71093..0000000
Binary files a/docs/images/themes/solarized-light.png and /dev/null differ
diff --git a/docs/images/themes/sublime-snazzy.png b/docs/images/themes/sublime-snazzy.png
deleted file mode 100644
index 6136a0b..0000000
Binary files a/docs/images/themes/sublime-snazzy.png and /dev/null differ
diff --git a/docs/images/themes/twodark.png b/docs/images/themes/twodark.png
deleted file mode 100644
index a301183..0000000
Binary files a/docs/images/themes/twodark.png and /dev/null differ
diff --git a/docs/images/themes/visual-studio-dark-plus.png b/docs/images/themes/visual-studio-dark-plus.png
deleted file mode 100644
index 1222644..0000000
Binary files a/docs/images/themes/visual-studio-dark-plus.png and /dev/null differ
diff --git a/docs/images/themes/zenburn.png b/docs/images/themes/zenburn.png
deleted file mode 100644
index 5aee8df..0000000
Binary files a/docs/images/themes/zenburn.png and /dev/null differ
diff --git a/docs/images/tools/global-settings-overrides-repl.png b/docs/images/tools/global-settings-overrides-repl.png
deleted file mode 100644
index 463f638..0000000
Binary files a/docs/images/tools/global-settings-overrides-repl.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-checkbox.png b/docs/images/tools/prompt-utils-checkbox.png
deleted file mode 100644
index f557f4a..0000000
Binary files a/docs/images/tools/prompt-utils-checkbox.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-confirm.png b/docs/images/tools/prompt-utils-confirm.png
deleted file mode 100644
index cc18a8b..0000000
Binary files a/docs/images/tools/prompt-utils-confirm.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-debug.png b/docs/images/tools/prompt-utils-debug.png
deleted file mode 100644
index 2eed9bb..0000000
Binary files a/docs/images/tools/prompt-utils-debug.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-error.png b/docs/images/tools/prompt-utils-error.png
deleted file mode 100644
index 3f3e733..0000000
Binary files a/docs/images/tools/prompt-utils-error.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-info.png b/docs/images/tools/prompt-utils-info.png
deleted file mode 100644
index 221f0e8..0000000
Binary files a/docs/images/tools/prompt-utils-info.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-input.png b/docs/images/tools/prompt-utils-input.png
deleted file mode 100644
index 513e567..0000000
Binary files a/docs/images/tools/prompt-utils-input.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-list.png b/docs/images/tools/prompt-utils-list.png
deleted file mode 100644
index 604d146..0000000
Binary files a/docs/images/tools/prompt-utils-list.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-password.png b/docs/images/tools/prompt-utils-password.png
deleted file mode 100644
index 2206020..0000000
Binary files a/docs/images/tools/prompt-utils-password.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-trace.png b/docs/images/tools/prompt-utils-trace.png
deleted file mode 100644
index f698c2e..0000000
Binary files a/docs/images/tools/prompt-utils-trace.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-validate-present.png b/docs/images/tools/prompt-utils-validate-present.png
deleted file mode 100644
index bacd958..0000000
Binary files a/docs/images/tools/prompt-utils-validate-present.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-warning.png b/docs/images/tools/prompt-utils-warning.png
deleted file mode 100644
index 5363986..0000000
Binary files a/docs/images/tools/prompt-utils-warning.png and /dev/null differ
diff --git a/docs/images/tools/prompt-utils-with-validate.png b/docs/images/tools/prompt-utils-with-validate.png
deleted file mode 100644
index 4c4dbb8..0000000
Binary files a/docs/images/tools/prompt-utils-with-validate.png and /dev/null differ
diff --git a/docs/images/vault/vault-demo.gif b/docs/images/vault/vault-demo.gif
deleted file mode 100644
index dd473e6..0000000
Binary files a/docs/images/vault/vault-demo.gif and /dev/null differ
diff --git a/docs/images/vault/vault-repl-commands.png b/docs/images/vault/vault-repl-commands.png
deleted file mode 100644
index 0562a49..0000000
Binary files a/docs/images/vault/vault-repl-commands.png and /dev/null differ
diff --git a/docs/images/vault/vault-repl.png b/docs/images/vault/vault-repl.png
deleted file mode 100644
index 9c7d6aa..0000000
Binary files a/docs/images/vault/vault-repl.png and /dev/null differ
diff --git a/models.yaml b/models.yaml
index 987c091..f440e21 100644
--- a/models.yaml
+++ b/models.yaml
@@ -211,23 +211,31 @@
supports_function_calling: true
- name: gemini-2.5-flash
max_input_tokens: 1048576
- max_output_tokens: 65535
- input_price: 0.3
- output_price: 2.5
+ max_output_tokens: 65536
+ input_price: 0
+ output_price: 0
supports_vision: true
supports_function_calling: true
- name: gemini-2.5-pro
max_input_tokens: 1048576
max_output_tokens: 65536
- input_price: 1.25
- output_price: 10
+ input_price: 0
+ output_price: 0
supports_vision: true
supports_function_calling: true
- name: gemini-2.5-flash-lite
+ max_input_tokens: 1000000
+ max_output_tokens: 64000
+ input_price: 0
+ output_price: 0
+ supports_vision: true
+ supports_function_calling: true
+ - name: gemini-3-pro-preview
+ max_input_tokens: 1048576
+ supports_vision: true
+ supports_function_calling: true
+ - name: gemini-3-flash-preview
max_input_tokens: 1048576
- max_output_tokens: 65535
- input_price: 0.1
- output_price: 0.4
supports_vision: true
supports_function_calling: true
- name: gemini-2.0-flash
@@ -245,11 +253,10 @@
supports_vision: true
supports_function_calling: true
- name: gemma-3-27b-it
- supports_vision: true
- max_input_tokens: 128000
- max_output_tokens: 65536
- input_price: 0.04
- output_price: 0.15
+ max_input_tokens: 131072
+ max_output_tokens: 8192
+ input_price: 0
+ output_price: 0
- name: text-embedding-004
type: embedding
input_price: 0
@@ -432,7 +439,7 @@
thinking:
type: enabled
budget_tokens: 16000
- - name: claude-sonnet-4-20250514
+ - name: claude-sonnet-4-5-20250929
max_input_tokens: 200000
max_output_tokens: 8192
require_max_tokens: true
@@ -440,8 +447,8 @@
output_price: 15
supports_vision: true
supports_function_calling: true
- - name: claude-sonnet-4-20250514:thinking
- real_name: claude-sonnet-4-20250514
+ - name: claude-sonnet-4-5-20250929:thinking
+ real_name: claude-sonnet-4-5-20250929
max_input_tokens: 200000
max_output_tokens: 24000
require_max_tokens: true
@@ -456,22 +463,23 @@
thinking:
type: enabled
budget_tokens: 16000
- - name: claude-3-7-sonnet-20250219
+ - name: claude-haiku-4-5-20251001
max_input_tokens: 200000
max_output_tokens: 8192
require_max_tokens: true
- input_price: 3
- output_price: 15
+ input_price: 1
+ output_price: 5
supports_vision: true
supports_function_calling: true
- - name: claude-3-7-sonnet-20250219:thinking
- real_name: claude-3-7-sonnet-20250219
+ - name: claude-haiku-4-5-20251001:thinking
+ real_name: claude-haiku-4-5-20251001
max_input_tokens: 200000
max_output_tokens: 24000
require_max_tokens: true
- input_price: 3
- output_price: 15
+ input_price: 1
+ output_price: 5
supports_vision: true
+ supports_function_calling: true
patch:
body:
temperature: null
@@ -494,6 +502,12 @@
# - https://docs.mistral.ai/api/
- provider: mistral
models:
+ - name: mistral-large-latest
+ max_output_tokens: 262144
+ input_price: 0.5
+ output_price: 1.5
+ supports_function_calling: true
+ supports_vision: true
- name: mistral-medium-latest
max_input_tokens: 131072
input_price: 0.4
@@ -507,28 +521,33 @@
supports_function_calling: true
supports_vision: true
- name: magistral-medium-latest
- max_input_tokens: 40960
+ max_input_tokens: 131072
input_price: 2
output_price: 5
- name: magistral-small-latest
- max_input_tokens: 40960
+ max_input_tokens: 131072
input_price: 0.5
output_price: 1.5
- name: devstral-medium-latest
- max_input_tokens: 256000
+ max_input_tokens: 262144
input_price: 0.4
output_price: 2
supports_function_calling: true
- name: devstral-small-latest
- max_input_tokens: 256000
+ max_input_tokens: 262144
input_price: 0.1
output_price: 0.3
supports_function_calling: true
- name: codestral-latest
- max_input_tokens: 256000
+ max_input_tokens: 262144
input_price: 0.3
output_price: 0.9
supports_function_calling: true
+ - name: ministral-14b-latest
+ max_input_tokens: 262144
+ input_price: 0.2
+ output_price: 0.2
+ supports_function_calling: true
- name: mistral-embed
type: embedding
max_input_tokens: 8092
@@ -577,8 +596,8 @@
output_price: 10
supports_vision: true
- name: command-r7b-12-2024
- max_input_tokens: 128000
- max_output_tokens: 4000
+ max_input_tokens: 131072
+ max_output_tokens: 4096
input_price: 0.0375
output_price: 0.15
- name: embed-v4.0
@@ -614,23 +633,17 @@
# - https://docs.x.ai/docs/api-reference#chat-completions
- provider: xai
models:
- - name: grok-4
- supports_vision: true
- max_input_tokens: 256000
- input_price: 3
- output_price: 15
- supports_function_calling: true
- - name: grok-4-fast-non-reasoning
+ - name: grok-4-1-fast-non-reasoning
max_input_tokens: 2000000
input_price: 0.2
output_price: 0.5
supports_function_calling: true
- - name: grok-4-fast-reasoning
+ - name: grok-4-1-fast-reasoning
max_input_tokens: 2000000
input_price: 0.2
output_price: 0.5
supports_function_calling: true
- - name: grok-code-fast
+ - name: grok-code-fast-1
max_input_tokens: 256000
input_price: 0.2
output_price: 1.5
@@ -755,16 +768,24 @@
supports_function_calling: true
- name: gemini-2.5-flash-lite
max_input_tokens: 1048576
- max_output_tokens: 65535
- input_price: 0.1
+ max_output_tokens: 65536
+ input_price: 0.3
output_price: 0.4
supports_vision: true
supports_function_calling: true
+ - name: gemini-3-pro-preview
+ max_input_tokens: 1048576
+ supports_vision: true
+ supports_function_calling: true
+ - name: gemini-3-flash-preview
+ max_input_tokens: 1048576
+ supports_vision: true
+ supports_function_calling: true
- name: gemini-2.0-flash-001
max_input_tokens: 1048576
max_output_tokens: 8192
- input_price: 0.1
- output_price: 0.4
+ input_price: 0.15
+ output_price: 0.6
supports_vision: true
supports_function_calling: true
- name: gemini-2.0-flash-lite-001
@@ -774,6 +795,75 @@
output_price: 0.3
supports_vision: true
supports_function_calling: true
+ - name: claude-opus-4-6
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ supports_function_calling: true
+ - name: claude-opus-4-6:thinking
+ real_name: claude-opus-4-6
+ max_input_tokens: 200000
+ max_output_tokens: 24000
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ patch:
+ body:
+ temperature: null
+ top_p: null
+ thinking:
+ type: enabled
+ budget_tokens: 16000
+ - name: claude-sonnet-4-6
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 3
+ output_price: 15
+ supports_vision: true
+ supports_function_calling: true
+ - name: claude-sonnet-4-6:thinking
+ real_name: claude-sonnet-4-6
+ max_input_tokens: 200000
+ max_output_tokens: 24000
+ require_max_tokens: true
+ input_price: 3
+ output_price: 15
+ supports_vision: true
+ patch:
+ body:
+ temperature: null
+ top_p: null
+ thinking:
+ type: enabled
+ budget_tokens: 16000
+ - name: claude-opus-4-5@20251101
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ supports_function_calling: true
+ - name: claude-opus-4-5@20251101:thinking
+ real_name: claude-opus-4-5@20251101
+ max_input_tokens: 200000
+ max_output_tokens: 24000
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ patch:
+ body:
+ temperature: null
+ top_p: null
+ thinking:
+ type: enabled
+ budget_tokens: 16000
- name: claude-sonnet-4-5@20250929
max_input_tokens: 200000
max_output_tokens: 8192
@@ -820,98 +910,6 @@
thinking:
type: enabled
budget_tokens: 16000
- - name: claude-opus-4-1@20250805
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- supports_function_calling: true
- - name: claude-opus-4-1@20250805:thinking
- real_name: claude-opus-4-1@20250805
- max_input_tokens: 200000
- max_output_tokens: 24000
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- patch:
- body:
- temperature: null
- top_p: null
- thinking:
- type: enabled
- budget_tokens: 16000
- - name: claude-opus-4@20250514
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- supports_function_calling: true
- - name: claude-opus-4@20250514:thinking
- real_name: claude-opus-4@20250514
- max_input_tokens: 200000
- max_output_tokens: 24000
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- patch:
- body:
- temperature: null
- top_p: null
- thinking:
- type: enabled
- budget_tokens: 16000
- - name: claude-sonnet-4@20250514
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- supports_function_calling: true
- - name: claude-sonnet-4@20250514:thinking
- real_name: claude-sonnet-4@20250514
- max_input_tokens: 200000
- max_output_tokens: 24000
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- patch:
- body:
- temperature: null
- top_p: null
- thinking:
- type: enabled
- budget_tokens: 16000
- - name: claude-3-7-sonnet@20250219
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- supports_function_calling: true
- - name: claude-3-7-sonnet@20250219:thinking
- real_name: claude-3-7-sonnet@20250219
- max_input_tokens: 200000
- max_output_tokens: 24000
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- patch:
- body:
- temperature: null
- top_p: null
- thinking:
- type: enabled
- budget_tokens: 16000
- name: claude-3-5-haiku@20241022
max_input_tokens: 200000
max_output_tokens: 8192
@@ -952,6 +950,81 @@
# - https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html
- provider: bedrock
models:
+ - name: us.anthropic.claude-opus-4-6-v1
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ supports_function_calling: true
+ - name: us.anthropic.claude-opus-4-6-v1:thinking
+ real_name: us.anthropic.claude-opus-4-6-v1
+ max_input_tokens: 200000
+ max_output_tokens: 24000
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ patch:
+ body:
+ inferenceConfig:
+ temperature: null
+ topP: null
+ additionalModelRequestFields:
+ thinking:
+ type: enabled
+ budget_tokens: 16000
+ - name: us.anthropic.claude-sonnet-4-6
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 3
+ output_price: 15
+ supports_vision: true
+ supports_function_calling: true
+ - name: us.anthropic.claude-sonnet-4-6:thinking
+ real_name: us.anthropic.claude-sonnet-4-6
+ max_input_tokens: 200000
+ max_output_tokens: 24000
+ require_max_tokens: true
+ input_price: 3
+ output_price: 15
+ supports_vision: true
+ patch:
+ body:
+ inferenceConfig:
+ temperature: null
+ topP: null
+ additionalModelRequestFields:
+ thinking:
+ type: enabled
+ budget_tokens: 16000
+ - name: us.anthropic.claude-opus-4-5-20251101-v1:0
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ supports_function_calling: true
+ - name: us.anthropic.claude-opus-4-5-20251101-v1:0:thinking
+ real_name: us.anthropic.claude-opus-4-5-20251101-v1:0
+ max_input_tokens: 200000
+ max_output_tokens: 24000
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ patch:
+ body:
+ inferenceConfig:
+ temperature: null
+ topP: null
+ additionalModelRequestFields:
+ thinking:
+ type: enabled
+ budget_tokens: 16000
- name: us.anthropic.claude-sonnet-4-5-20250929-v1:0
max_input_tokens: 200000
max_output_tokens: 8192
@@ -1002,114 +1075,6 @@
thinking:
type: enabled
budget_tokens: 16000
- - name: us.anthropic.claude-opus-4-1-20250805-v1:0
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- supports_function_calling: true
- - name: us.anthropic.claude-opus-4-1-20250805-v1:0:thinking
- real_name: us.anthropic.claude-opus-4-1-20250805-v1:0
- max_input_tokens: 200000
- max_output_tokens: 24000
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- patch:
- body:
- inferenceConfig:
- temperature: null
- topP: null
- additionalModelRequestFields:
- thinking:
- type: enabled
- budget_tokens: 16000
- - name: us.anthropic.claude-opus-4-20250514-v1:0
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- supports_function_calling: true
- - name: us.anthropic.claude-opus-4-20250514-v1:0:thinking
- real_name: us.anthropic.claude-opus-4-20250514-v1:0
- max_input_tokens: 200000
- max_output_tokens: 24000
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- patch:
- body:
- inferenceConfig:
- temperature: null
- topP: null
- additionalModelRequestFields:
- thinking:
- type: enabled
- budget_tokens: 16000
- - name: us.anthropic.claude-sonnet-4-20250514-v1:0
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- supports_function_calling: true
- - name: us.anthropic.claude-sonnet-4-20250514-v1:0:thinking
- real_name: us.anthropic.claude-sonnet-4-20250514-v1:0
- max_input_tokens: 200000
- max_output_tokens: 24000
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- patch:
- body:
- inferenceConfig:
- temperature: null
- topP: null
- additionalModelRequestFields:
- thinking:
- type: enabled
- budget_tokens: 16000
- - name: us.anthropic.claude-3-7-sonnet-20250219-v1:0
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- supports_function_calling: true
- - name: us.anthropic.claude-3-7-sonnet-20250219-v1:0:thinking
- real_name: us.anthropic.claude-3-7-sonnet-20250219-v1:0
- max_input_tokens: 200000
- max_output_tokens: 24000
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- patch:
- body:
- inferenceConfig:
- temperature: null
- topP: null
- additionalModelRequestFields:
- thinking:
- type: enabled
- budget_tokens: 16000
- - name: anthropic.claude-3-5-haiku-20241022-v1:0
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 0.8
- output_price: 4
- supports_vision: true
- supports_function_calling: true
- name: us.meta.llama4-maverick-17b-instruct-v1:0
max_input_tokens: 131072
max_output_tokens: 8192
@@ -1201,6 +1166,12 @@
require_max_tokens: true
input_price: 0
output_price: 0
+ - name: '@cf/zai-org/glm-4.7-flash'
+ max_input_tokens: 131072
+ max_output_tokens: 2048
+ require_max_tokens: true
+ input_price: 0
+ output_price: 0
- name: '@cf/google/gemma-3-12b-it'
max_input_tokens: 131072
max_output_tokens: 2048
@@ -1264,40 +1235,55 @@
# - https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api
- provider: qianwen
models:
- - name: qwen3-max
- input_price: 1.2
- output_price: 6
- max_output_tokens: 32768
+ - name: qwen3.5-plus
max_input_tokens: 262144
supports_function_calling: true
- - name: qwen-plus
- input_price: 0.4
- output_price: 1.2
- max_output_tokens: 32768
- max_input_tokens: 1000000
+ patch:
+ body:
+ enable_thinking: false
+ - name: qwen3.5-plus:thinking
+ real_name: qwen3.5-plus
+ max_input_tokens: 262144
supports_function_calling: true
- - name: qwen-flash
- max_input_tokens: 1000000
+ - name: qwen3-max
+ max_input_tokens: 262144
supports_function_calling: true
+ - name: qwen3-max:thinking
+ real_name: qwen3-max
+ max_input_tokens: 262144
+ supports_function_calling: true
+ patch:
+ body:
+ enable_thinking: true
- name: qwen3-vl-plus
max_input_tokens: 262144
supports_vision: true
- name: qwen3-vl-flash
max_input_tokens: 262144
supports_vision: true
- - name: qwen-coder-plus
+ - name: qwen3-coder-plus
max_input_tokens: 1000000
- - name: qwen-coder-flash
+ - name: qwen3-coder-flash
max_input_tokens: 1000000
- - name: qwen3-next-80b-a3b-instruct
+ - name: qwen3.5-397b-a17b
max_input_tokens: 262144
- input_price: 0.09
- output_price: 1.1
+ supports_function_calling: true
+ patch:
+ body:
+ enable_thinking: false
+ - name: qwen3.5-397b-a17b:thinking
+ real_name: qwen3.5-397b-a17b
+ max_input_tokens: 262144
+ supports_function_calling: true
+ - name: qwen3-next-80b-a3b-instruct
+ max_input_tokens: 131072
+ input_price: 0.14
+ output_price: 0.56
supports_function_calling: true
- name: qwen3-next-80b-a3b-thinking
- max_input_tokens: 128000
- input_price: 0.15
- output_price: 1.2
+ max_input_tokens: 131072
+ input_price: 0.14
+ output_price: 1.4
- name: qwen3-235b-a22b-instruct-2507
max_input_tokens: 131072
input_price: 0.28
@@ -1305,43 +1291,33 @@
supports_function_calling: true
- name: qwen3-235b-a22b-thinking-2507
max_input_tokens: 131072
- input_price: 0
- output_price: 0
+ input_price: 0.28
+ output_price: 2.8
- name: qwen3-30b-a3b-instruct-2507
- max_output_tokens: 262144
- max_input_tokens: 262144
- input_price: 0.09
- output_price: 0.3
+ max_input_tokens: 131072
+ input_price: 0.105
+ output_price: 0.42
supports_function_calling: true
- name: qwen3-30b-a3b-thinking-2507
- max_input_tokens: 32768
- input_price: 0.051
- output_price: 0.34
+ max_input_tokens: 131072
+ input_price: 0.105
+ output_price: 1.05
- name: qwen3-vl-32b-instruct
- max_output_tokens: 32768
- max_input_tokens: 131072
- input_price: 0.104
- output_price: 0.416
- supports_vision: true
- - name: qwen3-vl-8b-instruct
- max_output_tokens: 32768
- max_input_tokens: 131072
- input_price: 0.08
- output_price: 0.5
- supports_vision: true
- - name: qwen3-coder-480b-a35b-instruct
- max_input_tokens: 262144
- input_price: 1.26
- output_price: 5.04
- - name: qwen3-coder-30b-a3b-instruct
- max_output_tokens: 32768
- max_input_tokens: 160000
- input_price: 0.07
- output_price: 0.27
- - name: deepseek-v3.2-exp
max_input_tokens: 131072
input_price: 0.28
- output_price: 0.42
+ output_price: 1.12
+ supports_vision: true
+ - name: qwen3-vl-8b-instruct
+ max_input_tokens: 131072
+ input_price: 0.07
+ output_price: 0.28
+ supports_vision: true
+ - name: qwen3-coder-next
+ max_input_tokens: 262144
+ - name: qwen3-coder-480b-a35b-instruct
+ max_input_tokens: 262144
+ - name: qwen3-coder-30b-a3b-instruct
+ max_input_tokens: 262144
- name: text-embedding-v4
type: embedding
input_price: 0.1
@@ -1361,27 +1337,20 @@
# - https://cloud.tencent.com/document/product/1729/111007
- provider: hunyuan
models:
- - name: hunyuan-turbos-latest
- max_input_tokens: 28000
+ - name: hunyuan-2.0-instruct-20251111
+ max_input_tokens: 131072
input_price: 0.112
output_price: 0.28
supports_function_calling: true
- - name: hunyuan-t1-latest
- max_input_tokens: 28000
+ - name: hunyuan-2.0-thinking-20251109
+ max_input_tokens: 131072
input_price: 0.14
output_price: 0.56
- - name: hunyuan-lite
- max_input_tokens: 250000
- input_price: 0
- output_price: 0
supports_function_calling: true
- - name: hunyuan-turbos-vision
- max_input_tokens: 6144
+ - name: hunyuan-vision-1.5-instruct
+ max_input_tokens: 24576
input_price: 0.42
- output_price: 0.84
- supports_vision: true
- - name: hunyuan-t1-vision
- max_input_tokens: 24000
+ output_price: 1.26
supports_vision: true
- name: hunyuan-embedding
type: embedding
@@ -1395,6 +1364,12 @@
# - https://platform.moonshot.cn/docs/api/chat#%E5%85%AC%E5%BC%80%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%9C%B0%E5%9D%80
- provider: moonshot
models:
+ - name: kimi-k2.5
+ max_input_tokens: 262144
+ input_price: 0.56
+ output_price: 2.94
+ supports_vision: true
+ supports_function_calling: true
- name: kimi-k2-turbo-preview
max_input_tokens: 262144
input_price: 1.12
@@ -1413,9 +1388,9 @@
output_price: 8.12
supports_vision: true
- name: kimi-k2-thinking
- max_input_tokens: 131072
- input_price: 0.47
- output_price: 2
+ max_input_tokens: 262144
+ input_price: 0.56
+ output_price: 2.24
supports_vision: true
# Links:
@@ -1424,10 +1399,10 @@
- provider: deepseek
models:
- name: deepseek-chat
- max_input_tokens: 163840
- max_output_tokens: 163840
- input_price: 0.32
- output_price: 0.89
+ max_input_tokens: 64000
+ max_output_tokens: 8192
+ input_price: 0.56
+ output_price: 1.68
supports_function_calling: true
- name: deepseek-reasoner
max_input_tokens: 64000
@@ -1440,54 +1415,41 @@
# - https://open.bigmodel.cn/dev/api#glm-4
- provider: zhipuai
models:
- - name: glm-4.6
+ - name: glm-5
max_input_tokens: 202752
- input_price: 0.28
- output_price: 1.12
supports_function_calling: true
- - name: glm-4.5
- max_input_tokens: 131072
- input_price: 0.28
- output_price: 1.12
- - name: glm-4.5-x
- max_input_tokens: 131072
- input_price: 1.12
- output_price: 4.48
+ - name: glm-5:instruct
+ real_name: glm-5
+ max_input_tokens: 202752
supports_function_calling: true
- - name: glm-4.5-air
- max_input_tokens: 131072
- input_price: 0.084
- output_price: 0.56
- - name: glm-4.5-airx
- max_input_tokens: 131072
- input_price: 0.56
- output_price: 2.24
+ patch:
+ body:
+ thinking:
+ type: disabled
+ - name: glm-4.7
+ max_input_tokens: 202752
supports_function_calling: true
- - name: glm-4.5-flash
- max_input_tokens: 131072
+ - name: glm-4.7:instruct
+ real_name: glm-4.7
+ max_input_tokens: 202752
+ supports_function_calling: true
+ patch:
+ body:
+ thinking:
+ type: disabled
+ - name: glm-4.7-flash
+ max_input_tokens: 202752
input_price: 0
output_price: 0
- - name: glm-4.5v
+ supports_function_calling: true
+ - name: glm-4.6v
max_input_tokens: 65536
- input_price: 0.56
- output_price: 1.68
supports_vision: true
- - name: glm-z1-air
- max_input_tokens: 131072
- input_price: 0.07
- output_price: 0.07
- - name: glm-z1-airx
- max_input_tokens: 131072
- input_price: 0.7
- output_price: 0.7
- - name: glm-z1-flashx
- max_input_tokens: 131072
- input_price: 0.014
- output_price: 0.014
- - name: glm-z1-flash
- max_input_tokens: 131072
+ - name: glm-4.6v-flash
+ max_input_tokens: 65536
input_price: 0
output_price: 0
+ supports_vision: true
- name: embedding-3
type: embedding
max_input_tokens: 8192
@@ -1500,15 +1462,29 @@
input_price: 0.112
# Links:
-# - https://platform.minimaxi.com/docs/guides/pricing
+# - https://platform.minimaxi.com/docs/guides/pricing-paygo
# - https://platform.minimaxi.com/document/ChatCompletion%20v2
- provider: minimax
models:
- - name: minimax-m2
- max_output_tokens: 65536
- max_input_tokens: 196608
- input_price: 0.255
- output_price: 1
+ - name: minimax-m2.5
+ max_input_tokens: 204800
+ input_price: 0.294
+ output_price: 1.176
+ supports_function_calling: true
+ - name: minimax-m2.5-highspeed
+ max_input_tokens: 204800
+ input_price: 0.588
+ output_price: 2.352
+ supports_function_calling: true
+ - name: minimax-m2.1
+ max_input_tokens: 204800
+ input_price: 0.294
+ output_price: 1.176
+ supports_function_calling: true
+ - name: minimax-m2.1-highspeed
+ max_input_tokens: 204800
+ input_price: 0.588
+ output_price: 2.352
supports_function_calling: true
# Links:
@@ -1516,18 +1492,11 @@
# - https://openrouter.ai/docs/api-reference/chat-completion
- provider: openrouter
models:
- - name: openai/gpt-5.1
+ - name: openai/gpt-5.2
max_input_tokens: 400000
max_output_tokens: 128000
- input_price: 1.25
- output_price: 10
- supports_vision: true
- supports_function_calling: true
- - name: openai/gpt-5.1-chat
- max_input_tokens: 128000
- max_output_tokens: 16384
- input_price: 1.25
- output_price: 10
+ input_price: 1.75
+ output_price: 14
supports_vision: true
supports_function_calling: true
- name: openai/gpt-5
@@ -1537,13 +1506,6 @@
output_price: 10
supports_vision: true
supports_function_calling: true
- - name: openai/gpt-5-chat
- max_input_tokens: 128000
- max_output_tokens: 16384
- input_price: 1.25
- output_price: 10
- supports_vision: true
- supports_function_calling: true
- name: openai/gpt-5-mini
max_input_tokens: 400000
max_output_tokens: 128000
@@ -1565,164 +1527,82 @@
output_price: 8
supports_vision: true
supports_function_calling: true
- - name: openai/gpt-4.1-mini
- max_input_tokens: 1047576
- max_output_tokens: 32768
- input_price: 0.4
- output_price: 1.6
- supports_vision: true
- supports_function_calling: true
- - name: openai/gpt-4.1-nano
- max_input_tokens: 1047576
- max_output_tokens: 32768
- input_price: 0.1
- output_price: 0.4
- supports_vision: true
- supports_function_calling: true
- name: openai/gpt-4o
- max_output_tokens: 16384
max_input_tokens: 128000
input_price: 2.5
output_price: 10
supports_vision: true
supports_function_calling: true
- - name: openai/gpt-4o-mini
- max_output_tokens: 16384
- max_input_tokens: 128000
- input_price: 0.15
- output_price: 0.6
- supports_vision: true
- supports_function_calling: true
- - name: openai/o4-mini
- max_output_tokens: 100000
- max_input_tokens: 200000
- input_price: 1.1
- output_price: 4.4
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- max_tokens: null
- temperature: null
- top_p: null
- - name: openai/o4-mini-high
- max_output_tokens: 100000
- max_input_tokens: 200000
- input_price: 1.1
- output_price: 4.4
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- reasoning_effort: high
- max_tokens: null
- temperature: null
- top_p: null
- - name: openai/o3
- max_output_tokens: 100000
- max_input_tokens: 200000
- input_price: 2
- output_price: 8
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- max_tokens: null
- temperature: null
- top_p: null
- - name: openai/o3-high
- real_name: openai/o3
- max_input_tokens: 200000
- input_price: 2
- output_price: 8
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- reasoning_effort: high
- temperature: null
- top_p: null
- - name: openai/o3-mini
- max_output_tokens: 100000
- max_input_tokens: 200000
- input_price: 1.1
- output_price: 4.4
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- temperature: null
- top_p: null
- - name: openai/o3-mini-high
- max_output_tokens: 100000
- max_input_tokens: 200000
- input_price: 1.1
- output_price: 4.4
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- temperature: null
- top_p: null
- name: openai/gpt-oss-120b
max_input_tokens: 131072
- input_price: 0.039
- output_price: 0.19
+ input_price: 0.09
+ output_price: 0.45
supports_function_calling: true
- name: openai/gpt-oss-20b
max_input_tokens: 131072
- input_price: 0.03
- output_price: 0.14
+ input_price: 0.04
+ output_price: 0.16
supports_function_calling: true
- name: google/gemini-2.5-flash
- max_output_tokens: 65535
max_input_tokens: 1048576
input_price: 0.3
output_price: 2.5
supports_vision: true
supports_function_calling: true
- name: google/gemini-2.5-pro
- max_output_tokens: 65536
max_input_tokens: 1048576
input_price: 1.25
output_price: 10
supports_vision: true
supports_function_calling: true
- name: google/gemini-2.5-flash-lite
- max_output_tokens: 65535
max_input_tokens: 1048576
- input_price: 0.1
+ input_price: 0.3
output_price: 0.4
supports_vision: true
- name: google/gemini-2.0-flash-001
- max_output_tokens: 8192
- max_input_tokens: 1048576
- input_price: 0.1
- output_price: 0.4
+ max_input_tokens: 1000000
+ input_price: 0.15
+ output_price: 0.6
supports_vision: true
supports_function_calling: true
- name: google/gemini-2.0-flash-lite-001
- max_output_tokens: 8192
max_input_tokens: 1048576
input_price: 0.075
output_price: 0.3
supports_vision: true
supports_function_calling: true
- name: google/gemma-3-27b-it
- max_output_tokens: 65536
+ max_input_tokens: 131072
+ input_price: 0.1
+ output_price: 0.2
+ - name: anthropic/claude-opus-4.6
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
supports_vision: true
- max_input_tokens: 128000
- input_price: 0.04
- output_price: 0.15
+ supports_function_calling: true
+ - name: anthropic/claude-sonnet-4.6
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 3
+ output_price: 15
+ supports_vision: true
+ supports_function_calling: true
+ - name: anthropic/claude-opus-4.5
+ max_input_tokens: 200000
+ max_output_tokens: 8192
+ require_max_tokens: true
+ input_price: 5
+ output_price: 25
+ supports_vision: true
+ supports_function_calling: true
- name: anthropic/claude-sonnet-4.5
- max_input_tokens: 1000000
- max_output_tokens: 64000
+ max_input_tokens: 200000
+ max_output_tokens: 8192
require_max_tokens: true
input_price: 3
output_price: 15
@@ -1730,71 +1610,33 @@
supports_function_calling: true
- name: anthropic/claude-haiku-4.5
max_input_tokens: 200000
- max_output_tokens: 64000
+ max_output_tokens: 8192
require_max_tokens: true
input_price: 1
output_price: 5
supports_vision: true
supports_function_calling: true
- - name: anthropic/claude-opus-4.1
- max_input_tokens: 200000
- max_output_tokens: 32000
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- supports_function_calling: true
- - name: anthropic/claude-opus-4
- max_input_tokens: 200000
- max_output_tokens: 32000
- require_max_tokens: true
- input_price: 15
- output_price: 75
- supports_vision: true
- supports_function_calling: true
- - name: anthropic/claude-sonnet-4
- max_input_tokens: 1000000
- max_output_tokens: 64000
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- supports_function_calling: true
- - name: anthropic/claude-3.7-sonnet
- max_input_tokens: 200000
- max_output_tokens: 64000
- require_max_tokens: true
- input_price: 3
- output_price: 15
- supports_vision: true
- supports_function_calling: true
- - name: anthropic/claude-3.5-haiku
- max_input_tokens: 200000
- max_output_tokens: 8192
- require_max_tokens: true
- input_price: 0.8
- output_price: 4
- supports_vision: true
- supports_function_calling: true
- name: meta-llama/llama-4-maverick
- max_output_tokens: 16384
max_input_tokens: 1048576
- input_price: 0.15
+ input_price: 0.18
output_price: 0.6
supports_vision: true
supports_function_calling: true
- name: meta-llama/llama-4-scout
- max_output_tokens: 16384
max_input_tokens: 327680
input_price: 0.08
output_price: 0.3
supports_vision: true
supports_function_calling: true
- name: meta-llama/llama-3.3-70b-instruct
- max_output_tokens: 16384
max_input_tokens: 131072
- input_price: 0.1
- output_price: 0.32
+ input_price: 0.12
+ output_price: 0.3
+ - name: mistralai/mistral-large-2512
+ max_input_tokens: 262144
+ input_price: 0.5
+ output_price: 1.5
+ supports_function_calling: true
- name: mistralai/mistral-medium-3.1
max_input_tokens: 131072
input_price: 0.4
@@ -1802,142 +1644,119 @@
supports_function_calling: true
supports_vision: true
- name: mistralai/mistral-small-3.2-24b-instruct
- max_output_tokens: 131072
- max_input_tokens: 131072
- input_price: 0.06
- output_price: 0.18
- supports_vision: true
- - name: mistralai/magistral-medium-2506
- max_input_tokens: 40960
- input_price: 2
- output_price: 5
- - name: mistralai/magistral-medium-2506:thinking
- max_input_tokens: 40960
- input_price: 2
- output_price: 5
- - name: mistralai/magistral-small-2506
- max_input_tokens: 40960
- input_price: 0.5
- output_price: 1.5
- - name: mistralai/devstral-medium
- max_input_tokens: 131072
- input_price: 0.4
- output_price: 2
- supports_function_calling: true
- - name: mistralai/devstral-small
max_input_tokens: 131072
input_price: 0.1
output_price: 0.3
+ supports_vision: true
+ - name: mistralai/devstral-2512
+ max_input_tokens: 262144
+ input_price: 0.5
+ output_price: 0.22
+ supports_function_calling: true
+ - name: mistralai/devstral-small
+ max_input_tokens: 131072
+ input_price: 0.07
+ output_price: 0.28
supports_function_calling: true
- name: mistralai/codestral-2508
max_input_tokens: 256000
input_price: 0.3
output_price: 0.9
supports_function_calling: true
+ - name: mistralai/ministral-14b-2512
+ max_input_tokens: 262144
+ input_price: 0.2
+ output_price: 0.2
+ supports_function_calling: true
- name: ai21/jamba-large-1.7
- max_output_tokens: 4096
max_input_tokens: 256000
input_price: 2
output_price: 8
supports_function_calling: true
- - name: ai21/jamba-mini-1.7
- max_input_tokens: 256000
- input_price: 0.2
- output_price: 0.4
- supports_function_calling: true
- name: cohere/command-a
- max_output_tokens: 8192
max_input_tokens: 256000
input_price: 2.5
output_price: 10
supports_function_calling: true
- name: cohere/command-r7b-12-2024
max_input_tokens: 128000
- max_output_tokens: 4000
+ max_output_tokens: 4096
input_price: 0.0375
output_price: 0.15
- - name: deepseek/deepseek-v3.2-exp
- max_output_tokens: 65536
+ - name: deepseek/deepseek-v3.2
max_input_tokens: 163840
- input_price: 0.27
- output_price: 0.41
- - name: deepseek/deepseek-v3.1-terminus
- max_input_tokens: 163840
- input_price: 0.21
- output_price: 0.79
- - name: deepseek/deepseek-chat-v3.1
- max_output_tokens: 7168
- max_input_tokens: 32768
- input_price: 0.15
- output_price: 0.75
- - name: deepseek/deepseek-r1-0528
- max_output_tokens: 65536
- max_input_tokens: 163840
- input_price: 0.4
- output_price: 1.75
- patch:
- body:
- include_reasoning: true
+ input_price: 0.25
+ output_price: 0.38
- name: qwen/qwen3-max
- max_output_tokens: 32768
max_input_tokens: 262144
input_price: 1.2
output_price: 6
supports_function_calling: true
- - name: qwen/qwen-plus
+ - name: qwen/qwen3-max-thinking
+ max_input_tokens: 262144
+ input_price: 1.2
+ output_price: 6
+ supports_function_calling: true
+ - name: qwen/qwen3.5-plus-02-15
max_input_tokens: 1000000
- max_output_tokens: 32768
+ max_output_tokens: 8192
input_price: 0.4
- output_price: 1.2
+ output_price: 2.4
+ supports_function_calling: true
+ - name: qwen/qwen3.5-397b-a17b
+ max_input_tokens: 262144
+ max_output_tokens: 8192
+ input_price: 0.15
+ output_price: 1
supports_function_calling: true
- name: qwen/qwen3-next-80b-a3b-instruct
max_input_tokens: 262144
- input_price: 0.09
- output_price: 1.1
+ input_price: 0.1
+ output_price: 0.8
supports_function_calling: true
- name: qwen/qwen3-next-80b-a3b-thinking
- max_input_tokens: 128000
- input_price: 0.15
- output_price: 1.2
- - name: qwen/qwen5-235b-a22b-2507 # Qwen3 235B A22B Instruct 2507
+ max_input_tokens: 262144
+ input_price: 0.1
+ output_price: 0.8
+ - name: qwen/qwen3-235b-a22b-2507 # Qwen3 235B A22B Instruct 2507
max_input_tokens: 262144
input_price: 0.12
output_price: 0.59
supports_function_calling: true
- name: qwen/qwen3-235b-a22b-thinking-2507
- max_input_tokens: 131072
- input_price: 0
- output_price: 0
- - name: qwen/qwen3-30b-a3b-instruct-2507
- max_output_tokens: 262144
max_input_tokens: 262144
- input_price: 0.09
- output_price: 0.3
- - name: qwen/qwen3-30b-a3b-thinking-2507
- max_input_tokens: 32768
- input_price: 0.051
- output_price: 0.34
- - name: qwen/qwen3-vl-32b-instruct
- max_output_tokens: 32768
+ input_price: 0.118
+ output_price: 0.118
+ - name: qwen/qwen3-30b-a3b-instruct-2507
max_input_tokens: 131072
- input_price: 0.104
- output_price: 0.416
+ input_price: 0.2
+ output_price: 0.8
+ - name: qwen/qwen3-30b-a3b-thinking-2507
+ max_input_tokens: 262144
+ input_price: 0.071
+ output_price: 0.285
+ - name: qwen/qwen3-vl-32b-instruct
+ max_input_tokens: 262144
+ input_price: 0.35
+ output_price: 1.1
supports_vision: true
- name: qwen/qwen3-vl-8b-instruct
- max_output_tokens: 32768
- max_input_tokens: 131072
+ max_input_tokens: 262144
input_price: 0.08
- output_price: 0.5
+ output_price: 0.50
supports_vision: true
+ - name: qwen/qwen3-coder-next
+ max_input_tokens: 262144
+ input_price: 0.12
+ output_price: 0.75
+ supports_function_calling: true
- name: qwen/qwen3-coder-plus
- max_output_tokens: 65536
- max_input_tokens: 1000000
+ max_input_tokens: 128000
input_price: 1
output_price: 5
supports_function_calling: true
- name: qwen/qwen3-coder-flash
- max_output_tokens: 65536
- max_input_tokens: 1000000
+ max_input_tokens: 128000
input_price: 0.3
output_price: 1.5
supports_function_calling: true
@@ -1947,47 +1766,38 @@
output_price: 0.95
supports_function_calling: true
- name: qwen/qwen3-coder-30b-a3b-instruct
- max_output_tokens: 32768
- max_input_tokens: 160000
- input_price: 0.07
- output_price: 0.27
+ max_input_tokens: 262144
+ input_price: 0.052
+ output_price: 0.207
+ supports_function_calling: true
+ - name: moonshotai/kimi-k2.5
+ max_input_tokens: 262144
+ input_price: 0.57
+ output_price: 2.85
+ supports_vision: true
supports_function_calling: true
- name: moonshotai/kimi-k2-0905
- max_input_tokens: 131072
- input_price: 0.4
- output_price: 2
+ max_input_tokens: 262144
+ input_price: 0.296
+ output_price: 1.185
+ supports_vision: true
supports_function_calling: true
- name: moonshotai/kimi-k2-thinking
- max_input_tokens: 131072
- input_price: 0.47
- output_price: 2
+ max_input_tokens: 262144
+ input_price: 0.45
+ output_price: 2.35
supports_function_calling: true
- - name: moonshotai/kimi-dev-72b
- max_input_tokens: 131072
- input_price: 0.29
- output_price: 1.15
- supports_function_calling: true
- - name: x-ai/grok-4
- supports_vision: true
- max_input_tokens: 256000
- input_price: 3
- output_price: 15
- supports_function_calling: true
- - name: x-ai/grok-4-fast
- max_output_tokens: 30000
- supports_vision: true
+ - name: x-ai/grok-4.1-fast
max_input_tokens: 2000000
input_price: 0.2
output_price: 0.5
supports_function_calling: true
- name: x-ai/grok-code-fast-1
- max_output_tokens: 10000
max_input_tokens: 256000
input_price: 0.2
output_price: 1.5
supports_function_calling: true
- name: amazon/nova-premier-v1
- max_output_tokens: 32000
max_input_tokens: 1000000
input_price: 2.5
output_price: 12.5
@@ -2010,49 +1820,57 @@
input_price: 0.035
output_price: 0.14
- name: perplexity/sonar-pro
- max_output_tokens: 8000
- supports_vision: true
max_input_tokens: 200000
input_price: 3
output_price: 15
- name: perplexity/sonar
- supports_vision: true
max_input_tokens: 127072
input_price: 1
output_price: 1
- name: perplexity/sonar-reasoning-pro
- supports_vision: true
max_input_tokens: 128000
input_price: 2
output_price: 8
patch:
body:
include_reasoning: true
- - name: perplexity/sonar-reasoning
- max_input_tokens: 127000
- input_price: 1
- output_price: 5
- patch:
- body:
- include_reasoning: true
- name: perplexity/sonar-deep-research
- max_input_tokens: 128000
+ max_input_tokens: 200000
input_price: 2
output_price: 8
patch:
body:
include_reasoning: true
- - name: minimax/minimax-m2
- max_output_tokens: 65536
+ - name: minimax/minimax-m2.5
max_input_tokens: 196608
- input_price: 0.255
- output_price: 1
- - name: z-ai/glm-4.6
- max_output_tokens: 131072
- max_input_tokens: 202752
- input_price: 0.35
- output_price: 1.71
+ input_price: 0.3
+ output_price: 1.1
supports_function_calling: true
+ - name: minimax/minimax-m2.1
+ max_input_tokens: 196608
+ input_price: 0.12
+ output_price: 0.48
+ supports_function_calling: true
+ - name: z-ai/glm-5
+ max_input_tokens: 204800
+ input_price: 0.95
+ output_price: 2.55
+ supports_function_calling: true
+ - name: z-ai/glm-4.7
+ max_input_tokens: 202752
+ input_price: 0.16
+ output_price: 0.80
+ supports_function_calling: true
+ - name: z-ai/glm-4.7-flash
+ max_input_tokens: 202752
+ input_price: 0.07
+ output_price: 0.40
+ supports_function_calling: true
+ - name: z-ai/glm-4.6v
+ max_input_tokens: 131072
+ input_price: 0.3
+ output_price: 0.9
+ supports_vision: true
# Links:
# - https://github.com/marketplace?type=models
@@ -2063,11 +1881,6 @@
max_output_tokens: 128000
supports_vision: true
supports_function_calling: true
- - name: gpt-5-chat
- max_input_tokens: 400000
- max_output_tokens: 128000
- supports_vision: true
- supports_function_calling: true
- name: gpt-5-mini
max_input_tokens: 400000
max_output_tokens: 128000
@@ -2083,90 +1896,10 @@
max_output_tokens: 32768
supports_vision: true
supports_function_calling: true
- - name: gpt-4.1-mini
- max_input_tokens: 1047576
- max_output_tokens: 32768
- supports_vision: true
- supports_function_calling: true
- - name: gpt-4.1-nano
- max_input_tokens: 1047576
- max_output_tokens: 32768
- supports_vision: true
- supports_function_calling: true
- name: gpt-4o
max_input_tokens: 128000
max_output_tokens: 16384
supports_function_calling: true
- - name: gpt-4o-mini
- max_input_tokens: 128000
- max_output_tokens: 16384
- supports_function_calling: true
- - name: o4-mini
- max_input_tokens: 200000
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- max_tokens: null
- temperature: null
- top_p: null
- - name: o4-mini-high
- real_name: o4-mini
- max_input_tokens: 200000
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- reasoning_effort: high
- max_tokens: null
- temperature: null
- top_p: null
- - name: o3
- max_input_tokens: 200000
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- max_tokens: null
- temperature: null
- top_p: null
- - name: o3-high
- real_name: o3
- max_input_tokens: 200000
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- reasoning_effort: high
- max_tokens: null
- temperature: null
- top_p: null
- - name: o3-mini
- max_input_tokens: 200000
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- max_tokens: null
- temperature: null
- top_p: null
- - name: o3-mini-high
- real_name: o3-mini
- max_input_tokens: 200000
- supports_vision: true
- supports_function_calling: true
- system_prompt_prefix: Formatting re-enabled
- patch:
- body:
- reasoning_effort: high
- max_tokens: null
- temperature: null
- top_p: null
- name: text-embedding-3-large
type: embedding
max_tokens_per_chunk: 8191
@@ -2248,6 +1981,16 @@
input_price: 0.08
output_price: 0.3
supports_vision: true
+ - name: Qwen/Qwen3-Max
+ max_input_tokens: 262144
+ input_price: 1.2
+ output_price: 6
+ supports_function_calling: true
+ - name: Qwen/Qwen3-Max-Thinking
+ max_input_tokens: 262144
+ input_price: 1.2
+ output_price: 6
+ supports_function_calling: true
- name: Qwen/Qwen3-Next-80B-A3B-Instruct
max_input_tokens: 262144
input_price: 0.14
@@ -2285,22 +2028,11 @@
input_price: 0.18
output_price: 0.69
supports_vision: true
- - name: deepseek-ai/DeepSeek-V3.2-Exp
+ - name: deepseek-ai/DeepSeek-V3.2
max_input_tokens: 163840
- input_price: 0.27
- output_price: 0.40
- - name: deepseek-ai/DeepSeek-V3.1-Terminus
- max_input_tokens: 163840
- input_price: 0.27
- output_price: 1.0
- - name: deepseek-ai/DeepSeek-V3.1
- max_input_tokens: 163840
- input_price: 0.3
- output_price: 1.0
- - name: deepseek-ai/DeepSeek-R1-0528
- max_input_tokens: 163840
- input_price: 0.5
- output_price: 2.15
+ input_price: 0.26
+ output_price: 0.39
+ supports_function_calling: true
- name: google/gemma-3-27b-it
max_input_tokens: 131072
input_price: 0.1
@@ -2309,6 +2041,11 @@
max_input_tokens: 32768
input_price: 0.06
output_price: 0.12
+ - name: moonshotai/Kimi-K2.5
+ max_input_tokens: 262144
+ input_price: 0.5
+ output_price: 2.8
+ supports_function_calling: true
- name: moonshotai/Kimi-K2-Instruct-0905
max_input_tokens: 262144
input_price: 0.5
@@ -2319,11 +2056,36 @@
input_price: 0.55
output_price: 2.5
supports_function_calling: true
- - name: zai-org/GLM-4.6
- max_input_tokens: 202752
- input_price: 0.6
- output_price: 1.9
+ - name: MiniMaxAI/MiniMax-M2.5
+ max_input_tokens: 196608
+ input_price: 0.27
+ output_price: 0.95
supports_function_calling: true
+ - name: MiniMaxAI/MiniMax-M2.1
+ max_input_tokens: 196608
+ input_price: 0.27
+ output_price: 0.95
+ supports_function_calling: true
+ - name: zai-org/GLM-5
+ max_input_tokens: 202752
+ input_price: 0.8
+ output_price: 2.56
+ supports_function_calling: true
+ - name: zai-org/GLM-4.7
+ max_input_tokens: 202752
+ input_price: 0.43
+ output_price: 1.75
+ supports_function_calling: true
+ - name: zai-org/GLM-4.7-Flash
+ max_input_tokens: 202752
+ input_price: 0.06
+ output_price: 0.4
+ supports_function_calling: true
+ - name: zai-org/GLM-4.6V
+ max_input_tokens: 131072
+ input_price: 0.3
+ output_price: 0.9
+ supports_vision: true
- name: BAAI/bge-large-en-v1.5
type: embedding
input_price: 0.01
diff --git a/src/cli/completer.rs b/src/cli/completer.rs
index 81bc604..1809bbc 100644
--- a/src/cli/completer.rs
+++ b/src/cli/completer.rs
@@ -1,5 +1,7 @@
use crate::client::{ModelType, list_models};
-use crate::config::{Config, list_agents};
+use crate::config::paths;
+use crate::config::{AppConfig, Config, list_agents, list_sessions};
+use crate::vault::Vault;
use clap_complete::{CompletionCandidate, Shell, generate};
use clap_complete_nushell::Nushell;
use std::ffi::OsStr;
@@ -32,8 +34,8 @@ impl ShellCompletion {
pub(super) fn model_completer(current: &OsStr) -> Vec {
let cur = current.to_string_lossy();
- match Config::init_bare() {
- Ok(config) => list_models(&config, ModelType::Chat)
+ match load_app_config_for_completion() {
+ Ok(app_config) => list_models(&app_config, ModelType::Chat)
.into_iter()
.filter(|&m| m.id().starts_with(&*cur))
.map(|m| CompletionCandidate::new(m.id()))
@@ -42,9 +44,23 @@ pub(super) fn model_completer(current: &OsStr) -> Vec {
}
}
+fn load_app_config_for_completion() -> anyhow::Result {
+ let h = tokio::runtime::Handle::try_current().ok();
+ let cfg = match h {
+ Some(handle) => {
+ tokio::task::block_in_place(|| handle.block_on(Config::load_with_interpolation(true)))?
+ }
+ None => {
+ let rt = tokio::runtime::Runtime::new()?;
+ rt.block_on(Config::load_with_interpolation(true))?
+ }
+ };
+ AppConfig::from_config(cfg)
+}
+
pub(super) fn role_completer(current: &OsStr) -> Vec {
let cur = current.to_string_lossy();
- Config::list_roles(true)
+ paths::list_roles(true)
.into_iter()
.filter(|r| r.starts_with(&*cur))
.map(CompletionCandidate::new)
@@ -62,7 +78,7 @@ pub(super) fn agent_completer(current: &OsStr) -> Vec {
pub(super) fn rag_completer(current: &OsStr) -> Vec {
let cur = current.to_string_lossy();
- Config::list_rags()
+ paths::list_rags()
.into_iter()
.filter(|r| r.starts_with(&*cur))
.map(CompletionCandidate::new)
@@ -71,7 +87,7 @@ pub(super) fn rag_completer(current: &OsStr) -> Vec {
pub(super) fn macro_completer(current: &OsStr) -> Vec {
let cur = current.to_string_lossy();
- Config::list_macros()
+ paths::list_macros()
.into_iter()
.filter(|m| m.starts_with(&*cur))
.map(CompletionCandidate::new)
@@ -80,22 +96,17 @@ pub(super) fn macro_completer(current: &OsStr) -> Vec {
pub(super) fn session_completer(current: &OsStr) -> Vec {
let cur = current.to_string_lossy();
- match Config::init_bare() {
- Ok(config) => config
- .list_sessions()
- .into_iter()
- .filter(|s| s.starts_with(&*cur))
- .map(CompletionCandidate::new)
- .collect(),
- Err(_) => vec![],
- }
+ list_sessions()
+ .into_iter()
+ .filter(|s| s.starts_with(&*cur))
+ .map(CompletionCandidate::new)
+ .collect()
}
pub(super) fn secrets_completer(current: &OsStr) -> Vec {
let cur = current.to_string_lossy();
- match Config::init_bare() {
- Ok(config) => config
- .vault
+ match load_app_config_for_completion() {
+ Ok(app_config) => Vault::init(&app_config)
.list_secrets(false)
.unwrap_or_default()
.into_iter()
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 2f4f8fb..2fbe62b 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -176,3 +176,220 @@ impl Cli {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use clap::Parser;
+
+ fn parse(args: &[&str]) -> Cli {
+ let mut full_args = vec!["loki"];
+ full_args.extend_from_slice(args);
+ Cli::try_parse_from(full_args).unwrap()
+ }
+
+ #[test]
+ fn parse_no_args_defaults() {
+ let cli = parse(&[]);
+ assert!(cli.model.is_none());
+ assert!(cli.role.is_none());
+ assert!(cli.session.is_none());
+ assert!(cli.agent.is_none());
+ assert!(!cli.execute);
+ assert!(!cli.code);
+ assert!(!cli.no_stream);
+ assert!(!cli.dry_run);
+ assert!(!cli.info);
+ assert!(!cli.build_tools);
+ assert!(cli.file.is_empty());
+ assert!(cli.text.is_empty());
+ }
+
+ #[test]
+ fn parse_model_flag() {
+ let cli = parse(&["--model", "gpt-4o"]);
+ assert_eq!(cli.model, Some("gpt-4o".to_string()));
+ }
+
+ #[test]
+ fn parse_model_short_flag() {
+ let cli = parse(&["-m", "gpt-4o"]);
+ assert_eq!(cli.model, Some("gpt-4o".to_string()));
+ }
+
+ #[test]
+ fn parse_role_flag() {
+ let cli = parse(&["--role", "coder"]);
+ assert_eq!(cli.role, Some("coder".to_string()));
+ }
+
+ #[test]
+ fn parse_session_with_name() {
+ let cli = parse(&["--session", "my-session"]);
+ assert_eq!(cli.session, Some(Some("my-session".to_string())));
+ }
+
+ #[test]
+ fn parse_agent_flag() {
+ let cli = parse(&["--agent", "sisyphus"]);
+ assert_eq!(cli.agent, Some("sisyphus".to_string()));
+ }
+
+ #[test]
+ fn parse_agent_short_flag() {
+ let cli = parse(&["-a", "sisyphus"]);
+ assert_eq!(cli.agent, Some("sisyphus".to_string()));
+ }
+
+ #[test]
+ fn parse_execute_flag() {
+ let cli = parse(&["-e", "list files"]);
+ assert!(cli.execute);
+ }
+
+ #[test]
+ fn parse_code_flag() {
+ let cli = parse(&["-c", "hello world"]);
+ assert!(cli.code);
+ }
+
+ #[test]
+ fn parse_no_stream_flag() {
+ let cli = parse(&["-S", "test"]);
+ assert!(cli.no_stream);
+ }
+
+ #[test]
+ fn parse_dry_run_flag() {
+ let cli = parse(&["--dry-run", "test"]);
+ assert!(cli.dry_run);
+ }
+
+ #[test]
+ fn parse_info_flag() {
+ let cli = parse(&["--info"]);
+ assert!(cli.info);
+ }
+
+ #[test]
+ fn parse_list_flags() {
+ assert!(parse(&["--list-models"]).list_models);
+ assert!(parse(&["--list-roles"]).list_roles);
+ assert!(parse(&["--list-sessions"]).list_sessions);
+ assert!(parse(&["--list-agents"]).list_agents);
+ assert!(parse(&["--list-rags"]).list_rags);
+ assert!(parse(&["--list-macros"]).list_macros);
+ }
+
+ #[test]
+ fn parse_file_flag_single() {
+ let cli = parse(&["-f", "file.txt", "question"]);
+ assert_eq!(cli.file, vec!["file.txt"]);
+ }
+
+ #[test]
+ fn parse_file_flag_multiple() {
+ let cli = parse(&["-f", "a.txt", "-f", "b.txt", "question"]);
+ assert_eq!(cli.file, vec!["a.txt", "b.txt"]);
+ }
+
+ #[test]
+ fn parse_trailing_text() {
+ let cli = parse(&["hello", "world"]);
+ assert_eq!(cli.text, vec!["hello", "world"]);
+ }
+
+ #[test]
+ fn parse_prompt_flag() {
+ let cli = parse(&["--prompt", "be a pirate"]);
+ assert_eq!(cli.prompt, Some("be a pirate".to_string()));
+ }
+
+ #[test]
+ fn parse_empty_session_flag() {
+ let cli = parse(&["--session", "s", "--empty-session"]);
+ assert!(cli.empty_session);
+ }
+
+ #[test]
+ fn parse_save_session_flag() {
+ let cli = parse(&["--session", "s", "--save-session"]);
+ assert!(cli.save_session);
+ }
+
+ #[test]
+ fn parse_build_tools_flag() {
+ let cli = parse(&["--build-tools"]);
+ assert!(cli.build_tools);
+ }
+
+ #[test]
+ fn parse_sync_models_flag() {
+ let cli = parse(&["--sync-models"]);
+ assert!(cli.sync_models);
+ }
+
+ #[test]
+ fn parse_model_with_role() {
+ let cli = parse(&["-m", "gpt-4o", "-r", "coder"]);
+ assert_eq!(cli.model, Some("gpt-4o".to_string()));
+ assert_eq!(cli.role, Some("coder".to_string()));
+ }
+
+ #[test]
+ fn parse_agent_with_file_and_text() {
+ let cli = parse(&["-a", "sisyphus", "-f", "code.rs", "explain", "this"]);
+ assert_eq!(cli.agent, Some("sisyphus".to_string()));
+ assert_eq!(cli.file, vec!["code.rs"]);
+ assert_eq!(cli.text, vec!["explain", "this"]);
+ }
+
+ #[test]
+ fn parse_role_with_session() {
+ let cli = parse(&["-r", "coder", "-s", "dev-session"]);
+ assert_eq!(cli.role, Some("coder".to_string()));
+ assert_eq!(cli.session, Some(Some("dev-session".to_string())));
+ }
+
+ #[test]
+ fn cli_text_returns_none_when_no_text_no_stdin() {
+ let cli = parse(&[]);
+ assert!(cli.text().unwrap().is_none());
+ }
+
+ #[test]
+ fn cli_text_joins_trailing_args() {
+ let cli = parse(&["hello", "world"]);
+ assert_eq!(cli.text().unwrap(), Some("hello world".to_string()));
+ }
+
+ #[test]
+ fn parse_add_secret_flag() {
+ let cli = parse(&["--add-secret", "MY_KEY"]);
+ assert_eq!(cli.add_secret, Some("MY_KEY".to_string()));
+ }
+
+ #[test]
+ fn parse_get_secret_flag() {
+ let cli = parse(&["--get-secret", "MY_KEY"]);
+ assert_eq!(cli.get_secret, Some("MY_KEY".to_string()));
+ }
+
+ #[test]
+ fn parse_list_secrets_flag() {
+ let cli = parse(&["--list-secrets"]);
+ assert!(cli.list_secrets);
+ }
+
+ #[test]
+ fn parse_rag_flag() {
+ let cli = parse(&["--rag", "my-rag"]);
+ assert_eq!(cli.rag, Some("my-rag".to_string()));
+ }
+
+ #[test]
+ fn parse_macro_flag() {
+ let cli = parse(&["--macro", "my-macro"]);
+ assert_eq!(cli.macro_name, Some("my-macro".to_string()));
+ }
+}
diff --git a/src/client/common.rs b/src/client/common.rs
index 202d732..1b84cb4 100644
--- a/src/client/common.rs
+++ b/src/client/common.rs
@@ -1,7 +1,8 @@
use super::*;
+use crate::config::paths;
use crate::{
- config::{Config, GlobalConfig, Input},
+ config::{AppConfig, Input, RequestContext},
function::{FunctionDeclaration, ToolCall, ToolResult, eval_tool_calls},
render::render_stream,
utils::*,
@@ -24,7 +25,7 @@ use tokio::sync::mpsc::unbounded_channel;
pub const MODELS_YAML: &str = include_str!("../../models.yaml");
pub static ALL_PROVIDER_MODELS: LazyLock> = LazyLock::new(|| {
- Config::local_models_override()
+ paths::local_models_override()
.ok()
.unwrap_or_else(|| serde_yaml::from_str(MODELS_YAML).unwrap())
});
@@ -37,7 +38,7 @@ static ESCAPE_SLASH_RE: LazyLock = LazyLock::new(|| Regex::new(r"(? &GlobalConfig;
+ fn app_config(&self) -> &AppConfig;
fn extra_config(&self) -> Option<&ExtraConfig>;
@@ -58,7 +59,7 @@ pub trait Client: Sync + Send {
if let Some(proxy) = extra.and_then(|v| v.proxy.as_deref()) {
builder = set_proxy(builder, proxy)?;
}
- if let Some(user_agent) = self.global_config().read().user_agent.as_ref() {
+ if let Some(user_agent) = self.app_config().user_agent.as_ref() {
builder = builder.user_agent(user_agent);
}
let client = builder
@@ -69,7 +70,7 @@ pub trait Client: Sync + Send {
}
async fn chat_completions(&self, input: Input) -> Result {
- if self.global_config().read().dry_run {
+ if self.app_config().dry_run {
let content = input.echo_messages();
return Ok(ChatCompletionsOutput::new(&content));
}
@@ -89,7 +90,7 @@ pub trait Client: Sync + Send {
let input = input.clone();
tokio::select! {
ret = async {
- if self.global_config().read().dry_run {
+ if self.app_config().dry_run {
let content = input.echo_messages();
handler.text(&content)?;
return Ok(());
@@ -413,9 +414,10 @@ pub async fn call_chat_completions(
print: bool,
extract_code: bool,
client: &dyn Client,
+ ctx: &mut RequestContext,
abort_signal: AbortSignal,
) -> Result<(String, Vec)> {
- let is_child_agent = client.global_config().read().current_depth > 0;
+ let is_child_agent = ctx.current_depth > 0;
let spinner_message = if is_child_agent { "" } else { "Generating" };
let ret = abortable_run_with_spinner(
client.chat_completions(input.clone()),
@@ -436,15 +438,13 @@ pub async fn call_chat_completions(
text = extract_code_block(&strip_think_tag(&text)).to_string();
}
if print {
- client.global_config().read().print_markdown(&text)?;
+ ctx.app.config.print_markdown(&text)?;
}
}
- let tool_results = eval_tool_calls(client.global_config(), tool_calls).await?;
- if let Some(tracker) = client.global_config().write().tool_call_tracker.as_mut() {
- tool_results
- .iter()
- .for_each(|res| tracker.record_call(res.call.clone()));
- }
+ let tool_results = eval_tool_calls(ctx, tool_calls).await?;
+ tool_results
+ .iter()
+ .for_each(|res| ctx.tool_scope.tool_tracker.record_call(res.call.clone()));
Ok((text, tool_results))
}
Err(err) => Err(err),
@@ -454,6 +454,7 @@ pub async fn call_chat_completions(
pub async fn call_chat_completions_streaming(
input: &Input,
client: &dyn Client,
+ ctx: &mut RequestContext,
abort_signal: AbortSignal,
) -> Result<(String, Vec)> {
let (tx, rx) = unbounded_channel();
@@ -461,7 +462,7 @@ pub async fn call_chat_completions_streaming(
let (send_ret, render_ret) = tokio::join!(
client.chat_completions_streaming(input, &mut handler),
- render_stream(rx, client.global_config(), abort_signal.clone()),
+ render_stream(rx, client.app_config(), abort_signal.clone()),
);
if handler.abort().aborted() {
@@ -476,12 +477,10 @@ pub async fn call_chat_completions_streaming(
if !text.is_empty() && !text.ends_with('\n') {
println!();
}
- let tool_results = eval_tool_calls(client.global_config(), tool_calls).await?;
- if let Some(tracker) = client.global_config().write().tool_call_tracker.as_mut() {
- tool_results
- .iter()
- .for_each(|res| tracker.record_call(res.call.clone()));
- }
+ let tool_results = eval_tool_calls(ctx, tool_calls).await?;
+ tool_results
+ .iter()
+ .for_each(|res| ctx.tool_scope.tool_tracker.record_call(res.call.clone()));
Ok((text, tool_results))
}
Err(err) => {
diff --git a/src/client/macros.rs b/src/client/macros.rs
index 0c8ac9d..8e8bbe4 100644
--- a/src/client/macros.rs
+++ b/src/client/macros.rs
@@ -24,7 +24,7 @@ macro_rules! register_client {
$(
#[derive(Debug)]
pub struct $client {
- global_config: $crate::config::GlobalConfig,
+ app_config: std::sync::Arc<$crate::config::AppConfig>,
config: $config,
model: $crate::client::Model,
}
@@ -32,8 +32,8 @@ macro_rules! register_client {
impl $client {
pub const NAME: &'static str = $name;
- pub fn init(global_config: &$crate::config::GlobalConfig, model: &$crate::client::Model) -> Option> {
- let config = global_config.read().clients.iter().find_map(|client_config| {
+ pub fn init(app_config: &std::sync::Arc<$crate::config::AppConfig>, model: &$crate::client::Model) -> Option> {
+ let config = app_config.clients.iter().find_map(|client_config| {
if let ClientConfig::$config(c) = client_config {
if Self::name(c) == model.client_name() {
return Some(c.clone())
@@ -43,7 +43,7 @@ macro_rules! register_client {
})?;
Some(Box::new(Self {
- global_config: global_config.clone(),
+ app_config: std::sync::Arc::clone(app_config),
config,
model: model.clone(),
}))
@@ -72,10 +72,9 @@ macro_rules! register_client {
)+
- pub fn init_client(config: &$crate::config::GlobalConfig, model: Option<$crate::client::Model>) -> anyhow::Result> {
- let model = model.unwrap_or_else(|| config.read().model.clone());
+ pub fn init_client(app_config: &std::sync::Arc<$crate::config::AppConfig>, model: $crate::client::Model) -> anyhow::Result> {
None
- $(.or_else(|| $client::init(config, &model)))+
+ $(.or_else(|| $client::init(app_config, &model)))+
.ok_or_else(|| {
anyhow::anyhow!("Invalid model '{}'", model.id())
})
@@ -101,7 +100,7 @@ macro_rules! register_client {
static ALL_CLIENT_NAMES: std::sync::OnceLock> = std::sync::OnceLock::new();
- pub fn list_client_names(config: &$crate::config::Config) -> Vec<&'static String> {
+ pub fn list_client_names(config: &$crate::config::AppConfig) -> Vec<&'static String> {
let names = ALL_CLIENT_NAMES.get_or_init(|| {
config
.clients
@@ -117,7 +116,7 @@ macro_rules! register_client {
static ALL_MODELS: std::sync::OnceLock> = std::sync::OnceLock::new();
- pub fn list_all_models(config: &$crate::config::Config) -> Vec<&'static $crate::client::Model> {
+ pub fn list_all_models(config: &$crate::config::AppConfig) -> Vec<&'static $crate::client::Model> {
let models = ALL_MODELS.get_or_init(|| {
config
.clients
@@ -131,7 +130,7 @@ macro_rules! register_client {
models.iter().collect()
}
- pub fn list_models(config: &$crate::config::Config, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
+ pub fn list_models(config: &$crate::config::AppConfig, model_type: $crate::client::ModelType) -> Vec<&'static $crate::client::Model> {
list_all_models(config).into_iter().filter(|v| v.model_type() == model_type).collect()
}
};
@@ -140,8 +139,8 @@ macro_rules! register_client {
#[macro_export]
macro_rules! client_common_fns {
() => {
- fn global_config(&self) -> &$crate::config::GlobalConfig {
- &self.global_config
+ fn app_config(&self) -> &$crate::config::AppConfig {
+ &self.app_config
}
fn extra_config(&self) -> Option<&$crate::client::ExtraConfig> {
diff --git a/src/client/model.rs b/src/client/model.rs
index c55c045..325ce47 100644
--- a/src/client/model.rs
+++ b/src/client/model.rs
@@ -3,7 +3,7 @@ use super::{
message::{Message, MessageContent, MessageContentPart},
};
-use crate::config::Config;
+use crate::config::AppConfig;
use crate::utils::{estimate_token_length, strip_think_tag};
use anyhow::{Result, bail};
@@ -44,7 +44,11 @@ impl Model {
.collect()
}
- pub fn retrieve_model(config: &Config, model_id: &str, model_type: ModelType) -> Result {
+ pub fn retrieve_model(
+ config: &AppConfig,
+ model_id: &str,
+ model_type: ModelType,
+ ) -> Result {
let models = list_all_models(config);
let (client_name, model_name) = match model_id.split_once(':') {
Some((client_name, model_name)) => {
diff --git a/src/client/oauth.rs b/src/client/oauth.rs
index 6390dc3..0a9555f 100644
--- a/src/client/oauth.rs
+++ b/src/client/oauth.rs
@@ -1,6 +1,6 @@
use super::ClientConfig;
use super::access_token::{is_valid_access_token, set_access_token};
-use crate::config::Config;
+use crate::config::paths;
use anyhow::{Result, anyhow, bail};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
@@ -178,13 +178,13 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
}
pub fn load_oauth_tokens(client_name: &str) -> Option {
- let path = Config::token_file(client_name);
+ let path = paths::token_file(client_name);
let content = fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
fn save_oauth_tokens(client_name: &str, tokens: &OAuthTokens) -> Result<()> {
- let path = Config::token_file(client_name);
+ let path = paths::token_file(client_name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
diff --git a/src/client/stream.rs b/src/client/stream.rs
index d2fe7f7..a14c461 100644
--- a/src/client/stream.rs
+++ b/src/client/stream.rs
@@ -311,10 +311,8 @@ impl JsonStreamParser {
}
self.balances.push(ch);
}
- '[' => {
- if self.start.is_some() {
- self.balances.push(ch);
- }
+ '[' if self.start.is_some() => {
+ self.balances.push(ch);
}
'}' => {
self.balances.pop();
diff --git a/src/config/agent.rs b/src/config/agent.rs
index 87490ba..6f0f302 100644
--- a/src/config/agent.rs
+++ b/src/config/agent.rs
@@ -1,4 +1,3 @@
-use super::todo::TodoList;
use super::*;
use crate::{
@@ -6,6 +5,8 @@ use crate::{
function::{Functions, run_llm_function},
};
+use super::rag_cache::RagKey;
+use crate::config::paths;
use crate::config::prompts::{
DEFAULT_SPAWN_INSTRUCTIONS, DEFAULT_TEAMMATE_INSTRUCTIONS, DEFAULT_TODO_INSTRUCTIONS,
DEFAULT_USER_INTERACTION_INSTRUCTIONS,
@@ -38,16 +39,13 @@ pub struct Agent {
rag: Option>,
model: Model,
vault: GlobalVault,
- todo_list: TodoList,
- continuation_count: usize,
- last_continuation_response: Option,
}
impl Agent {
pub fn install_builtin_agents() -> Result<()> {
info!(
"Installing built-in agents in {}",
- Config::agents_data_dir().display()
+ paths::agents_data_dir().display()
);
for file in AgentAssets::iter() {
@@ -56,7 +54,7 @@ impl Agent {
let embedded_file = AgentAssets::get(&file)
.ok_or_else(|| anyhow!("Failed to load embedded agent file: {}", file.as_ref()))?;
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
- let file_path = Config::agents_data_dir().join(file.as_ref());
+ let file_path = paths::agents_data_dir().join(file.as_ref());
let file_extension = file_path
.extension()
.and_then(OsStr::to_str)
@@ -88,14 +86,17 @@ impl Agent {
}
pub async fn init(
- config: &GlobalConfig,
+ app: &AppConfig,
+ app_state: &AppState,
+ current_model: &Model,
+ info_flag: bool,
name: &str,
abort_signal: AbortSignal,
) -> Result {
- let agent_data_dir = Config::agent_data_dir(name);
- let loaders = config.read().document_loaders.clone();
- let rag_path = Config::agent_rag_file(name, DEFAULT_AGENT_NAME);
- let config_path = Config::agent_config_file(name);
+ let agent_data_dir = paths::agent_data_dir(name);
+ let loaders = app.document_loaders.clone();
+ let rag_path = paths::agent_rag_file(name, DEFAULT_AGENT_NAME);
+ let config_path = paths::agent_config_file(name);
let mut agent_config = if config_path.exists() {
AgentConfig::load(&config_path)?
} else {
@@ -103,57 +104,33 @@ impl Agent {
};
let mut functions = Functions::init_agent(name, &agent_config.global_tools)?;
- config.write().functions.clear_mcp_meta_functions();
- let mcp_servers = if config.read().mcp_server_support {
- (!agent_config.mcp_servers.is_empty()).then(|| agent_config.mcp_servers.join(","))
- } else {
- eprintln!(
- "{}",
- formatdoc!(
- "
- This agent uses MCP servers, but MCP support is disabled.
- To enable it, exit the agent and set 'mcp_server_support: true', then try again
- "
- )
- );
- None
- };
+ agent_config.load_envs(app);
- let registry = config
- .write()
- .mcp_registry
- .take()
- .with_context(|| "MCP registry should be populated")?;
- let new_mcp_registry =
- McpRegistry::reinit(registry, mcp_servers, abort_signal.clone()).await?;
-
- if !new_mcp_registry.is_empty() {
- functions.append_mcp_meta_functions(new_mcp_registry.list_started_servers());
- }
-
- config.write().mcp_registry = Some(new_mcp_registry);
-
- agent_config.load_envs(&config.read());
-
- let model = {
- let config = config.read();
- match agent_config.model_id.as_ref() {
- Some(model_id) => Model::retrieve_model(&config, model_id, ModelType::Chat)?,
- None => {
- if agent_config.temperature.is_none() {
- agent_config.temperature = config.temperature;
- }
- if agent_config.top_p.is_none() {
- agent_config.top_p = config.top_p;
- }
- config.current_model().clone()
+ let model = match agent_config.model_id.as_ref() {
+ Some(model_id) => Model::retrieve_model(app, model_id, ModelType::Chat)?,
+ None => {
+ if agent_config.temperature.is_none() {
+ agent_config.temperature = app.temperature;
}
+ if agent_config.top_p.is_none() {
+ agent_config.top_p = app.top_p;
+ }
+ current_model.clone()
}
};
let rag = if rag_path.exists() {
- Some(Arc::new(Rag::load(config, DEFAULT_AGENT_NAME, &rag_path)?))
- } else if !agent_config.documents.is_empty() && !config.read().info_flag {
+ let key = RagKey::Agent(name.to_string());
+ let app_clone = app.clone();
+ let rag_path_clone = rag_path.clone();
+ let rag = app_state
+ .rag_cache
+ .load_with(key, || async move {
+ Rag::load(&app_clone, DEFAULT_AGENT_NAME, &rag_path_clone)
+ })
+ .await?;
+ Some(rag)
+ } else if !agent_config.documents.is_empty() && !info_flag {
let mut ans = false;
if *IS_STDOUT_TERMINAL {
ans = Confirm::new("The agent has documents attached, init RAG?")
@@ -185,9 +162,23 @@ impl Agent {
document_paths.push(path.to_string())
}
}
- let rag =
- Rag::init(config, "rag", &rag_path, &document_paths, abort_signal).await?;
- Some(Arc::new(rag))
+ let key = RagKey::Agent(name.to_string());
+ let app_clone = app.clone();
+ let rag_path_clone = rag_path.clone();
+ let rag = app_state
+ .rag_cache
+ .load_with(key, || async move {
+ Rag::init(
+ &app_clone,
+ "rag",
+ &rag_path_clone,
+ &document_paths,
+ abort_signal,
+ )
+ .await
+ })
+ .await?;
+ Some(rag)
} else {
None
}
@@ -218,10 +209,7 @@ impl Agent {
functions,
rag,
model,
- vault: Arc::clone(&config.read().vault),
- todo_list: TodoList::default(),
- continuation_count: 0,
- last_continuation_response: None,
+ vault: app_state.vault.clone(),
})
}
@@ -295,11 +283,11 @@ impl Agent {
let mut config = self.config.clone();
config.instructions = self.interpolated_instructions();
value["definition"] = json!(config);
- value["data_dir"] = Config::agent_data_dir(&self.name)
+ value["data_dir"] = paths::agent_data_dir(&self.name)
.display()
.to_string()
.into();
- value["config_file"] = Config::agent_config_file(&self.name)
+ value["config_file"] = paths::agent_config_file(&self.name)
.display()
.to_string()
.into();
@@ -323,6 +311,14 @@ impl Agent {
self.rag.clone()
}
+ pub fn append_mcp_meta_functions(&mut self, mcp_servers: Vec) {
+ self.functions.append_mcp_meta_functions(mcp_servers);
+ }
+
+ pub fn mcp_server_names(&self) -> &[String] {
+ &self.config.mcp_servers
+ }
+
pub fn conversation_starters(&self) -> Vec {
self.config
.conversation_starters
@@ -443,44 +439,6 @@ impl Agent {
self.config.escalation_timeout
}
- pub fn continuation_count(&self) -> usize {
- self.continuation_count
- }
-
- pub fn increment_continuation(&mut self) {
- self.continuation_count += 1;
- }
-
- pub fn reset_continuation(&mut self) {
- self.continuation_count = 0;
- self.last_continuation_response = None;
- }
-
- pub fn set_last_continuation_response(&mut self, response: String) {
- self.last_continuation_response = Some(response);
- }
-
- pub fn todo_list(&self) -> &TodoList {
- &self.todo_list
- }
-
- pub fn init_todo_list(&mut self, goal: &str) {
- self.todo_list = TodoList::new(goal);
- }
-
- pub fn add_todo(&mut self, task: &str) -> usize {
- self.todo_list.add(task)
- }
-
- pub fn mark_todo_done(&mut self, id: usize) -> bool {
- self.todo_list.mark_done(id)
- }
-
- pub fn clear_todo_list(&mut self) {
- self.todo_list.clear();
- self.reset_continuation();
- }
-
pub fn continuation_prompt(&self) -> String {
self.config.continuation_prompt.clone().unwrap_or_else(|| {
formatdoc! {"
@@ -696,12 +654,12 @@ impl AgentConfig {
Ok(agent_config)
}
- fn load_envs(&mut self, config: &Config) {
+ fn load_envs(&mut self, app: &AppConfig) {
let name = &self.name;
let with_prefix = |v: &str| normalize_env_name(&format!("{name}_{v}"));
if self.agent_session.is_none() {
- self.agent_session = config.agent_session.clone();
+ self.agent_session = app.agent_session.clone();
}
if let Some(v) = read_env_value::(&with_prefix("model")) {
@@ -793,7 +751,7 @@ pub struct AgentVariable {
}
pub fn list_agents() -> Vec {
- let agents_data_dir = Config::agents_data_dir();
+ let agents_data_dir = paths::agents_data_dir();
if !agents_data_dir.exists() {
return vec![];
}
@@ -803,6 +761,7 @@ pub fn list_agents() -> Vec {
for entry in entries.flatten() {
if entry.path().is_dir()
&& let Some(name) = entry.file_name().to_str()
+ && !name.starts_with('.')
{
agents.push(name.to_string());
}
@@ -813,7 +772,7 @@ pub fn list_agents() -> Vec {
}
pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option)> {
- let config_path = Config::agent_config_file(agent_name);
+ let config_path = paths::agent_config_file(agent_name);
if !config_path.exists() {
return vec![];
}
@@ -832,3 +791,89 @@ pub fn complete_agent_variables(agent_name: &str) -> Vec<(String, Option
})
.collect()
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn agent_config_parses_from_yaml() {
+ let yaml = r#"
+name: test-agent
+description: A test agent
+instructions: You are helpful
+auto_continue: true
+max_auto_continues: 5
+can_spawn_agents: true
+max_concurrent_agents: 8
+max_agent_depth: 2
+mcp_servers:
+ - github
+ - jira
+global_tools:
+ - execute_command.sh
+ - fs_read.sh
+conversation_starters:
+ - "Hello!"
+ - "How are you?"
+variables:
+ - name: username
+ description: Your name
+"#;
+
+ let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
+
+ assert_eq!(config.name, "test-agent");
+ assert_eq!(config.description, "A test agent");
+ assert!(config.auto_continue);
+ assert_eq!(config.max_auto_continues, 5);
+ assert!(config.can_spawn_agents);
+ assert_eq!(config.max_concurrent_agents, 8);
+ assert_eq!(config.max_agent_depth, 2);
+ assert_eq!(config.mcp_servers, vec!["github", "jira"]);
+ assert_eq!(config.global_tools.len(), 2);
+ assert_eq!(config.conversation_starters.len(), 2);
+ assert_eq!(config.variables.len(), 1);
+ assert_eq!(config.variables[0].name, "username");
+ }
+
+ #[test]
+ fn agent_config_defaults() {
+ let yaml = "name: minimal\ninstructions: hi\n";
+ let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
+
+ assert_eq!(config.name, "minimal");
+ assert!(!config.auto_continue);
+ assert!(!config.can_spawn_agents);
+ assert_eq!(config.max_concurrent_agents, 4);
+ assert_eq!(config.max_agent_depth, 3);
+ assert_eq!(config.max_auto_continues, 10);
+ assert!(config.mcp_servers.is_empty());
+ assert!(config.global_tools.is_empty());
+ assert!(config.conversation_starters.is_empty());
+ assert!(config.variables.is_empty());
+ assert!(config.model_id.is_none());
+ assert!(config.temperature.is_none());
+ assert!(config.top_p.is_none());
+ }
+
+ #[test]
+ fn agent_config_with_model() {
+ let yaml =
+ "name: test\nmodel: openai:gpt-4\ntemperature: 0.7\ntop_p: 0.9\ninstructions: hi\n";
+ let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
+
+ assert_eq!(config.model_id, Some("openai:gpt-4".to_string()));
+ assert_eq!(config.temperature, Some(0.7));
+ assert_eq!(config.top_p, Some(0.9));
+ }
+
+ #[test]
+ fn agent_config_inject_defaults_true() {
+ let yaml = "name: test\ninstructions: hi\n";
+ let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
+
+ assert!(config.inject_todo_instructions);
+ assert!(config.inject_spawn_instructions);
+ }
+}
diff --git a/src/config/app_config.rs b/src/config/app_config.rs
new file mode 100644
index 0000000..cdce823
--- /dev/null
+++ b/src/config/app_config.rs
@@ -0,0 +1,740 @@
+use crate::client::{ClientConfig, list_models};
+use crate::render::{MarkdownRender, RenderOptions};
+use crate::utils::{IS_STDOUT_TERMINAL, NO_COLOR, decode_bin, get_env_name};
+
+use super::paths;
+use anyhow::{Context, Result, anyhow};
+use indexmap::IndexMap;
+use serde::Deserialize;
+use std::collections::HashMap;
+use std::env;
+use std::path::PathBuf;
+use syntect::highlighting::ThemeSet;
+use terminal_colorsaurus::{ColorScheme, QueryOptions, color_scheme};
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(default)]
+pub struct AppConfig {
+ #[serde(rename(serialize = "model", deserialize = "model"))]
+ #[serde(default)]
+ pub model_id: String,
+ pub temperature: Option,
+ pub top_p: Option,
+
+ pub dry_run: bool,
+ pub stream: bool,
+ pub save: bool,
+ pub keybindings: String,
+ pub editor: Option,
+ pub wrap: Option,
+ pub wrap_code: bool,
+ pub(crate) vault_password_file: Option,
+
+ pub function_calling_support: bool,
+ pub mapping_tools: IndexMap,
+ pub enabled_tools: Option,
+ pub visible_tools: Option>,
+
+ pub mcp_server_support: bool,
+ pub mapping_mcp_servers: IndexMap,
+ pub enabled_mcp_servers: Option,
+
+ pub repl_prelude: Option,
+ pub cmd_prelude: Option,
+ pub agent_session: Option,
+
+ pub save_session: Option,
+ pub compression_threshold: usize,
+ pub summarization_prompt: Option,
+ pub summary_context_prompt: Option,
+
+ pub rag_embedding_model: Option,
+ pub rag_reranker_model: Option,
+ pub rag_top_k: usize,
+ pub rag_chunk_size: Option,
+ pub rag_chunk_overlap: Option,
+ pub rag_template: Option,
+
+ #[serde(default)]
+ pub document_loaders: HashMap,
+
+ pub highlight: bool,
+ pub theme: Option,
+ pub left_prompt: Option,
+ pub right_prompt: Option,
+
+ pub user_agent: Option,
+ pub save_shell_history: bool,
+ pub sync_models_url: Option,
+
+ pub clients: Vec,
+}
+
+impl Default for AppConfig {
+ fn default() -> Self {
+ Self {
+ model_id: Default::default(),
+ temperature: None,
+ top_p: None,
+
+ dry_run: false,
+ stream: true,
+ save: false,
+ keybindings: "emacs".into(),
+ editor: None,
+ wrap: None,
+ wrap_code: false,
+ vault_password_file: None,
+
+ function_calling_support: true,
+ mapping_tools: Default::default(),
+ enabled_tools: None,
+ visible_tools: None,
+
+ mcp_server_support: true,
+ mapping_mcp_servers: Default::default(),
+ enabled_mcp_servers: None,
+
+ repl_prelude: None,
+ cmd_prelude: None,
+ agent_session: None,
+
+ save_session: None,
+ compression_threshold: 4000,
+ summarization_prompt: None,
+ summary_context_prompt: None,
+
+ rag_embedding_model: None,
+ rag_reranker_model: None,
+ rag_top_k: 5,
+ rag_chunk_size: None,
+ rag_chunk_overlap: None,
+ rag_template: None,
+
+ document_loaders: Default::default(),
+
+ highlight: true,
+ theme: None,
+ left_prompt: None,
+ right_prompt: None,
+
+ user_agent: None,
+ save_shell_history: true,
+ sync_models_url: None,
+
+ clients: vec![],
+ }
+ }
+}
+
+impl AppConfig {
+ pub fn from_config(config: super::Config) -> Result {
+ let mut app_config = Self {
+ model_id: config.model_id,
+ temperature: config.temperature,
+ top_p: config.top_p,
+
+ dry_run: config.dry_run,
+ stream: config.stream,
+ save: config.save,
+ keybindings: config.keybindings,
+ editor: config.editor,
+ wrap: config.wrap,
+ wrap_code: config.wrap_code,
+ vault_password_file: config.vault_password_file,
+
+ function_calling_support: config.function_calling_support,
+ mapping_tools: config.mapping_tools,
+ enabled_tools: config.enabled_tools,
+ visible_tools: config.visible_tools,
+
+ mcp_server_support: config.mcp_server_support,
+ mapping_mcp_servers: config.mapping_mcp_servers,
+ enabled_mcp_servers: config.enabled_mcp_servers,
+
+ repl_prelude: config.repl_prelude,
+ cmd_prelude: config.cmd_prelude,
+ agent_session: config.agent_session,
+
+ save_session: config.save_session,
+ compression_threshold: config.compression_threshold,
+ summarization_prompt: config.summarization_prompt,
+ summary_context_prompt: config.summary_context_prompt,
+
+ rag_embedding_model: config.rag_embedding_model,
+ rag_reranker_model: config.rag_reranker_model,
+ rag_top_k: config.rag_top_k,
+ rag_chunk_size: config.rag_chunk_size,
+ rag_chunk_overlap: config.rag_chunk_overlap,
+ rag_template: config.rag_template,
+
+ document_loaders: config.document_loaders,
+
+ highlight: config.highlight,
+ theme: config.theme,
+ left_prompt: config.left_prompt,
+ right_prompt: config.right_prompt,
+
+ user_agent: config.user_agent,
+ save_shell_history: config.save_shell_history,
+ sync_models_url: config.sync_models_url,
+
+ clients: config.clients,
+ };
+ app_config.load_envs();
+ if let Some(wrap) = app_config.wrap.clone() {
+ app_config.set_wrap(&wrap)?;
+ }
+ app_config.setup_document_loaders();
+ app_config.setup_user_agent();
+ app_config.resolve_model()?;
+ Ok(app_config)
+ }
+
+ pub fn resolve_model(&mut self) -> Result<()> {
+ if self.model_id.is_empty() {
+ let models = list_models(self, crate::client::ModelType::Chat);
+ if models.is_empty() {
+ anyhow::bail!("No available model");
+ }
+ self.model_id = models[0].id();
+ }
+ Ok(())
+ }
+
+ pub fn vault_password_file(&self) -> PathBuf {
+ match &self.vault_password_file {
+ Some(path) => match path.exists() {
+ true => path.clone(),
+ false => gman::config::Config::local_provider_password_file(),
+ },
+ None => gman::config::Config::local_provider_password_file(),
+ }
+ }
+
+ pub fn editor(&self) -> Result {
+ super::EDITOR.get_or_init(move || {
+ let editor = self.editor.clone()
+ .or_else(|| env::var("VISUAL").ok().or_else(|| env::var("EDITOR").ok()))
+ .unwrap_or_else(|| {
+ if cfg!(windows) {
+ "notepad".to_string()
+ } else {
+ "nano".to_string()
+ }
+ });
+ which::which(&editor).ok().map(|_| editor)
+ })
+ .clone()
+ .ok_or_else(|| anyhow!("Editor not found. Please add the `editor` configuration or set the $EDITOR or $VISUAL environment variable."))
+ }
+
+ pub fn sync_models_url(&self) -> String {
+ self.sync_models_url
+ .clone()
+ .unwrap_or_else(|| super::SYNC_MODELS_URL.into())
+ }
+
+ pub fn light_theme(&self) -> bool {
+ matches!(self.theme.as_deref(), Some("light"))
+ }
+
+ pub fn render_options(&self) -> Result {
+ let theme = if self.highlight {
+ let theme_mode = if self.light_theme() { "light" } else { "dark" };
+ let theme_filename = format!("{theme_mode}.tmTheme");
+ let theme_path = paths::local_path(&theme_filename);
+ if theme_path.exists() {
+ let theme = ThemeSet::get_theme(&theme_path)
+ .with_context(|| format!("Invalid theme at '{}'", theme_path.display()))?;
+ Some(theme)
+ } else {
+ let theme = if self.light_theme() {
+ decode_bin(super::LIGHT_THEME).context("Invalid builtin light theme")?
+ } else {
+ decode_bin(super::DARK_THEME).context("Invalid builtin dark theme")?
+ };
+ Some(theme)
+ }
+ } else {
+ None
+ };
+ let wrap = if *IS_STDOUT_TERMINAL {
+ self.wrap.clone()
+ } else {
+ None
+ };
+ let truecolor = matches!(
+ env::var("COLORTERM").as_ref().map(|v| v.as_str()),
+ Ok("truecolor")
+ );
+ Ok(RenderOptions::new(theme, wrap, self.wrap_code, truecolor))
+ }
+
+ pub fn print_markdown(&self, text: &str) -> Result<()> {
+ if *IS_STDOUT_TERMINAL {
+ let render_options = self.render_options()?;
+ let mut markdown_render = MarkdownRender::init(render_options)?;
+ println!("{}", markdown_render.render(text));
+ } else {
+ println!("{text}");
+ }
+ Ok(())
+ }
+}
+
+impl AppConfig {
+ pub fn set_wrap(&mut self, value: &str) -> Result<()> {
+ if value == "no" {
+ self.wrap = None;
+ } else if value == "auto" {
+ self.wrap = Some(value.into());
+ } else {
+ value
+ .parse::()
+ .map_err(|_| anyhow!("Invalid wrap value"))?;
+ self.wrap = Some(value.into())
+ }
+ Ok(())
+ }
+
+ pub fn setup_document_loaders(&mut self) {
+ [("pdf", "pdftotext $1 -"), ("docx", "pandoc --to plain $1")]
+ .into_iter()
+ .for_each(|(k, v)| {
+ let (k, v) = (k.to_string(), v.to_string());
+ self.document_loaders.entry(k).or_insert(v);
+ });
+ }
+
+ pub fn setup_user_agent(&mut self) {
+ if let Some("auto") = self.user_agent.as_deref() {
+ self.user_agent = Some(format!(
+ "{}/{}",
+ env!("CARGO_CRATE_NAME"),
+ env!("CARGO_PKG_VERSION")
+ ));
+ }
+ }
+
+ pub fn load_envs(&mut self) {
+ if let Ok(v) = env::var(get_env_name("model")) {
+ self.model_id = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("temperature")) {
+ self.temperature = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("top_p")) {
+ self.top_p = v;
+ }
+
+ if let Some(Some(v)) = super::read_env_bool(&get_env_name("dry_run")) {
+ self.dry_run = v;
+ }
+ if let Some(Some(v)) = super::read_env_bool(&get_env_name("stream")) {
+ self.stream = v;
+ }
+ if let Some(Some(v)) = super::read_env_bool(&get_env_name("save")) {
+ self.save = v;
+ }
+ if let Ok(v) = env::var(get_env_name("keybindings"))
+ && v == "vi"
+ {
+ self.keybindings = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("editor")) {
+ self.editor = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("wrap")) {
+ self.wrap = v;
+ }
+ if let Some(Some(v)) = super::read_env_bool(&get_env_name("wrap_code")) {
+ self.wrap_code = v;
+ }
+
+ if let Some(Some(v)) = super::read_env_bool(&get_env_name("function_calling_support")) {
+ self.function_calling_support = v;
+ }
+ if let Ok(v) = env::var(get_env_name("mapping_tools"))
+ && let Ok(v) = serde_json::from_str(&v)
+ {
+ self.mapping_tools = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("enabled_tools")) {
+ self.enabled_tools = v;
+ }
+
+ if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
+ self.mcp_server_support = v;
+ }
+ if let Ok(v) = env::var(get_env_name("mapping_mcp_servers"))
+ && let Ok(v) = serde_json::from_str(&v)
+ {
+ self.mapping_mcp_servers = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("enabled_mcp_servers")) {
+ self.enabled_mcp_servers = v;
+ }
+
+ if let Some(v) = super::read_env_value::(&get_env_name("repl_prelude")) {
+ self.repl_prelude = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("cmd_prelude")) {
+ self.cmd_prelude = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("agent_session")) {
+ self.agent_session = v;
+ }
+
+ if let Some(v) = super::read_env_bool(&get_env_name("save_session")) {
+ self.save_session = v;
+ }
+ if let Some(Some(v)) =
+ super::read_env_value::(&get_env_name("compression_threshold"))
+ {
+ self.compression_threshold = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("summarization_prompt")) {
+ self.summarization_prompt = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("summary_context_prompt")) {
+ self.summary_context_prompt = v;
+ }
+
+ if let Some(v) = super::read_env_value::(&get_env_name("rag_embedding_model")) {
+ self.rag_embedding_model = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("rag_reranker_model")) {
+ self.rag_reranker_model = v;
+ }
+ if let Some(Some(v)) = super::read_env_value::(&get_env_name("rag_top_k")) {
+ self.rag_top_k = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("rag_chunk_size")) {
+ self.rag_chunk_size = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("rag_chunk_overlap")) {
+ self.rag_chunk_overlap = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("rag_template")) {
+ self.rag_template = v;
+ }
+
+ if let Ok(v) = env::var(get_env_name("document_loaders"))
+ && let Ok(v) = serde_json::from_str(&v)
+ {
+ self.document_loaders = v;
+ }
+
+ if let Some(Some(v)) = super::read_env_bool(&get_env_name("highlight")) {
+ self.highlight = v;
+ }
+ if *NO_COLOR {
+ self.highlight = false;
+ }
+ if self.highlight && self.theme.is_none() {
+ if let Some(v) = super::read_env_value::(&get_env_name("theme")) {
+ self.theme = v;
+ } else if *IS_STDOUT_TERMINAL
+ && let Ok(color_scheme) = color_scheme(QueryOptions::default())
+ {
+ let theme = match color_scheme {
+ ColorScheme::Dark => "dark",
+ ColorScheme::Light => "light",
+ };
+ self.theme = Some(theme.into());
+ }
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("left_prompt")) {
+ self.left_prompt = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("right_prompt")) {
+ self.right_prompt = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("user_agent")) {
+ self.user_agent = v;
+ }
+ if let Some(Some(v)) = super::read_env_bool(&get_env_name("save_shell_history")) {
+ self.save_shell_history = v;
+ }
+ if let Some(v) = super::read_env_value::(&get_env_name("sync_models_url")) {
+ self.sync_models_url = v;
+ }
+ }
+}
+
+impl AppConfig {
+ #[allow(dead_code)]
+ pub fn set_temperature_default(&mut self, value: Option) {
+ self.temperature = value;
+ }
+
+ #[allow(dead_code)]
+ pub fn set_top_p_default(&mut self, value: Option) {
+ self.top_p = value;
+ }
+
+ #[allow(dead_code)]
+ pub fn set_enabled_tools_default(&mut self, value: Option) {
+ self.enabled_tools = value;
+ }
+
+ #[allow(dead_code)]
+ pub fn set_enabled_mcp_servers_default(&mut self, value: Option) {
+ self.enabled_mcp_servers = value;
+ }
+
+ #[allow(dead_code)]
+ pub fn set_save_session_default(&mut self, value: Option) {
+ self.save_session = value;
+ }
+
+ #[allow(dead_code)]
+ pub fn set_compression_threshold_default(&mut self, value: Option) {
+ self.compression_threshold = value.unwrap_or_default();
+ }
+
+ #[allow(dead_code)]
+ pub fn set_rag_reranker_model_default(&mut self, value: Option) {
+ self.rag_reranker_model = value;
+ }
+
+ #[allow(dead_code)]
+ pub fn set_rag_top_k_default(&mut self, value: usize) {
+ self.rag_top_k = value;
+ }
+
+ #[allow(dead_code)]
+ pub fn set_model_id_default(&mut self, model_id: String) {
+ self.model_id = model_id;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::config::Config;
+
+ fn cached_editor() -> Option {
+ super::super::EDITOR.get().cloned().flatten()
+ }
+
+ #[test]
+ fn from_config_copies_serialized_fields() {
+ let cfg = Config {
+ model_id: "test-model".to_string(),
+ temperature: Some(0.7),
+ top_p: Some(0.9),
+ dry_run: true,
+ stream: false,
+ save: true,
+ highlight: false,
+ compression_threshold: 2000,
+ rag_top_k: 10,
+ clients: vec![ClientConfig::default()],
+ ..Config::default()
+ };
+
+ let app = AppConfig::from_config(cfg).unwrap();
+
+ assert_eq!(app.model_id, "test-model");
+ assert_eq!(app.temperature, Some(0.7));
+ assert_eq!(app.top_p, Some(0.9));
+ assert!(app.dry_run);
+ assert!(!app.stream);
+ assert!(app.save);
+ assert!(!app.highlight);
+ assert_eq!(app.compression_threshold, 2000);
+ assert_eq!(app.rag_top_k, 10);
+ }
+
+ #[test]
+ fn from_config_copies_clients() {
+ let cfg = Config {
+ model_id: "test-model".to_string(),
+ clients: vec![ClientConfig::default()],
+ ..Config::default()
+ };
+ let app = AppConfig::from_config(cfg).unwrap();
+
+ assert_eq!(app.clients.len(), 1);
+ }
+
+ #[test]
+ fn from_config_copies_mapping_fields() {
+ let mut cfg = Config {
+ model_id: "test-model".to_string(),
+ clients: vec![ClientConfig::default()],
+ ..Config::default()
+ };
+ cfg.mapping_tools
+ .insert("alias".to_string(), "real_tool".to_string());
+ cfg.mapping_mcp_servers
+ .insert("gh".to_string(), "github-mcp".to_string());
+
+ let app = AppConfig::from_config(cfg).unwrap();
+
+ assert_eq!(
+ app.mapping_tools.get("alias"),
+ Some(&"real_tool".to_string())
+ );
+ assert_eq!(
+ app.mapping_mcp_servers.get("gh"),
+ Some(&"github-mcp".to_string())
+ );
+ }
+
+ #[test]
+ fn editor_returns_configured_value() {
+ let configured = cached_editor()
+ .unwrap_or_else(|| std::env::current_exe().unwrap().display().to_string());
+ let app = AppConfig {
+ editor: Some(configured.clone()),
+ ..AppConfig::default()
+ };
+
+ assert_eq!(app.editor().unwrap(), configured);
+ }
+
+ #[test]
+ fn editor_falls_back_to_env() {
+ if let Some(expected) = cached_editor() {
+ let app = AppConfig::default();
+ assert_eq!(app.editor().unwrap(), expected);
+ return;
+ }
+
+ let expected = std::env::current_exe().unwrap().display().to_string();
+ unsafe {
+ std::env::set_var("VISUAL", &expected);
+ }
+
+ let app = AppConfig::default();
+ let result = app.editor();
+
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), expected);
+ }
+
+ #[test]
+ fn light_theme_default_is_false() {
+ let app = AppConfig::default();
+ assert!(!app.light_theme());
+ }
+
+ #[test]
+ fn sync_models_url_has_default() {
+ let app = AppConfig::default();
+ let url = app.sync_models_url();
+ assert!(!url.is_empty());
+ }
+
+ #[test]
+ fn from_config_copies_serde_fields() {
+ let cfg = Config {
+ model_id: "provider:model-x".to_string(),
+ temperature: Some(0.42),
+ compression_threshold: 1234,
+ ..Config::default()
+ };
+
+ let app = AppConfig::from_config(cfg).unwrap();
+
+ assert_eq!(app.model_id, "provider:model-x");
+ assert_eq!(app.temperature, Some(0.42));
+ assert_eq!(app.compression_threshold, 1234);
+ }
+
+ #[test]
+ fn from_config_installs_default_document_loaders() {
+ let cfg = Config {
+ model_id: "provider:test".to_string(),
+ ..Config::default()
+ };
+ let app = AppConfig::from_config(cfg).unwrap();
+
+ assert_eq!(
+ app.document_loaders.get("pdf"),
+ Some(&"pdftotext $1 -".to_string())
+ );
+ assert_eq!(
+ app.document_loaders.get("docx"),
+ Some(&"pandoc --to plain $1".to_string())
+ );
+ }
+
+ #[test]
+ fn from_config_resolves_auto_user_agent() {
+ let cfg = Config {
+ model_id: "provider:test".to_string(),
+ user_agent: Some("auto".to_string()),
+ ..Config::default()
+ };
+
+ let app = AppConfig::from_config(cfg).unwrap();
+
+ let ua = app.user_agent.as_deref().unwrap();
+ assert!(ua != "auto", "user_agent should have been resolved");
+ assert!(ua.contains('/'), "user_agent should be '/'");
+ }
+
+ #[test]
+ fn from_config_preserves_explicit_user_agent() {
+ let cfg = Config {
+ model_id: "provider:test".to_string(),
+ user_agent: Some("custom/1.0".to_string()),
+ ..Config::default()
+ };
+
+ let app = AppConfig::from_config(cfg).unwrap();
+
+ assert_eq!(app.user_agent.as_deref(), Some("custom/1.0"));
+ }
+
+ #[test]
+ fn from_config_validates_wrap_value() {
+ let cfg = Config {
+ model_id: "provider:test".to_string(),
+ wrap: Some("invalid".to_string()),
+ ..Config::default()
+ };
+
+ let result = AppConfig::from_config(cfg);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn from_config_accepts_wrap_auto() {
+ let cfg = Config {
+ model_id: "provider:test".to_string(),
+ wrap: Some("auto".to_string()),
+ ..Config::default()
+ };
+
+ let app = AppConfig::from_config(cfg).unwrap();
+ assert_eq!(app.wrap.as_deref(), Some("auto"));
+ }
+
+ #[test]
+ fn resolve_model_errors_when_no_models_available() {
+ let mut app = AppConfig {
+ model_id: String::new(),
+ clients: vec![],
+ ..AppConfig::default()
+ };
+
+ let result = app.resolve_model();
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn resolve_model_keeps_explicit_model_id() {
+ let mut app = AppConfig {
+ model_id: "provider:explicit".to_string(),
+ ..AppConfig::default()
+ };
+
+ app.resolve_model().unwrap();
+ assert_eq!(app.model_id, "provider:explicit");
+ }
+}
diff --git a/src/config/app_state.rs b/src/config/app_state.rs
new file mode 100644
index 0000000..d3e8332
--- /dev/null
+++ b/src/config/app_state.rs
@@ -0,0 +1,93 @@
+use super::mcp_factory::{McpFactory, McpServerKey};
+use super::rag_cache::RagCache;
+use crate::config::AppConfig;
+use crate::function::Functions;
+use crate::mcp::{McpRegistry, McpServersConfig};
+use crate::utils::AbortSignal;
+use crate::vault::{GlobalVault, Vault};
+
+use anyhow::Result;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+#[derive(Clone)]
+pub struct AppState {
+ pub config: Arc,
+ pub vault: GlobalVault,
+ pub mcp_factory: Arc,
+ pub rag_cache: Arc,
+ pub mcp_config: Option,
+ pub mcp_log_path: Option,
+ pub mcp_registry: Option>,
+ pub functions: Functions,
+}
+
+impl AppState {
+ #[cfg(test)]
+ pub fn test_default() -> Self {
+ Self {
+ config: Arc::new(AppConfig::default()),
+ vault: Arc::new(Vault::default()),
+ mcp_factory: Arc::new(McpFactory::default()),
+ rag_cache: Arc::new(RagCache::default()),
+ mcp_config: None,
+ mcp_log_path: None,
+ mcp_registry: None,
+ functions: Functions::default(),
+ }
+ }
+
+ pub async fn init(
+ config: Arc,
+ log_path: Option,
+ start_mcp_servers: bool,
+ abort_signal: AbortSignal,
+ ) -> Result {
+ let vault = Arc::new(Vault::init(&config));
+
+ let mcp_registry = McpRegistry::init(
+ log_path,
+ start_mcp_servers,
+ config.enabled_mcp_servers.clone(),
+ abort_signal,
+ &config,
+ &vault,
+ )
+ .await?;
+
+ let mcp_config = mcp_registry.mcp_config().cloned();
+ let mcp_log_path = mcp_registry.log_path().cloned();
+
+ let mcp_factory = Arc::new(McpFactory::default());
+ if let Some(mcp_servers_config) = &mcp_config {
+ for (id, handle) in mcp_registry.running_servers() {
+ if let Some(spec) = mcp_servers_config.mcp_servers.get(id) {
+ let key = McpServerKey::from_spec(id, spec);
+ mcp_factory.insert_active(key, handle);
+ }
+ }
+ }
+
+ let mut functions = Functions::init(config.visible_tools.as_ref().unwrap_or(&Vec::new()))?;
+ if !mcp_registry.is_empty() && config.mcp_server_support {
+ functions.append_mcp_meta_functions(mcp_registry.list_started_servers());
+ }
+
+ let mcp_registry = if mcp_registry.is_empty() {
+ None
+ } else {
+ Some(Arc::new(mcp_registry))
+ };
+
+ Ok(Self {
+ config,
+ vault,
+ mcp_factory,
+ rag_cache: Arc::new(RagCache::default()),
+ mcp_config,
+ mcp_log_path,
+ mcp_registry,
+ functions,
+ })
+ }
+}
diff --git a/src/config/input.rs b/src/config/input.rs
index ab4e029..f030ab6 100644
--- a/src/config/input.rs
+++ b/src/config/input.rs
@@ -9,7 +9,7 @@ use crate::utils::{AbortSignal, base64_encode, is_loader_protocol, sha256};
use anyhow::{Context, Result, bail};
use indexmap::IndexSet;
-use std::{collections::HashMap, fs::File, io::Read};
+use std::{collections::HashMap, fs::File, io::Read, sync::Arc};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
const IMAGE_EXTS: [&str; 5] = ["png", "jpeg", "jpg", "webp", "gif"];
@@ -17,7 +17,11 @@ const SUMMARY_MAX_WIDTH: usize = 80;
#[derive(Debug, Clone)]
pub struct Input {
- config: GlobalConfig,
+ app_config: Arc,
+ stream_enabled: bool,
+ session: Option,
+ rag: Option>,
+ functions: Option>,
text: String,
raw: (String, Vec),
patched_text: Option,
@@ -34,10 +38,15 @@ pub struct Input {
}
impl Input {
- pub fn from_str(config: &GlobalConfig, text: &str, role: Option) -> Self {
- let (role, with_session, with_agent) = resolve_role(&config.read(), role);
+ pub fn from_str(ctx: &RequestContext, text: &str, role: Option) -> Self {
+ let (role, with_session, with_agent) = resolve_role(ctx, role);
+ let captured = capture_input_config(ctx, &role);
Self {
- config: config.clone(),
+ app_config: Arc::clone(&ctx.app.config),
+ stream_enabled: captured.stream_enabled,
+ session: captured.session,
+ rag: captured.rag,
+ functions: captured.functions,
text: text.to_string(),
raw: (text.to_string(), vec![]),
patched_text: None,
@@ -55,12 +64,12 @@ impl Input {
}
pub async fn from_files(
- config: &GlobalConfig,
+ ctx: &RequestContext,
raw_text: &str,
paths: Vec,
role: Option,
) -> Result {
- let loaders = config.read().document_loaders.clone();
+ let loaders = ctx.app.config.document_loaders.clone();
let (raw_paths, local_paths, remote_urls, external_cmds, protocol_paths, with_last_reply) =
resolve_paths(&loaders, paths)?;
let mut last_reply = None;
@@ -78,7 +87,7 @@ impl Input {
texts.push(raw_text.to_string());
};
if with_last_reply {
- if let Some(LastMessage { input, output, .. }) = config.read().last_message.as_ref() {
+ if let Some(LastMessage { input, output, .. }) = ctx.last_message.as_ref() {
if !output.is_empty() {
last_reply = Some(output.clone())
} else if let Some(v) = input.last_reply.as_ref() {
@@ -102,9 +111,14 @@ impl Input {
));
}
}
- let (role, with_session, with_agent) = resolve_role(&config.read(), role);
+ let (role, with_session, with_agent) = resolve_role(ctx, role);
+ let captured = capture_input_config(ctx, &role);
Ok(Self {
- config: config.clone(),
+ app_config: Arc::clone(&ctx.app.config),
+ stream_enabled: captured.stream_enabled,
+ session: captured.session,
+ rag: captured.rag,
+ functions: captured.functions,
text: texts.join("\n"),
raw: (raw_text.to_string(), raw_paths),
patched_text: None,
@@ -122,14 +136,14 @@ impl Input {
}
pub async fn from_files_with_spinner(
- config: &GlobalConfig,
+ ctx: &RequestContext,
raw_text: &str,
paths: Vec,
role: Option,
abort_signal: AbortSignal,
) -> Result {
abortable_run_with_spinner(
- Input::from_files(config, raw_text, paths, role),
+ Input::from_files(ctx, raw_text, paths, role),
"Loading files",
abort_signal,
)
@@ -164,7 +178,7 @@ impl Input {
}
pub fn stream(&self) -> bool {
- self.config.read().stream && !self.role().model().no_stream()
+ self.stream_enabled && !self.role().model().no_stream()
}
pub fn continue_output(&self) -> Option<&str> {
@@ -183,10 +197,9 @@ impl Input {
self.regenerate
}
- pub fn set_regenerate(&mut self) {
- let role = self.config.read().extract_role();
- if role.name() == self.role().name() {
- self.role = role;
+ pub fn set_regenerate(&mut self, current_role: Role) {
+ if current_role.name() == self.role().name() {
+ self.role = current_role;
}
self.regenerate = true;
self.tool_calls = None;
@@ -196,9 +209,10 @@ impl Input {
if self.text.is_empty() {
return Ok(());
}
- let rag = self.config.read().rag.clone();
- if let Some(rag) = rag {
- let result = Config::search_rag(&self.config, &rag, &self.text, abort_signal).await?;
+ if let Some(rag) = &self.rag {
+ let result = rag
+ .search_with_template(&self.app_config, &self.text, abort_signal)
+ .await?;
self.patched_text = Some(result);
self.rag_name = Some(rag.name().to_string());
}
@@ -220,7 +234,7 @@ impl Input {
}
pub fn create_client(&self) -> Result> {
- init_client(&self.config, Some(self.role().model().clone()))
+ init_client(&self.app_config, self.role().model().clone())
}
pub async fn fetch_chat_text(&self) -> Result {
@@ -240,7 +254,7 @@ impl Input {
model.guard_max_input_tokens(&messages)?;
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
let functions = if model.supports_function_calling() {
- let fns = self.config.read().select_functions(self.role());
+ let fns = self.functions.clone();
if let Some(vec) = &fns {
for def in vec {
debug!("Function definition: {:?}", def.name);
@@ -260,7 +274,7 @@ impl Input {
}
pub fn build_messages(&self) -> Result> {
- let mut messages = if let Some(session) = self.session(&self.config.read().session) {
+ let mut messages = if let Some(session) = self.session(&self.session) {
session.build_messages(self)
} else {
self.role().build_messages(self)
@@ -275,7 +289,7 @@ impl Input {
}
pub fn echo_messages(&self) -> String {
- if let Some(session) = self.session(&self.config.read().session) {
+ if let Some(session) = self.session(&self.session) {
session.echo_messages(self)
} else {
self.role().echo_messages(self)
@@ -384,17 +398,33 @@ impl Input {
}
}
-fn resolve_role(config: &Config, role: Option) -> (Role, bool, bool) {
+fn resolve_role(ctx: &RequestContext, role: Option) -> (Role, bool, bool) {
match role {
Some(v) => (v, false, false),
None => (
- config.extract_role(),
- config.session.is_some(),
- config.agent.is_some(),
+ ctx.extract_role(ctx.app.config.as_ref()),
+ ctx.session.is_some(),
+ ctx.agent.is_some(),
),
}
}
+struct CapturedInputConfig {
+ stream_enabled: bool,
+ session: Option,
+ rag: Option>,
+ functions: Option>,
+}
+
+fn capture_input_config(ctx: &RequestContext, role: &Role) -> CapturedInputConfig {
+ CapturedInputConfig {
+ stream_enabled: ctx.app.config.stream,
+ session: ctx.session.clone(),
+ rag: ctx.rag.clone(),
+ functions: ctx.select_functions(role),
+ }
+}
+
type ResolvePathsOutput = (
Vec,
Vec,
@@ -548,3 +578,390 @@ fn read_media_to_data_url(image_path: &str) -> Result {
Ok(data_url)
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::config::request_context::RequestContext;
+ use crate::config::{AppState, WorkingMode};
+ use std::fs;
+ use std::sync::Arc;
+ use std::time::SystemTime;
+
+ fn default_app_state() -> Arc {
+ Arc::new(AppState::test_default())
+ }
+
+ fn create_test_ctx() -> RequestContext {
+ RequestContext::new(default_app_state(), WorkingMode::Cmd)
+ }
+
+ #[test]
+ fn resolve_role_with_explicit_role() {
+ let ctx = create_test_ctx();
+ let role = Role::new("custom", "be helpful");
+ let (resolved, with_session, with_agent) = resolve_role(&ctx, Some(role));
+ assert_eq!(resolved.name(), "custom");
+ assert!(!with_session);
+ assert!(!with_agent);
+ }
+
+ #[test]
+ fn resolve_role_without_role_no_session_no_agent() {
+ let ctx = create_test_ctx();
+ let (resolved, with_session, with_agent) = resolve_role(&ctx, None);
+ assert_eq!(resolved.name(), "");
+ assert!(!with_session);
+ assert!(!with_agent);
+ }
+
+ #[test]
+ fn resolve_role_without_role_with_session() {
+ let mut ctx = create_test_ctx();
+ ctx.session = Some(Session::default());
+ let (_resolved, with_session, with_agent) = resolve_role(&ctx, None);
+ assert!(with_session);
+ assert!(!with_agent);
+ }
+
+ #[test]
+ fn resolve_role_explicit_role_overrides_session_flag() {
+ let mut ctx = create_test_ctx();
+ ctx.session = Some(Session::default());
+ let role = Role::new("explicit", "prompt");
+ let (_resolved, with_session, _with_agent) = resolve_role(&ctx, Some(role));
+ assert!(!with_session);
+ }
+
+ #[test]
+ fn resolve_paths_detects_last_reply_syntax() {
+ let loaders = HashMap::new();
+ let (_, _, _, _, _, with_last_reply) =
+ resolve_paths(&loaders, vec!["%%".to_string()]).unwrap();
+ assert!(with_last_reply);
+ }
+
+ #[test]
+ fn resolve_paths_detects_url() {
+ let loaders = HashMap::new();
+ let (_, local, remote, _, _, _) =
+ resolve_paths(&loaders, vec!["https://example.com".to_string()]).unwrap();
+ assert!(local.is_empty());
+ assert_eq!(remote, vec!["https://example.com"]);
+ }
+
+ #[test]
+ fn resolve_paths_detects_external_command() {
+ let loaders = HashMap::new();
+ let (_, _, _, external, _, _) =
+ resolve_paths(&loaders, vec!["`echo hello`".to_string()]).unwrap();
+ assert_eq!(external, vec!["echo hello"]);
+ }
+
+ #[test]
+ fn resolve_paths_empty_input() {
+ let loaders = HashMap::new();
+ let (raw, local, remote, external, protocol, with_last) =
+ resolve_paths(&loaders, vec![]).unwrap();
+ assert!(raw.is_empty());
+ assert!(local.is_empty());
+ assert!(remote.is_empty());
+ assert!(external.is_empty());
+ assert!(protocol.is_empty());
+ assert!(!with_last);
+ }
+
+ #[test]
+ fn resolve_paths_rejects_url_with_glob_suffix() {
+ let loaders = HashMap::new();
+ let result = resolve_paths(&loaders, vec!["https://example.com**".to_string()]);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn resolve_paths_mixed_inputs() {
+ let loaders = HashMap::new();
+ let paths = vec![
+ "%%".to_string(),
+ "https://example.com".to_string(),
+ "`ls`".to_string(),
+ ];
+ let (_, _, remote, external, _, with_last) = resolve_paths(&loaders, paths).unwrap();
+ assert!(with_last);
+ assert_eq!(remote.len(), 1);
+ assert_eq!(external.len(), 1);
+ }
+
+ #[test]
+ fn input_from_str_captures_text() {
+ let ctx = create_test_ctx();
+ let input = Input::from_str(&ctx, "hello world", None);
+ assert_eq!(input.text(), "hello world");
+ }
+
+ #[test]
+ fn input_from_str_with_explicit_role() {
+ let ctx = create_test_ctx();
+ let role = Role::new("pirate", "you are a pirate");
+ let input = Input::from_str(&ctx, "ahoy", Some(role));
+ assert_eq!(input.role().name(), "pirate");
+ assert!(!input.with_agent());
+ }
+
+ #[test]
+ fn input_from_str_captures_stream_from_config() {
+ let mut state = AppState::test_default();
+ let mut config = (*state.config).clone();
+ config.stream = false;
+ state.config = Arc::new(config);
+ let ctx = RequestContext::new(Arc::new(state), WorkingMode::Cmd);
+ let input = Input::from_str(&ctx, "test", None);
+ assert!(!input.stream_enabled);
+ }
+
+ #[test]
+ fn input_is_empty_with_no_text_and_no_medias() {
+ let ctx = create_test_ctx();
+ let input = Input::from_str(&ctx, "", None);
+ assert!(input.is_empty());
+ }
+
+ #[test]
+ fn input_is_not_empty_with_text() {
+ let ctx = create_test_ctx();
+ let input = Input::from_str(&ctx, "hello", None);
+ assert!(!input.is_empty());
+ }
+
+ #[test]
+ fn input_set_text_changes_text() {
+ let ctx = create_test_ctx();
+ let mut input = Input::from_str(&ctx, "original", None);
+ input.set_text("modified".to_string());
+ assert_eq!(input.text(), "modified");
+ }
+
+ #[test]
+ fn input_text_returns_patched_when_set() {
+ let ctx = create_test_ctx();
+ let mut input = Input::from_str(&ctx, "original", None);
+ input.patched_text = Some("patched".to_string());
+ assert_eq!(input.text(), "patched");
+ }
+
+ #[test]
+ fn input_clear_patch_restores_original() {
+ let ctx = create_test_ctx();
+ let mut input = Input::from_str(&ctx, "original", None);
+ input.patched_text = Some("patched".to_string());
+ input.clear_patch();
+ assert_eq!(input.text(), "original");
+ }
+
+ #[test]
+ fn input_set_continue_output_accumulates() {
+ let ctx = create_test_ctx();
+ let mut input = Input::from_str(&ctx, "test", None);
+ assert!(input.continue_output().is_none());
+ input.set_continue_output("first ");
+ assert_eq!(input.continue_output(), Some("first "));
+ input.set_continue_output("second");
+ assert_eq!(input.continue_output(), Some("first second"));
+ }
+
+ #[test]
+ fn input_set_regenerate_sets_flag_and_clears_tool_calls() {
+ let ctx = create_test_ctx();
+ let mut input = Input::from_str(&ctx, "test", None);
+ let role = input.role().clone();
+ assert!(!input.regenerate());
+ input.set_regenerate(role);
+ assert!(input.regenerate());
+ assert!(input.tool_calls().is_none());
+ }
+
+ #[test]
+ fn input_summary_truncates_long_text() {
+ let ctx = create_test_ctx();
+ let long_text = "a".repeat(200);
+ let input = Input::from_str(&ctx, &long_text, None);
+ let summary = input.summary();
+ assert!(summary.len() < 200);
+ assert!(summary.ends_with("..."));
+ }
+
+ #[test]
+ fn input_summary_preserves_short_text() {
+ let ctx = create_test_ctx();
+ let input = Input::from_str(&ctx, "short", None);
+ assert_eq!(input.summary(), "short");
+ }
+
+ #[test]
+ fn input_raw_with_no_files() {
+ let ctx = create_test_ctx();
+ let input = Input::from_str(&ctx, "hello", None);
+ assert_eq!(input.raw(), "hello");
+ }
+
+ #[test]
+ fn input_render_with_no_medias() {
+ let ctx = create_test_ctx();
+ let input = Input::from_str(&ctx, "hello", None);
+ assert_eq!(input.render(), "hello");
+ }
+
+ #[test]
+ fn input_with_agent_false_when_no_agent() {
+ let ctx = create_test_ctx();
+ let input = Input::from_str(&ctx, "test", None);
+ assert!(!input.with_agent());
+ }
+
+ #[test]
+ fn input_session_returns_none_when_with_session_false() {
+ let ctx = create_test_ctx();
+ let input = Input::from_str(&ctx, "test", Some(Role::new("r", "p")));
+ let session = Some(Session::default());
+ assert!(input.session(&session).is_none());
+ }
+
+ #[test]
+ fn input_session_returns_some_when_with_session_true() {
+ let mut ctx = create_test_ctx();
+ ctx.session = Some(Session::default());
+ let input = Input::from_str(&ctx, "test", None);
+ let session = Some(Session::default());
+ assert!(input.session(&session).is_some());
+ }
+
+ #[test]
+ fn is_image_recognizes_image_extensions() {
+ assert!(is_image("photo.png"));
+ assert!(is_image("photo.jpeg"));
+ assert!(is_image("photo.jpg"));
+ assert!(is_image("photo.webp"));
+ assert!(is_image("photo.gif"));
+ }
+
+ #[test]
+ fn is_image_rejects_non_image_extensions() {
+ assert!(!is_image("file.txt"));
+ assert!(!is_image("file.rs"));
+ assert!(!is_image("file.pdf"));
+ }
+
+ #[test]
+ fn resolve_data_url_returns_path_for_known_hash() {
+ let mut data_urls = HashMap::new();
+ let data_url = "data:image/png;base64,abc123";
+ let hash = sha256(data_url);
+ data_urls.insert(hash, "/path/to/image.png".to_string());
+ let result = resolve_data_url(&data_urls, data_url.to_string());
+ assert_eq!(result, "/path/to/image.png");
+ }
+
+ #[test]
+ fn resolve_data_url_returns_original_for_non_data_url() {
+ let data_urls = HashMap::new();
+ let result = resolve_data_url(&data_urls, "https://example.com/image.png".to_string());
+ assert_eq!(result, "https://example.com/image.png");
+ }
+
+ fn run_async(f: F) -> F::Output {
+ tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .unwrap()
+ .block_on(f)
+ }
+
+ #[test]
+ fn from_files_loads_single_text_file() {
+ let dir = env::temp_dir().join(format!(
+ "loki-input-test-{}",
+ SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ create_dir_all(&dir).unwrap();
+ let file_path = dir.join("test.txt");
+ fs::write(&file_path, "file content here").unwrap();
+
+ let ctx = create_test_ctx();
+ let input = run_async(Input::from_files(
+ &ctx,
+ "question",
+ vec![file_path.to_string_lossy().to_string()],
+ None,
+ ))
+ .unwrap();
+
+ assert!(input.text().contains("file content here"));
+ assert!(input.text().contains("question"));
+ let _ = fs::remove_dir_all(&dir);
+ }
+
+ #[test]
+ fn from_files_loads_multiple_files() {
+ let dir = env::temp_dir().join(format!(
+ "loki-input-test-multi-{}",
+ SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ create_dir_all(&dir).unwrap();
+ fs::write(dir.join("a.txt"), "content A").unwrap();
+ fs::write(dir.join("b.txt"), "content B").unwrap();
+
+ let ctx = create_test_ctx();
+ let input = run_async(Input::from_files(
+ &ctx,
+ "question",
+ vec![
+ dir.join("a.txt").to_string_lossy().to_string(),
+ dir.join("b.txt").to_string_lossy().to_string(),
+ ],
+ None,
+ ))
+ .unwrap();
+
+ assert!(input.text().contains("content A"));
+ assert!(input.text().contains("content B"));
+ let _ = fs::remove_dir_all(&dir);
+ }
+
+ #[test]
+ fn from_files_with_no_paths_just_text() {
+ let ctx = create_test_ctx();
+ let input = run_async(Input::from_files(&ctx, "just text", vec![], None)).unwrap();
+ assert_eq!(input.text(), "just text");
+ }
+
+ #[test]
+ fn from_files_with_external_command() {
+ let ctx = create_test_ctx();
+ let input = run_async(Input::from_files(
+ &ctx,
+ "question",
+ vec!["`echo hello from cmd`".to_string()],
+ None,
+ ))
+ .unwrap();
+ assert!(input.text().contains("hello from cmd"));
+ }
+
+ #[test]
+ fn from_files_nonexistent_file_errors() {
+ let ctx = create_test_ctx();
+ let result = run_async(Input::from_files(
+ &ctx,
+ "question",
+ vec!["/nonexistent/path/xyz.txt".to_string()],
+ None,
+ ));
+ assert!(result.is_err());
+ }
+}
diff --git a/src/config/macros.rs b/src/config/macros.rs
index 2b93c5d..fcdef5d 100644
--- a/src/config/macros.rs
+++ b/src/config/macros.rs
@@ -1,12 +1,12 @@
-use crate::config::{Config, GlobalConfig, RoleLike, ensure_parent_exists};
+use crate::config::paths;
+use crate::config::{RequestContext, RoleLike, ensure_parent_exists};
use crate::repl::{run_repl_command, split_args_text};
use crate::utils::{AbortSignal, multiline_text};
-use anyhow::{Result, anyhow};
+use anyhow::{Context, Result, anyhow};
use indexmap::IndexMap;
-use parking_lot::RwLock;
use rust_embed::Embed;
use serde::Deserialize;
-use std::fs::File;
+use std::fs::{File, read_to_string};
use std::io::Write;
use std::sync::Arc;
@@ -16,12 +16,12 @@ struct MacroAssets;
#[async_recursion::async_recursion]
pub async fn macro_execute(
- config: &GlobalConfig,
+ ctx: &mut RequestContext,
name: &str,
args: Option<&str>,
abort_signal: AbortSignal,
) -> Result<()> {
- let macro_value = Config::load_macro(name)?;
+ let macro_value = Macro::load(name)?;
let (mut new_args, text) = split_args_text(args.unwrap_or_default(), cfg!(windows));
if !text.is_empty() {
new_args.push(text.to_string());
@@ -29,25 +29,42 @@ pub async fn macro_execute(
let variables = macro_value
.resolve_variables(&new_args)
.map_err(|err| anyhow!("{err}. Usage: {}", macro_value.usage(name)))?;
- let role = config.read().extract_role();
- let mut config = config.read().clone();
- config.temperature = role.temperature();
- config.top_p = role.top_p();
- config.enabled_tools = role.enabled_tools().clone();
- config.enabled_mcp_servers = role.enabled_mcp_servers().clone();
- config.macro_flag = true;
- config.model = role.model().clone();
- config.role = None;
- config.session = None;
- config.rag = None;
- config.agent = None;
- config.discontinuous_last_message();
- let config = Arc::new(RwLock::new(config));
- config.write().macro_flag = true;
+ let role = ctx.extract_role(ctx.app.config.as_ref());
+ let mut app_config = (*ctx.app.config).clone();
+ app_config.temperature = role.temperature();
+ app_config.top_p = role.top_p();
+ app_config.enabled_tools = role.enabled_tools().clone();
+ app_config.enabled_mcp_servers = role.enabled_mcp_servers().clone();
+
+ let mut app_state = (*ctx.app).clone();
+ app_state.config = Arc::new(app_config);
+
+ let mut macro_ctx = RequestContext::new(Arc::new(app_state), ctx.working_mode);
+ macro_ctx.macro_flag = true;
+ macro_ctx.info_flag = ctx.info_flag;
+ macro_ctx.model = role.model().clone();
+ macro_ctx.agent_variables = ctx.agent_variables.clone();
+ macro_ctx.last_message = ctx.last_message.clone();
+ macro_ctx.supervisor = ctx.supervisor.clone();
+ macro_ctx.parent_supervisor = ctx.parent_supervisor.clone();
+ macro_ctx.self_agent_id = ctx.self_agent_id.clone();
+ macro_ctx.inbox = ctx.inbox.clone();
+ macro_ctx.escalation_queue = ctx.escalation_queue.clone();
+ macro_ctx.current_depth = ctx.current_depth;
+ macro_ctx.auto_continue_count = ctx.auto_continue_count;
+ macro_ctx.todo_list = ctx.todo_list.clone();
+ macro_ctx.tool_scope.tool_tracker = ctx.tool_scope.tool_tracker.clone();
+ macro_ctx.discontinuous_last_message();
+
+ let app = macro_ctx.app.config.clone();
+ macro_ctx
+ .bootstrap_tools(app.as_ref(), true, abort_signal.clone())
+ .await?;
+
for step in ¯o_value.steps {
let command = Macro::interpolate_command(step, &variables);
println!(">> {}", multiline_text(&command));
- run_repl_command(&config, abort_signal.clone(), &command).await?;
+ run_repl_command(&mut macro_ctx, abort_signal.clone(), &command).await?;
}
Ok(())
}
@@ -60,10 +77,18 @@ pub struct Macro {
}
impl Macro {
+ pub fn load(name: &str) -> Result {
+ let path = paths::macro_file(name);
+ let err = || format!("Failed to load macro '{name}' at '{}'", path.display());
+ let content = read_to_string(&path).with_context(err)?;
+ let value: Macro = serde_yaml::from_str(&content).with_context(err)?;
+ Ok(value)
+ }
+
pub fn install_macros() -> Result<()> {
info!(
"Installing built-in macros in {}",
- Config::macros_dir().display()
+ paths::macros_dir().display()
);
for file in MacroAssets::iter() {
@@ -71,7 +96,7 @@ impl Macro {
let embedded_file = MacroAssets::get(&file)
.ok_or_else(|| anyhow!("Failed to load embedded macro file: {}", file.as_ref()))?;
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
- let file_path = Config::macros_dir().join(file.as_ref());
+ let file_path = paths::macros_dir().join(file.as_ref());
if file_path.exists() {
debug!(
@@ -144,3 +169,205 @@ pub struct MacroVariable {
pub rest: bool,
pub default: Option,
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn var(name: &str, rest: bool, default: Option<&str>) -> MacroVariable {
+ MacroVariable {
+ name: name.to_string(),
+ rest,
+ default: default.map(String::from),
+ }
+ }
+
+ fn macro_with_vars(vars: Vec) -> Macro {
+ Macro {
+ variables: vars,
+ steps: vec![],
+ }
+ }
+
+ #[test]
+ fn resolve_no_variables() {
+ let m = macro_with_vars(vec![]);
+ let result = m.resolve_variables(&[]).unwrap();
+ assert!(result.is_empty());
+ }
+
+ #[test]
+ fn resolve_required_variable_provided() {
+ let m = macro_with_vars(vec![var("name", false, None)]);
+ let result = m.resolve_variables(&["Alice".into()]).unwrap();
+ assert_eq!(result["name"], "Alice");
+ }
+
+ #[test]
+ fn resolve_required_variable_missing_errors() {
+ let m = macro_with_vars(vec![var("name", false, None)]);
+ let result = m.resolve_variables(&[]);
+ assert!(result.is_err());
+ assert!(result.unwrap_err().to_string().contains("name"));
+ }
+
+ #[test]
+ fn resolve_default_variable_uses_default() {
+ let m = macro_with_vars(vec![var("color", false, Some("blue"))]);
+ let result = m.resolve_variables(&[]).unwrap();
+ assert_eq!(result["color"], "blue");
+ }
+
+ #[test]
+ fn resolve_default_variable_overridden() {
+ let m = macro_with_vars(vec![var("color", false, Some("blue"))]);
+ let result = m.resolve_variables(&["red".into()]).unwrap();
+ assert_eq!(result["color"], "red");
+ }
+
+ #[test]
+ fn resolve_rest_variable_captures_all_remaining() {
+ let m = macro_with_vars(vec![var("first", false, None), var("rest", true, None)]);
+ let result = m
+ .resolve_variables(&["a".into(), "b".into(), "c".into()])
+ .unwrap();
+ assert_eq!(result["first"], "a");
+ assert_eq!(result["rest"], "b c");
+ }
+
+ #[test]
+ fn resolve_rest_variable_with_default() {
+ let m = macro_with_vars(vec![var("args", true, Some("default text"))]);
+ let result = m.resolve_variables(&[]).unwrap();
+ assert_eq!(result["args"], "default text");
+ }
+
+ #[test]
+ fn resolve_multiple_variables() {
+ let m = macro_with_vars(vec![
+ var("a", false, None),
+ var("b", false, None),
+ var("c", false, Some("default_c")),
+ ]);
+ let result = m.resolve_variables(&["x".into(), "y".into()]).unwrap();
+ assert_eq!(result["a"], "x");
+ assert_eq!(result["b"], "y");
+ assert_eq!(result["c"], "default_c");
+ }
+
+ #[test]
+ fn usage_no_variables() {
+ let m = macro_with_vars(vec![]);
+ assert_eq!(m.usage("my-macro"), "my-macro");
+ }
+
+ #[test]
+ fn usage_required_variable() {
+ let m = macro_with_vars(vec![var("name", false, None)]);
+ assert_eq!(m.usage("greet"), "greet ");
+ }
+
+ #[test]
+ fn usage_optional_variable() {
+ let m = macro_with_vars(vec![var("color", false, Some("blue"))]);
+ assert_eq!(m.usage("paint"), "paint [color]");
+ }
+
+ #[test]
+ fn usage_rest_variable() {
+ let m = macro_with_vars(vec![var("args", true, None)]);
+ assert_eq!(m.usage("run"), "run ...");
+ }
+
+ #[test]
+ fn usage_rest_with_default() {
+ let m = macro_with_vars(vec![var("args", true, Some("default"))]);
+ assert_eq!(m.usage("run"), "run [args]...");
+ }
+
+ #[test]
+ fn usage_mixed_variables() {
+ let m = macro_with_vars(vec![
+ var("target", false, None),
+ var("flags", true, Some("")),
+ ]);
+ assert_eq!(m.usage("build"), "build [flags]...");
+ }
+
+ #[test]
+ fn interpolate_replaces_variables() {
+ let vars = IndexMap::from([("name".to_string(), "world".to_string())]);
+ let result = Macro::interpolate_command("hello {{name}}", &vars);
+ assert_eq!(result, "hello world");
+ }
+
+ #[test]
+ fn interpolate_multiple_variables() {
+ let vars = IndexMap::from([
+ ("a".to_string(), "1".to_string()),
+ ("b".to_string(), "2".to_string()),
+ ]);
+ let result = Macro::interpolate_command("{{a}} + {{b}}", &vars);
+ assert_eq!(result, "1 + 2");
+ }
+
+ #[test]
+ fn interpolate_no_variables_passthrough() {
+ let vars = IndexMap::new();
+ let result = Macro::interpolate_command("no vars here", &vars);
+ assert_eq!(result, "no vars here");
+ }
+
+ #[test]
+ fn interpolate_variable_not_found_left_as_is() {
+ let vars = IndexMap::new();
+ let result = Macro::interpolate_command("hello {{missing}}", &vars);
+ assert_eq!(result, "hello {{missing}}");
+ }
+
+ #[test]
+ fn deserialize_macro_from_yaml() {
+ let yaml = r#"
+steps:
+ - ".role coder"
+ - "write code for {{task}}"
+variables:
+ - name: task
+"#;
+ let m: Macro = serde_yaml::from_str(yaml).unwrap();
+ assert_eq!(m.steps.len(), 2);
+ assert_eq!(m.variables.len(), 1);
+ assert_eq!(m.variables[0].name, "task");
+ assert!(!m.variables[0].rest);
+ assert!(m.variables[0].default.is_none());
+ }
+
+ #[test]
+ fn deserialize_macro_with_defaults() {
+ let yaml = r#"
+steps:
+ - "test"
+variables:
+ - name: mode
+ default: "fast"
+ - name: args
+ rest: true
+ default: "none"
+"#;
+ let m: Macro = serde_yaml::from_str(yaml).unwrap();
+ assert_eq!(m.variables[0].default, Some("fast".to_string()));
+ assert!(m.variables[1].rest);
+ assert_eq!(m.variables[1].default, Some("none".to_string()));
+ }
+
+ #[test]
+ fn deserialize_macro_no_variables() {
+ let yaml = r#"
+steps:
+ - ".help"
+"#;
+ let m: Macro = serde_yaml::from_str(yaml).unwrap();
+ assert!(m.variables.is_empty());
+ assert_eq!(m.steps.len(), 1);
+ }
+}
diff --git a/src/config/mcp_factory.rs b/src/config/mcp_factory.rs
new file mode 100644
index 0000000..9484ba0
--- /dev/null
+++ b/src/config/mcp_factory.rs
@@ -0,0 +1,306 @@
+use crate::mcp::{ConnectedServer, JsonField, McpServer, McpTransportType, spawn_mcp_server};
+
+use anyhow::Result;
+use parking_lot::Mutex;
+use std::collections::HashMap;
+use std::path::Path;
+use std::sync::{Arc, Weak};
+
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub struct McpServerKey {
+ pub name: String,
+ pub transport: McpTransportKey,
+}
+
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub enum McpTransportKey {
+ Stdio {
+ command: String,
+ args: Vec,
+ env: Vec<(String, String)>,
+ },
+ Remote {
+ transport_type: McpTransportType,
+ url: String,
+ headers: Vec<(String, String)>,
+ },
+}
+
+impl McpServerKey {
+ pub fn from_spec(name: &str, spec: &McpServer) -> Self {
+ let transport = if spec.is_remote() {
+ let url = spec.url.clone().unwrap_or_default();
+ let mut headers: Vec<(String, String)> = spec
+ .headers
+ .as_ref()
+ .map(|h| h.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
+ .unwrap_or_default();
+ headers.sort();
+ McpTransportKey::Remote {
+ transport_type: spec.transport_type.clone(),
+ url,
+ headers,
+ }
+ } else {
+ let command = spec.command.clone().unwrap_or_default();
+ let mut args = spec.args.clone().unwrap_or_default();
+ args.sort();
+ let mut env: Vec<(String, String)> = spec
+ .env
+ .as_ref()
+ .map(|e| {
+ e.iter()
+ .map(|(k, v)| {
+ let v_str = match v {
+ JsonField::Str(s) => s.clone(),
+ JsonField::Bool(b) => b.to_string(),
+ JsonField::Int(i) => i.to_string(),
+ };
+ (k.clone(), v_str)
+ })
+ .collect()
+ })
+ .unwrap_or_default();
+ env.sort();
+ McpTransportKey::Stdio { command, args, env }
+ };
+ Self {
+ name: name.into(),
+ transport,
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct McpFactory {
+ active: Mutex>>,
+}
+
+impl McpFactory {
+ pub fn try_get_active(&self, key: &McpServerKey) -> Option> {
+ let map = self.active.lock();
+ map.get(key).and_then(|weak| weak.upgrade())
+ }
+
+ pub fn insert_active(&self, key: McpServerKey, handle: &Arc) {
+ let mut map = self.active.lock();
+ map.insert(key, Arc::downgrade(handle));
+ }
+
+ pub async fn acquire(
+ &self,
+ name: &str,
+ spec: &McpServer,
+ log_path: Option<&Path>,
+ ) -> Result