From 85f7ad15e537a75b0724daa46cf7821a7e95cb70 Mon Sep 17 00:00:00 2001 From: Liam Rohan Date: Tue, 15 Sep 2026 14:13:22 +0100 Subject: [PATCH] init --- .gitattributes | 2 + .gitignore | 9 + CONTRIBUTING.md | 32 ++ Cargo.lock | 1191 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 29 + LICENSE | 21 + README.md | 54 ++ docs/assets/resume.png | Bin 0 -> 165846 bytes docs/configuration.md | 68 +++ scripts/render_demo.py | 75 +++ src/index.rs | 542 ++++++++++++++++++ src/main.rs | 162 ++++++ src/opencode.rs | 210 +++++++ src/parse.rs | 416 ++++++++++++++ src/search.rs | 201 +++++++ src/session.rs | 206 +++++++ src/ui.rs | 738 +++++++++++++++++++++++++ src/ui/demo.rs | 178 ++++++ tests/cli.rs | 202 +++++++ tests/pty_smoke.py | 230 ++++++++ 20 files changed, 4566 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/assets/resume.png create mode 100644 docs/configuration.md create mode 100644 scripts/render_demo.py create mode 100644 src/index.rs create mode 100644 src/main.rs create mode 100644 src/opencode.rs create mode 100644 src/parse.rs create mode 100644 src/search.rs create mode 100644 src/session.rs create mode 100644 src/ui.rs create mode 100644 src/ui/demo.rs create mode 100644 tests/cli.rs create mode 100644 tests/pty_smoke.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d0d6371 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.png binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70a6e68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target/ +.DS_Store +__pycache__/ +*.py[cod] +.venv/ +.env +.env.* +/cache/ +/sessions/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8f533b6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Keep changes focused. Open an issue before adding a major feature or dependency. Semantic search, dashboards, remote syncing and background services are outside the project's scope. + +## Checks + +Requires Rust, a C compiler and Python 3 for the terminal tests. + +```sh +cargo fmt --check +cargo clippy --locked --all-targets -- -D warnings +cargo test --locked +cargo build --release --locked +python3 tests/pty_smoke.py target/release/cont +``` + +Tests use fictional data. Terminal tests launch stub executables in place of real agents. + +## Bug reports and fixes + +Include steps to reproduce and the relevant agent version. For parser bugs, provide a minimal fictional JSONL or SQLite fixture. Add a regression test with bug fixes where practical. + +Do not commit real session histories, caches, credentials or screenshots of private work. + +## Code structure + +- `src/parse.rs`: Pi, Claude Code, Codex and OMP JSONL parsing. +- `src/opencode.rs`: OpenCode SQLite reader. +- `src/index.rs`: session discovery and caching. +- `src/search.rs`: title and human-message search. +- `src/session.rs`: session metadata and agent launch commands. +- `src/ui.rs`: terminal UI. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4afa31c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1191 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compact_str" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.5", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "cont" +version = "0.2.0" +dependencies = [ + "anyhow", + "clap", + "crossterm", + "dirs", + "nucleo-matcher", + "ratatui", + "rayon", + "rusqlite", + "serde", + "serde_json", + "tempfile", + "walkdir", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c1bb441 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "cont" +version = "0.2.0" +edition = "2024" +description = "A tiny, local session picker for Pi, Claude Code, Codex, OpenCode, and OMP" +license = "MIT" +repository = "https://src.liamrohan.com/ldr/cont" +homepage = "https://src.liamrohan.com/ldr/cont" +readme = "README.md" +keywords = ["cli", "tui", "agents", "sessions", "search"] +categories = ["command-line-utilities", "development-tools"] + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +crossterm = "0.28" +dirs = "6" +nucleo-matcher = "0.3" +ratatui = "0.29" +rayon = "1" +rusqlite = { version = "0.38", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["raw_value"] } +tempfile = "3" +walkdir = "2" + +[profile.release] +lto = "thin" +strip = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..06681b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 resume contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ebf757 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# cont + +Find and resume coding agent sessions from the terminal. Supports Pi, Claude Code, Codex, OpenCode and Oh My Pi. + +## Install + +Requires Rust and a C compiler. The agents you use must be on your `PATH`. + +```sh +cargo install --git https://src.liamrohan.com/ldr/cont.git --locked +``` + +## Usage + +```sh +cont # sessions in the current directory +cont --all # sessions across all directories +cont --all auth refactor # search sessions +cont --agent claude # filter by agent +cont -C ~/code/my-project # use a different directory +cont --all --list postgres # search without the terminal UI +cont --all --json # JSON output +cont --print # print the selected session's command +``` + +Type to search titles and human messages. Press Enter to resume a session in its original directory. + +| Key | Action | +| --- | --- | +| Up / Down | Move selection | +| Tab | Switch between the current directory and all directories | +| Shift-Tab | Cycle agents | +| Ctrl-L | Choose a directory | +| Ctrl-R | Refresh sessions | +| Esc | Clear search, then quit | +| Ctrl-C | Quit | + +Directory filtering matches the exact directory, excluding subdirectories. + +Run `cont --help` for all options. See [configuration](docs/configuration.md) for session paths, custom stores and search behaviour. + +## Privacy + +Reads local session stores without uploading conversations or modifying session logs. The cache contains human messages in plaintext under `${XDG_CACHE_HOME:-~/.cache}/cont/`. Use `--no-cache` to disable it, or delete that directory to remove cached text. + +## Development + +See [CONTRIBUTING.md](CONTRIBUTING.md) for build and test instructions. + +## Credits + +Based on and [coding-agent-session-search](https://github.com/dicklesworthstone/coding_agent_session_search). + +[MIT licence](LICENSE). diff --git a/docs/assets/resume.png b/docs/assets/resume.png new file mode 100644 index 0000000000000000000000000000000000000000..7466968e72919b0db3ffc7d6193f7fc5742b090c GIT binary patch literal 165846 zcmeFZcTiJZ+dj&pkDydhks?J=snR>4SZIRuCM|$~NN+-D0Tn4yMWi&+GesbLRWanKQq0W`2KsnLW;CyR!D)YpwfU*L~gBoi|z<%2eb` z9+S0-TISB6k08+Ze>J|0C3 zSHQFAQi<1054){Mz8P?FzkK5ElI9*aK3=(et$H}xYTU`u@e65WSw5)(*^Sq)weTpl z!io5Rz#=1ocW;BqRc4NSy;%(^L@Go)dV1C}db^~^A<8gtzkfc};> z3ihQpW!EhG^55V7*V9ZrZSR$morK-9T=>O5_xGP?{K1#b9NGEhgNue1G);}~&-L*; z0r#|pd*XsN=oud8o%>IF#=`dI-44oDU5mSVfBLxi{&JA|6F?OMt3bTqU$bq z&bdFWBG0I$dSSDCf+3XFpX~n9{@1r^a{sInQz#af#u4I_;k(g5qOe;>SPy`SZp2 z=IW$sIN3j~Nl3UDQGZTJYUh=Or>OrsTWPhhxErYOr(u>M`}BXB{qLE{mrobSFPv-# z${(Qu4{nFPn!Nz-vh@*nF-+z7?6sm3%CW7U^CdFNtMiz^RxHx$Z^PqRn!u+=3+9H$ zvHf7SzEHhQO7mKfrHN0t(zw2r$uT+UbFj;2T*o0OIf&_jW@$b5@;aP0l+XI99G_T? zwUXag^zfK94>+NS2KEii`3FN7c@ZbIy!bCVy2qN#0ggX;c|K>TIG(;PWpY&1YHO)H zZf@mk7Wn(LIUh4!q@LqX7d=6pvU^S1f0dn%EopYm`sQW%z{pv_7eCjzIBgU6>r#e} zw;w%#JHcQQZI?ltuMDK_6nW;Y_*&}p+mCDNY9mxE??KI+w!i#d z1=q(dPj43>dt4|#HaaT1muJ4c^}N@lMn*>1z`;sa-zZ@BOU6o3MZ(Axa>e^PqdDyZ z@S`(g#;Lij`vhA}*3qW=!otEKa-?x8qsP_O+c|Z$ep_2xqT+*eICJTMEiF&|+HFVd zaq|gbWBX)?aAJ}a*e)!@&a-wL%E~Lid)$U?s==?vwgoU0}So>e?%2)$*z5XRQacDOayP?6rWif8Sc$OL%@hl1ndM69VpH&48;&i>ZxIZ+oj`6z?2VJ&lG z9q+>*FZ79Cre}8MCJbPCn>W%i)^Ouj-SHv$g$K>W?!@+c@mC6!#!BV)AIP6vFJrng*f}O}L+U7|X^(irWs(IZ|7&WB zi=8_zcl4Rl9g}j`_JYHPwEme_%M(3s+Io79mt&59jJ5R&f0q~hCNFqPm}jlq@T8CU z&O`p>!#Onrdr@OTrnb<9$;M;NqxotxGvDQYX2fjr`*(7ltuh2FU=8^qxDZ8!6T(Q- z^XHW`PQldF(M0$Wv{{{Le3_Yv795vTcQFnEkB6)! zV7;MDgv*5FdkSZQS{iqv@GSDaclRmcN*o|$gf)f+ZE0TMIA1_ccXy@E8JlXDmzU$% zrjq6HZG_I^>ax7eK67UUMPf;~x{443<-yL1WZ;^$vR|tC-StiM^3j=BR|T4mT8P;4 zuH?5e?Y;6UrTFp2`R>lda;gx1dc&BhpX;x4DyJPMzf&xD1y?%w$1PK*__~GzE!<6D|?4uSZPlkZ)&-yp(4LiQ4zuxg|{tH?sTIm((=r>7i8t_RkLF)(@VE${ft$zb~!5)NP}2f#bIw zx@D4PeT0_N`ktMqB=>Z8w{v!`d_;30;<@V}dIo<|-}hnn>0 z*vbkE`^On)6HnXcru{kXA{y4retS?f-F+m3$lK|-1@w%%em)Oo-VXR)qE<@D%` zk}wI$RO^70)Cr-s31@A)w4KhvvKqXXIO%hgiOU1#H1M?Nax9-qlmD}mwbl@6RAa@I zdk?R`F{&clN@t%ky~n7r!5w3$Al#erL&onT3e{~PIH+fs_Cs=OlR}f)mY~5RKRzr#=4B0+30{_ef0vyg!=Gv85 zJCOyVSx!~%5?D|6&h~@L5Ru*t8PCJdVSk4zK7Pu8o;A{jmTylCr7$f=DYYQd?s!~- zsg8;k8$ezogB2A*6%}AGSkq>|E?5awm7ow7zj7pq@4FINqq_qF2vl>phS=@*X=#HAlUVLyz`5 z3MU(zr1Yl!URVmZIQZN5^)559q6Y>Dn8FjTt|n{#jcRkhGE^~gh&=Rjy7}H(4S)7? z1F%KE7@rAZ1+XGABl70Kfp}$m{F0sFTnB@d@8$6O$Ch zZ=GYB`ONKjN2lU$_I?b8hn@66Vl72%6~=(JtQyAf)k{P0ua{Z&NaIp`SBH#^?9t$x z7WSE$rL~nAj6^JQC(~15Q&qNzGd-GJoH$!z7$oAykZ4C1EJ*0h8U3YAjuh$yT$JY1e zn(hN%p~Rwy!cRg3&w z5|8;~w%50+z-Xm}CRWdHfW?m!fHGcT-Z{#YiH%D)GB$l|U_1!pnpn0GWl+q?%WJb1 zW~3q|D8#jMvD+hk&yPu!mu!dkFO<*!)w&WtO?B@EyQdhMBd*=o*F)FJRynjd9qi72 zx>l%dT`DRhCNQOiK`n1J8!l5=$Wwf>vbdOEm`{G5+|z@=@PP*!k(U?~wpFKQK>|E4 zZ2ZU*p_6amupgZbNrSU}ZH>8COD|GixAoV{8yY?Q-Re>7mo8nMpPG7XrVlmKcROpZ z;%)7?KWKFPvYDD%y7QW;f+*9qLTNlVJ31akMpcqu!W>{1Fke4g7ujz_3!LkH3-FJi zDsV_8+wU>0UQ;&6Q2~<;UcQ5pD5v9_#|vT0JZO2;G>FNWv0A#4MNL! z_@ce4%4>DiDN(?NlOqr7Lk#Mb+dC*JDY-GU2S7k8ZBVo8#@E`M_w5^R;-Z=NA1Peg z-%=L!1^Y=qx?(|KmB@`ywL`kElF->DjqSzbqnCyMaV@`t#V=foYio@d-TfYFS}DJ! z;RKjC)l+>jbbP6cFJ>I|NK3`red&sT3-*9*e@!>=$jRzKIv70uqsqfkY9IO%7)1Fe zK#ME5t62Kt)gxqYK4*qa8d&DCrwB(bOy9h zuJO3jpcw15eYg3+B=N6F<=M`?3eGqk4$fQZ((b=R5IS~iTkUo7AGHf^fc>}v$446* z*>a$fF4_Osd}w6k20OrCG5!mjhC_QL0le?jGrJZ!G8jt^iq3;6vZM^UcOF3QUwm|d zL(w;i{NJh_dCs$ie%{RD1A&b7U`n0Bw~_DqC8ZbY3qM`QPff;T?qRV2QuaR+b-Tcg z5n+fVEn|B5rK>Jx^IApvKGzWI7Q%Zp$wRe5=_3%^!s*OtwIPRd~dd9IMd zXas2OK9#b#_mKs^rg1;68(k{saF1)RnO{weQnDJIW5kiVExl8OI(sQA*9TgJRYaIP z^R7e%S^3zx!Od2mu^~RMa|Pdb&mTXDe6lXOmDKx;35U>-yMu~C` z^V;5rLhS#-S-?WSFu)d{B(z&ol3yTh?kpb^1qEgMqs4}B22;?HyJ{YBVY_4@Vfuw` zs-uIc$n;rbJGYnZ)|F*tQJQJivhe;nM)v+;EnAH%tvwr$RMVeKv@$VXcCp1<2r_i2 zMTCB?S}F2izeE>jVU|vsT~K~lF)^lSz0E>geBLAR6pOa+ETFLeD!5_);ha(M zf0b?JS1_}9^2Cp#yJb1#_DZ5+C>vI$gEFtu&5evfiY8o}ZnxCLFTe92;qALX`|#1j zm^Yehi;L~juqD#Lx8a*lXy2%*q|10#p-@3lQBJPUz6He`enC$>@Qq0|gStu?W@(MX zPb40+WDnXSL0}Vdz*>bwqGH8rYxD1w;`Mmq)eMU~2Bo(B3Y({T!+~*JY(3?B7yt?! zU0Ru8%9ONQ4EF?$;v8V3&bGFhDRQ!6aNkm~Icr;hE{rv$JeGc~?5VM^cqVX|p&C6- z@*s(K%>B+*dCZKUX5lAaE&>Mu!SB_XqN}@tvorB54sC?pz90Z+%St$rV)}kwP8AuP z{6gC$;Mp=}rWcP+Lx#heii%m9ru}l=0~prdZgz-`uuWr%+{w^)z?VI8adC+&UDA2l z2`Tko2t5&y5%yY&^hvN1+gR%NWYMJMu1OoijLs~u-D3Y9oS2%z)d_Sv61L@e{ye|i4vLmmTnK`^gm*A=u|?`4fb`M+ylxTfATg|6w(rz^iOWM&8h%_|dI54reL4@m8L;aYZskcvW+gMS3Fe4h^ z>d~<=s6Vb(WZ_P@+9OX-5B%C%esR8xAI~fp@CVM9SWvS*J6mP%W)8uN)ztcVR#t=9 zbL601nZ+DkBmV+}NubX>A^?1P>0N(=|Dh<8ldct9dwM?d-s{^@RGyIPCclI9YxGzz zzvlV0f{sn?3D|PaRz$?64JFO%jCddWj;&hucRo9FQTaE(n7KImI`_P?dB_c%Vo7>B zi{d#SKlweC{G|nc0zhpGnfsZFod%)Gt@ffR;pOipAo7AVA(o;{&cn&map`Q&y`JC7 z&8e@eC5{R8{j)k5n1a)@50XYdeEy|J%Pr7@5)zQO>nK<`9a~Iw@fxL*x3$CBZ=?#R zbX4`L|AgBMAj~r%oX=5DKE>R)!bJ(xZlKrDi z3Y#7p6flM#kC}Qu7&;0be!5$v^AcLtyqJ1c6rB>YHNQ z?Qj)Ft5as_yxFA-E!Sg`VpY$-PCey%NETTbOdFq<)vOCETS*?CeYL><*bBE+7G6Gs zNq9w?;zix8xE)mN53S4i&Gv{+aeUmM$;G5s&i_Rc9@u=)?B|3B3GpTQ>i ze=`&e4ui~RBG18CM|L?=`)c3g3Lk=Ymqu zXX7r!I01}TouOlHNWkUVwyuzpkp>qx5&~%VEC{*f@9wL*9)9Gyr~Z2hg5h*F3aP3( zq^W}X9;El|lXiY%jmsCFeT@C`p}5F@s`9Qv3Pr@IYG>Nl`%+a8%`CN-gTznrLeCAWNKC!Qu8Mt|wj2>lwBrBO~L2|KC+EKHZz% zdnxq*y5!USm#$A!QSa;3Png!eWzfT*rOw~ zz*%~}IhE*|I7Twfj(f}8tF4%?hvS?5AM4iw2ir%)Z2BwX+Nha;vo`qfRcrsf+^2*O zrr;hRtuQ+{-Xy=6oWjlyrlU7-rzT8cysYP@RD0`4Z6N|7w|u`jl+DOo%^ANcyDHJ- z9uZUVNPA@X+1LB&U+=dJ70VKuP7FUEwKSGKS;jHd*&})@^XooXL}drHTm1TzZ`ajc zER&*L<M1$ped1<4y^~w2-=}Y4 zOvEbTj1j)ROFg#wtlgg=nP?rIarE%uz(BUAd;d8yAA~x&8L|V@?KzHq1ksH7Q7@k6 zx5U_^Wo2ZT*f`~`v6*AXgB+b`sV{GMZ6yr#_Br^@nRh_;T2gYUu3lX|Y8%A4nGnxG z3qdVQE>+dck8?SHYtVyXym%(a#I1LArOWPR*fVBk+%fiq=e=rYg*ZQdqloQpPg*Q$ z5nV(Ny+TVS-{jMKIMLIRjYZ}=I`#+b41i_t+NQC*c>~qP~JKAj}(-H=SW4N9U>=-Qwt6n6kTJutMo&rkLP0F zJ<*_0P{=oc?C!k@(^YZpx58@pu8buZK97h=-IG6oZ^>tJP1NH%MnlR4ggFLY$zr{8 zgorw&?dhJL;1J8xF!!HF=HLT3{a075n(mJt><|0-@#+=)iXw)LS3SzogSuF}-6+H| zpG>gniHq+{LZNXS!6imE`69FT@9XT3EAlGL#nWd#e?F+5BHY;~aW}gU5M}hZg)|wj zPZilH?776^(Tl8%4zh@k4c46|0uwKs#2FfE)_Sd(1J#Y?0$!F~$PYNaw?iGRiYRC- zkv=$Fq)5)ItFI4jayOKS`S3RE%LhSDdP6d@bk$-E%xe&%M~7Q@nkjE1%H-G0#!6T1 zIPXjAbAS#6|FdI^M$yP|uxAD|Y(K#yLn)ThO#AbCs41zdVXlDw>kOv=WGI70%N6qP zT~ccA26Bx8nY?}Xe0}#?Tl1X#@Mv5vDNolrZ=`CmhKedhM5KoYanRmrE1g9QO$Qhp zj|!1e^ljp&s|byShUsHvC#O}_aDEp*JQ7;iZ|@}T=s!5}9a)yOmOhlphFJD#vs*Mb z)TUk)4M5>lmj@L6OgeM8EAD^&2tpX8?6UHPx#2$e*_f-45(i=Qrf;XsQr9`)s|@wu zv-A$jTVdF34T&N9qb&!p#Q3WknJ6x4exvNZgv6_>V-+Ywt=+D<%I;^dD1oDTbPHqG ztS^5jPrcoDS}|BvY}>G@!GNM(u_Gq%F?{nqLY{4CILWT*Xd8*S!*0yUnfFr=P~-)d znvR|=HXij4hyaU9rF3$F51et}Kpk(HA7iYRqH8=R9-nL%qWS#}s1T~R$%DfvonFj4 z2W5>}JhOLls!PixPA05-R&&CMLI~e9RQFA*OS#>qAGEl-0_$q(5IO2G;p(GxKDq!b zGD!Osh|y|qu=n>(8kn0yGI|c|yI&y+Z)RVfTUs_jH5@&e)rC3w6&53HB$unp%ETl# zOe$cmlx;@MQjc|l2RXGp{~e~$ z#z=>S%md{g;bV7TR*^nKPS!T=5O0a zaR^HXw5HwO*`e+n8b8AE_NMHcrZqYoKoZ&MF;vwn2nem`>eTcwrq4prgW zLrsvzb-M?3>zm)NTZ)d}$yjstBWnZZp6rqED=p2i1Vuz99?Nx^FoIik8n|c`im|LZ zkrCH;cxnc3$~NDitysV&$T?{=MF37I=^Cyl6H*q~tGxMrJGC`!#lsVcz9t?pfbPC| z+sU{TW6jg$sm~CviuCW6#Z@(cy|Y4Cv#J{8gX(j0d&AUKwrMDhYPNRL$0i#$6pc)i zVC=JZ_XnkPpLPid@bU}rz2i;1lsbmnS>5~8Sd}vH-8J0(eBQ=%1MdRAX>%NMG}x4N<-LXDS&N#B&b<=!3+8MOD*do(kt zS+y{44Z^uzBiGazmlqWg4!Tw?f=zG7JJ_!Nv)f!~A0A2G+p4fRAYL0CMddw8-Wp&t zkv<>(xEdpznqNVfiV5h~(P3wA-P!Tbl$~W}U5gigSfW5-n|bJ`39AnC&b?)lP#~M= zvo8?nz{jg=C+Y>w$jIk@NkMO)p0-p`u4Fx{Zf3?-jIx@hI1z+By2ZX4ub(NiyG_UD zs695Ck|!A)B2Ism%E`QyBPu9}ILPgUKtLB4N&HNLa6 zeRG3~!TG|{woAj|^U^oPeBJcInv7H--#dpE(@YP~J~&N05O{z?bwG-5H#M2>g zZ+vR$Q<$z{utb%nh@9NsE3rFOm|h}$d^vwKrDKJVH+zZm_AO1;{Hj`$imwW796sK@ z5kJaq9z?T^xFqiZ&hb5u{&Pp2#|n|9fm(`28&LusR-webT~gA&gZYgD+Qv%dhWh%} z))r+QCUt0Q-}>+@d^OUbZ@JTPBds-*P_nYNHd*Hr2gTaSwSYv|)Q5rFw-A0l2Y|%{ zY~t_rQd(`C64|QTM(qiMnRiIZ+qBAnz`ZqOAMA|^w+LNjixu=BN31D;ZG71rwRc}w zNbU|;Edb(O?%%*3mpGpMr6%Iw@UXp0{-_K$XopNscW{if5sxok?U#U}FshDe>7MuQ zbq5ENBXpcxT#`#%o;~B{UlV6|l%8$|o+6Pzn4A!Pbqx#EN23SFjdkoVP^M5&P!&6| z=%eeVt`n|6XkxNfBO<3A%xzERHDxCz;+3Nl)+UBrh4QkiT8Ah5B<7y+31oVUVT1&~ zaNl~Ik0!-dbwOmidpgzfbG*bi9Oy*1_ zA#lun;1WI!e@kUx*eVW?m7SO^8O_O2rw3`PgcdR{BD|`xBO~b zOP>TE6bfpWJ!F3($e_U{KYs9F=SSnO;ajwCqSectd+IF}M+OEefQ7@H{0E!0tG)N? z(#NnyRqiO&Vvw1U`3&RCi-3k`y&v*pV#C#M9@bvQRet$+9slIMPF+}ykM|BaC5MiB zt})aF>FDHCH9mmpAD^39njF`2@u@0Ce*cc1Xx5f60It9HkL&+7priBTA;p|(&_T~O z>`j=%qb<+vJJ$GBwCUsFfr$xi69-0~FM(@mXh_vidar#KYVd8GuIEZp7SieOYV0t^ zx$*s^@b{miJnXGK(E=1>lN$8QqOakMv*!{wzPT`Yc#vgu4~|G}@D%H08*PsAqBwkW zbGia|7ByMDKOuG_%sbP!w~-4yJ_tlq;t>xo_lii3r>2perSvuZF}aoJAT z3>xCrFD@aPDD&yO<1IfbQf+>yb>nc>ZQP=fr?#o0aW-_d3Yo9^Y09x!n}C~=4! z-@W33B>~CbY&1{F8ogR}AGPf~f5{q%^w@O_Mtd(5ROWne8B0s8&r?NQiUsBmLN?OJ zwQ&`lVU^tzujrJrw7mCr->BI5Jj6nWQkkw9r(E;Y;GZ-|bO)RF$}^XP-E5eP`N>@i z*WC$X;8^N#l8!le;E}u@vQ`G)5`@3W@h`) z*$c$dthMz<0lwv+NGzL;*;!~cH6c07i}>EXfLnHxPfeZ zz3r$=0CK6tX&{hj;wK=$SVq09&ff2Yp0jN1nDkvSMJS2P0gxqa??9B_IVycb;meZz z=A;Dk>`7zQM!^z1K;~%5(b;(gQiiB~r~p`W!1i}fG=L}-=ye*#i@IGJwDgT~bqeXR zRXreqZ^~?9n#ZggGw#Uth;jhwT{soZ(=7E0E||yVCj^xyOyoKjsy9g9 z`e*N26HIyw-c{LJ76cnnQs!Hv9@B_KSzTSzj~{cIrk#!!dfYU_PPV39yaop+Kcwq% zg*FIS4eSLr{4_GxTwU#yy3@6SC4`DgDp-l0qOmBw~1*DjCQiL%PAw~tcM z6zCNIiA!WJG5W}=5OwJtko;MWUG_H`DTg8W@a(M7YpQSyo|TZo#X5 zw%Cu%J$C-j6s2|}>N)LC&DjvypyO1s$LAC2e}ggR-y3#*{c=jvd|gV(5-IV}qu1Pj zb-TG};3uYvIEIay^fn(((NEZjD;}w<=k#Chn{7Hu_um$q9i<)VzJc$2d`CFD49Lg? z0+pnta}K8tpm76E6XRD@ZTxJ|(GUr(vbK4n{HE!vcbtSsgDh7Z?a}ek1+p@5>oiI( zr+^PES-Rm{Fu!}7Ir;25+R)8roce?z4c8MZE68iQ9S<*5|5BboA(@Yub6b2w?19;9 zauIeqT&H_&>j!S!N1?}Jqm#QIE0IW(I(HNT-EWh{$#_A-^u=P zCYl+hiN8Ti7F?&4=Cn0|_|euTdFIyluFtNq9uZqGa73-ai0xatvEzOJXDH#c{GJg+ z({ba$VoO$5N$WV*$_(yaFe8F|05Ozm^0NdT6;csV7*>fC#TI?kuYEHQUzD7X z62)JG=XWcDdQzizPxR8 z@((y}X4f$>>#L^P=h&&^1hjB>B*DF?qtel*ycDyQ*=Q$pV;wVH{h96=PBclrG2;r_^!lG{Zv8dJPc-x zA|DtS&^0r9%8zWMSBKQsfcRS>r~KpAQ#zadN~wD{XxTRxOKA8Pppx#cF=4lEX-0=X z%-ISxI*p;QpJDUg3V{p0q1WlQ6>L2Ye+*U?5Q;nB+hcP?%3vfV?|4tMy^OeO?Me$#bO7G};Ua93)fy@?M_Kn?~Tlwns!09XN-brgfSE#0wS z7Z3+^aXgthgGiXUv`imWi-(e#vqfcFff(tlL22Ea17YzI-x}Vc01I;mf*;855nI)K5=K^JWX3OFZtWZ66Tg9;Z0_?_eQsJ_%Ob@I@mO)pQ!NJBzG-@w z!iI?1`SlE0A3X&S^oZc@#2%B@Mn!2dT5RmsF%k4nKz`<^=V$-8b>maa_#Ixu>INWW zqxx)2dx0vZDx;*&{_?wn7&Q-p42jZ#1J#!B#bSo6-Je;lYid7y;ogeq{Ap@sV ztxOnsbjJXeG;P1ooCzY$L(N7Tn>K`H*!2n@4rSGYW>=dt7S0xx)Dj#(KnWm#cA*$; z-vyBce%e?d_5~q;^Lpz)L1<}>U}ZTn=ikT8eaW~EdypL*S~ulKK&XCM&W zA<%@s(X?}N>gkc0nC!#N&qF*Wb&v%oz4?IiZT)uJ!O78}sP(TF+Ey z!r-n$M_BhkDLxaVC1F{BGLJ~OVDK-zRHrJ2wC!cK@s$bW)wt8!( z>%_Yi;dPzp;??)gu|9m$Vs8DG$-{kLox(R?a%*pDh_T^$`}xTXcPne=Nr-3bp%^Tn z+xLl|-&l093u7tk!UF}2v-f8_IuXzrF-N->Iy2`HiufkNrj~^AG;VDXx0d`)9EHsj zSbz>!EC;zg_xAJk^>T*$HTMH5%dOcm795yfjGm#jO$M{Y1}--Q1cU17QrNM!lMJ#( ztK`P{)j<9Cbg67?Z@F!f!LczC41|~6V)!>TEzyM z8RA3!j5W3609dbAp<&zUO{ikeg`H}{@qn0gRLeU$Sh&6bTU*RZrmz*XsnmUHfJG_j z#%(8GEc|4KazwVP*~Mj>+)6mv%MtE@u8RW58pObPn(0nQjy^^k8R>ZJ?nfZU=rky) zMpoeamtlchC7#OcAZyMRxU~jARb8DA;JSQ?%6Ogq2mBpb>8;8qW>RkU!f1C%w0k9> zCuaVj9TP1FyG2{9|MR8N42BO7Z!6opy^k)meG=k;GLQ~_ZR&uF9vzM3445$WM%!ow zL%e)%aR%RMa(cAdnKU2!F75avAYd~la7mmYq~TqA({3PHzp5&)RaHq} zAC=V5iqFG*17(*6fwe5Mwh2Ez2}s4qBJch=BOu9QimE~)@`13l-73_%!?lgCJIck` zm*YCupuFJV$OyWpxetTsu@8xtl?tjln@IQJr$fR!K;9+@;0LD2wJPdV+1=;`!3E4Pt z8!ro$8WbuUiZwFO@t*k=0Qhaw`o49-QN6&^^f#KSK(gUBC}k&V3?#GpWsLE!c)i5(cQ-dZ3Fw}&>XGmYTt&^wC+V`$B3flt~3BSq+cicDv2Yn(p zhCU#*}gZ8d-!-dC-CHrRKQOOvDLs8zwTf~5NhR}95)Smc>&U40%R!a6cxFKNUJ?I*1Mm1vZL~_5yKS8!jd>SVp<0a zbg;{}DJ5Goe|A2?AF)$V(961KU<*I`QgM#oRB(GpGl@*1L^|N(=8xEr2q~v|IzXj> zhcr&_5fc0ff$RJ#(D#D2tiBuTr|GA9`}VyYtdE@5XZ|}U<2(&XD~rbL2aZ!o$57`V zSqLazc=?6Is?GJx*+|yYLb^ORt+>;iXRXbdv$qj{YLxza(Zu!t6w|ST`X3So3c!)N z9gkm#K1<@tME0p2^z*_s^t8L*pa17dJ;7f>&Nld-l+Js9zVrWC+0yS0{}(cc|0x^% zGu;1srD6Z)jPV!s9Nfi7vHzQlK*=liT7~81VXy)1pZnYJeWIh!@@*cz*1p9v?3XTH zNl6m^-X`&-6;pyjA&YX>SK70YV)4w3zh4WCOwC`H-VFdc-V$X0CJa?0C?q-Vn)mQK zx5wq4`#V^ADUFWY&cTU~ms>J$$6H-2s+NIq;3>aKs`S`qp2h<6p8Jzxx(1K8G{a2^i!hQg5-~5M+A|Gn;Eyu*flaZU}x72g49(+3A~ z{M>bp!i*^YL)#uRQ0*b%u-1=j4SCfm#mq&Sd>V@Q#fFKBVO#=FJmdPfP8InkSonM+ zSyLghELjTHUhi`3kyQEwKjpn3i~T88n{*=n>4>~{2D@e z-H@oPX9U$T^{xeFB$Jaj?y%`wDDr|5 zGZg$RkMhU1q^yn29k8JiY0kU34uT3dP!b|w-XRBD`qXUk;qSI!5aJSoUNuBKLBoGG zeCOpe(o=ahxUShlXY-v?nEEO`E_tXo_Rj(#5s6o&yK?CUd+=T@yUzQ}YpHLUuCm5$ z>`j{F(7XlpOP_y8O29OWF-q7GZC$fiKOqr-XY%rFUz~#Db95NA!onP{st-5w z+}RrdX$_DFAf}wAv$$@t3vzVc$7PY?Rr@{BK)RVyu-TAS)c@|)%NPQ~ z^vsO2b6If-559U&zwz066hrJ6AOr{?_{Bu1v@ZB*S~I)44kFZ(Ss0jcpFEr$oJtDr zK;Vaj(G;G}L-OD!xU}2tb^}G4899S%yS*i7yv@Q zO~W%mAlMszttUYv0P@JWts!qvcxlcB{bg}uBG$8HQG!yo2wg!?nj0(=&3jyCycwZR>8kC@upL+jS&qOLfQ zECgcS{6&!`z$DH-xXO=$FH}|g@gcOhoK4_{S}HYRSLV&7J(g5J?QOfYySw|F$cHNG znY5WnUMoN3IuB+02eiyhOy3%sKCf*osmsnr<<*aY&>FFsN(qW z_oJgDZQl@&l7%!8I^8Kvjv(mWO7YoG`;Nmkfn4|eQ`;(}c;TSEa(YE#qKoyTC)jJk ztsH6^ANoeO;F$gt{LGAad1;{@w_#Cj9tvrLzt7 zo{6TEzxxU}jX0e=u#6y*OV)>fOM#k&3`jaH^QBtr?=1F6X&w$Y@VW%|a9;AI7SYarxo%uC4!zD!8kAL(Bgl12=FXE>WTbs%55dHK5r=xdj$#`}u;MqM1~&3d6^c+xv(}(E)!ez_HOiLeXNRZZK2k1E`IrOwj_c1wpcn^Yk>}D2 z2?@YDtW7)G%NxS`o(;mhFlap+n(PCn+MO57)y0jShr2+X zrs%+jtE*S2`sL^d!TDPD3?WfHvGbDDsF*l(PM@tmD5+>H z!F}Bu2pWIUKXb?{dsq{bI3WQETzf6W_xW>V-x6+`+YtNK(wTI%&iB}@&EgkG>s^U` z@j(#O^+L_`5V^f?3&;GG!{aKJ0eB%YA!|V0+$CK8;L?i^S_G3}Um42pbRc=E_^nyn z)~uW3QSGA~xOToCj%

F6^0;BdB2KU~h|FbFnX?ryYpj)GusPem^-n>R8M9MaJNs zhc0T~$YaO}Bk|}_j{R@3ItkdhTeE+LG0-k9^(Q7?ZBKl8Z!PUyCmp%sv&Vx|sZBNw zV#2=v%Jr2tPACH3k%g$$1`7nfy?Q2En$x4u0jdFtj+hQR_jb<#9hdh3>96NKOvM-( z|B|iRa_>ldqoR$ET`6~hafP7|QUH!mT_sBv3N&-%u zb+o!LhYLICw7Ij9DP&s)rVSJ?^|aS2D|zGX9e9?DNF~-fyC(+5e=;XZa`U^rqkJ!0 z1AYlWy}>!JnLEF0ER}YXt zI431VgxuP6)iX@!H1xbIKODD+HXC`A^!rtv?@GWN=U1;?%zTdlNpcubyfd}a^wsRM zm^$?dgS_*)cD`(s1hcpuzjz}f=RP+ehkY*cB7 zZfuOK{%epQEFF?A^t97PPd!o^(>IBkm?+lcNl!Cd5HQZy;_AteF*eh8mgOmbJkfn~ zc17u}gKT!vr+*pwsW&aW*zWJwOrmG+sipF5UTx$u-olt4r|%$o)->9wMw;RCR$dm1 zDd&CiV0NE=J=%OO0U)gASRRv9`1Ul1><7#01Y5b9lu@F4IY4Cq!}|N51$visGgGwv zcBK&oK-rp^k}}cu8gQrW$%(Oj4H#kl-E!@=0;#Oy$`_I6rnO^M=Z{8X^lG=3o-F4k zk$3_xTaT#i8~NTn(W0gR^64Vfflp<>zVvBeRBO_?jOL=VakHHKrTFIelKs)W9H4Zb z^7>8b7x%#B9llZ`eCMTdz}zPf@~AMz>w*klzh_9`_J5Z8$Md5mEd-y5GI81t1=zZJ zxzWW|B|NPy$j%T@T6s>~V(oegxOS)26~(zz%_fl5CZ(Qp3C~(R76c{kgF|TROnE-` z8cf1^D>x4Ua*>RDb}7Dqu0Bjw!A^MXcRUS4i!soF3VP{^EpP*o+GDu9m)3pdPf^qu-EURCzX?cvzI3!8L7ADEQw z^ya`@p6BW6l?6V!mesz*EwM%Nov2;Pp7V>th6ssW1Sr?^vC9JW>Ppn>1}p)!zqZqH zWYp2piz2ziuqI-t`Cgk93olR2Ekj>f9{#SlW9Rd{9Zk;*;l}Ep2TrrHzXi)eKl%^# zpgO~NO5CD&RaL-W&F-racNM>3dM3LkbV=b8zqa|=AJj>2^P1l#KFJLOm|<`$8I_SD z1JBr&ao=9AVxO7%lqzKVX3N8}(Zta|{4o`qlW;PS4Nz5!fea(?q%Vp0u_Km^0+ym{ ziVUM`G1m#Zj6g}TA*Z4QN}HcMz6(o}P45%y4`QVW*-LQ%6_TJ@k_ZVZE}kI4&)iFh z*pt1@j!7&_4V_U@(O}yk9*5~2yBW4^3fNRvBg4b;s!Np)Y;>Nu;4N6m5d|N+eD#7% z+FnbU{eDSMv+-7lnSp^a#6e6x9(v&tDZ|or0phFL)L4*g0p(}Zxx54r5k6r0`}8Fj zzK+z5+S4Y>z}G-@@Lbw9UUhzEsnMd{-s8Zlje@=L3+Y)WmnX(~*?7SP!lu6T<>4*Xb#k92y4YVpoCg{jBF&>+vY3W_&W zYmZZhMT99znFh|oE}c$T^1LRC@(1dZQuC=LNYhg*tFeg*`0kgr3y1ll&A&zPr;&b( zMZ-^fr^7S%L58;>#|69pv|F7DNXMkpJ)U`Yz3laZI%NDesSvY55#o(I!hZ+((8aY^ zS}msn66m>`C(zhJ;7g9nV&FF|@FRPTTSLjcJ;g2pkLx(tdH5yo2m^GrKa#Q(4#EPA zF7?{P{Nnub%u+^@u#}KZxs5M?KqafGLs;UEFOX9(3U+az4B1mNFww+reZ?A=vRYWI zhL9aV71x&OcZXv#cir<;BPIhE=BHPVJ$1Q&8=V-J)$PwE{$K39by$>b*Ds8T2_gt8 zAUX<2H%J?ZN{BSlDcv9v0~iQMiF6A{mq^zPf^rB>H-L=NJ=@SZ`4|8kq1=j!19Ft58nV}`oXJyen_h{!sa0U? z=Ol;miWCjH@+>7y!Ikrl_*mkm2h%q$k&|>5)dO>uCFS(%uS@{IGCE_w1c@R$&nOXtU=4 z<7aClqYbnTpnNPTaRmAd01AbroSVAOT0Qc5FDG_uPw#A1w?yFXa4a*RaCcyF@wnUn zM9Y?At{HUjPRH2)L(C)S(Q)hLtub>be9SRQDwd{cABdI)+rjQOw&eUJ=864Wmr`X%CTENf&+ypYn z7XJY!+|~Y<4+fRlpT|E}@1+z(*?N?8Wjh1tR3C@)aA zwEubgKdJ8Ay}$LgwYgv_BL{n}xqrU|xFCOC3r%djO@$Y53v<1{|s&!zBJk@GL{(Hn`3m<=s7|8||| z1=oDNZ^|POf2gQfZLQwB^dMkd3pMop`}bjU8;8ws7=%r?Ej=wQ3?8{MBiNJ8dD-ut ztM~lrr}7;iS^|*!dxaJl1_XL{A*aaovjMvgejac#MMYfQn5dweNM}ureNB2d#$|0# zHEW{|&W~^r9X9)C?t^H}hyA(T&EUqhI=5tdn?~enR-lHXsp;HXK?^KTZhHF4>rKqu z^z_yO7WZRrDlC+j|N22S=P-q=s1iwhGIQ|h(f zUoD7+U6_oD!)<5Xa6_P6SnDeSiR1KH=i#wB&Sv6QuU^Bzq46WnY*n>$Eis0ZggT?6 z{}`)9_G0^S%fXTE4$g@%JP8t0p&HYfCW=o=DRVyB8y6h?#io1q&9@sgG+NHi&Pglj z>UFInIJ(8%5?e!OX%MpqvK`m9O0f8SHV0p*oW$lLSAOARd^Ps2a!%JYRlxpE(Qx%e z<;uHh3OE?NObv&r+Xl&~h@_sbzMDt^22RoMM%)U2=Iy!C8>QU})S`jgYg~x)>~68I z(HO7H)vumccvFrUwnpO`0^B~EEx$4jl27?WEAvP)me09XrEhLA*5cij-{%1dv-W66 zZ0J^urO(5D)IPwB*KM4?4sQ<8BMjSOw>S9EI-C)iaosa^y%l#aygc{&eQ@`P%U13= z%DZmUNcbgd?|P=-xHo6(-AZ&nENQ{2)%aoVES(Zv6D_{0G{m9gXyolK#EQ&Q7k~*_897 zLC0P1-%}3Tez`AA{qQfS26*beI_!(ZwPGpzyD}Ff3?{kS{IpfCB0@qg9DjWWfPm=! zxwQPXhNjS*X@|5|drctkM{pW0y3EahF2x}e!@k7C%f}aGX>cOI>y!TP`3Tp?{=!7C zEOv2`6efKurKSITBvAA=T{qIanq!K7q=-5TvEO`{(Y>>?BeL}4o}2SZT6$xnYnX|d znUM2jzE`Gt!A=xr$3Gw-N69oOI(lWzwr$YJ{J3ik*xY2UMj&hkS1x2&YRyefj$m8P z{vKC?cuB;X?(D3<4h@=F(8`Ki2^S`c=b)qGy?N`!{@b!%%i=-KisaIrDbd}AWMvFo z5mSOvlhrcekRF(oU(jhuUvI0 zoeDC|O8@*Bf{#BF=iGmD>-aOUe?vR9ndNOyuzvwLbA_NOYu%qE=uM^eP(M^>iT-@l z3BT&QVPby{@SkrOceVe#_V4z!Lsbfwv-ZOWt|}92kr*4Nm;JT>CozGEiJ}4Rv97;L zR|Evb_lLgK>cr%gd2x3sp8DS<`2Ke`yMO!K|9Kbwb9H|k%71%M+;i`>T>Jd+uP7V{ z0)IWgIpEI`2tdz16#3lT&GYr2|N0;JUjOZr`TLgsKYEq_2M;{d2u1+}y{3?afXg9d z6F?I>|KMPtU0y???&5X&<)P!?A&Fe9x-eM>kZV$YvAdoP(|4l+>ytr%Mh^>>o0*vb z;0Lnu^#Y!n`(cvrSRpmG@q%NpUxwe&xXv3Q0RME{4eh;k6%>SL9r!|98zxMN@wdfT zC-fI^)59n(vtn#i><9|hR1gmGQL$cQFYX2brHbek^b~cI8uBmLBSD|=#{%VN5Z6A4 z2{L#WQ}G&}o+&a4X1aR!hUf53?-CE9(j~p41XsC-Ugn&FXrSp&J{XVc0i=>!XtL5! zke`_}3UDPzDhqgdHJtWmf;?B*4EXqX(z{KF+2ZQMzfJdjvaWCd&K>9dgMAY9^)SJk zw}MO8SkE~5&XG}(r^%&31)X?dkQM>uUKvX*qNZ=W2DL6AUTDyF!Y+h8rV@UlrL4z& zSwK(_VE>K9k&W6WnXnKtn_xH|Jm7FJlW$v;3G<2DJc5RXZ%O% z@1uO0Mk@JUWSXsMIa!OlX14OxC}A48-$%Y%O%1nJ(V2e_?=@vf=;Tk4j-e*ItYe8y z#m?VkVYub0j|TbhPV4Ud@z*U;P8!kKPi}(C>xXFGmQsC4z2T`hCJ&~tCbsgg*Y`$c zt%0dW5m=;AEER#@i-fwEnVC6{YL*w+ zglhG^>QxI46(A1_CnvcuHMy8pklXF>JioB8OCbbwh~W4Oj<&w_h(Ad2N=S8QM%V1W zhgf4J)h!EP8dRq4xt7G{SoOI4|3I((Y6EvTs`yk0DqoltSKA}KIxQFgf_?l8g4N^4 zl1)z4(tRp*e8FE<%*(T3E3gDri~@_ql9H!l0G21}o(KB|87g6`tg!I^k_@=k^```d zauH~JL1Ucn!cRoxbLIrmAZV5EJDxP)(+{QLkJapTI6ZjoT}NxHYs(HH$ZDL7FhlY(boJG!9) zSm9a$lD~*B6I1hCDUdo>Y<{hC%K8e}Wc>j=Da3=U^~9nzwUoL1pT}8#KDf#J9{-wb zq;f_td=D5Jd|kco9zJR}_*M7>ShY1;2!T|!3B$Msh-WNx%!y*~)~+M>gBmWLBjYvv zy28LUxmo54CRwYGra+>$yXm4IYs7ik_DfrLw~}SwmNb;Mg7b}m&ak2J99AI~mXQ(epkbNAN*Eh802IZKc`R2l&XCbN!skR#{D}KI@<~Qvb-$MC%Dvh)}iRjSGV7DajBe?MOjXQ z1e|LPXg%&+8`UA%k9MGwjxaDarAL&BxTFATB$NyG5@3j}K}oaVu>W@$(JB4!FygK- z*3QAfa=82vmpOjyqf|jlB9Ooq!!!HW7VOP!P z;L)dO&`;nHaW?B_3-y9VV}k%JDC#BXJDC2b09nM?i)Rn>VeDCKDiZX9goG+uBHm0W zUY<1-{byhnILYz{Q?u499_jH<2yN3EFw^0KCnoek)v9tJiAOK5bXI7V79?%oon%P- z`uWASKunMiSO-q@e!qx8!kX>=-HOlTtOKdIzMYG{oluB*`4gKH;vt}E-bN|RiD;f$ zwQbgL0W5QE{{tfe*r*jKn%-`O^+8E6TOc}diYEtCnj`Zs@ZuE6BOR7kryhr+yHv@> zHMm8sR+m0)y-fs>wF!8g8s+mhiY6-)?mO z)7-30@FCAf#UuCMUS;HM*kf@11D_%zN(%pR`I3*KSP|w~ES|*ki)F1~7j}9|hS=ta zgwJZu+R~Va=D*y6s&ESq>98-q5&!1(5S&EH^Xq5Jg9Ht~+IZY0xlC@bzt9gpzbhomn8#`N56%>pw+xwxC0QpC3Y z1SB~OlPE3NN7Xh@Lpq_LthTT?df_sO9&)6epFfI*n~!HhLs8OJloY-vLbVPPX^eLa z^+Ly#rt0dEB#HaWXSI~WaC`lvQ*E(N7(*-;HC+$gfkhDzl*@k4msjkej10geD5il^ zz@ky2nbH){Pwm(KT_$m%lBERXA_5KqV@^d@-`H5{3gG_j?NI=TH&zhI?AXx- z&T0%Hjrk0DK)g2%zaJ6id}q164D0|LtnGUT`i^>t!86>G_Lj9BK5Z;+lV#!>vyH~_ z?1s=rm#ouusi6?7@Qr*&RRN=Kl2GGq_8rudf#Ckh{ErElTWj9cFeYGKI%(J+4>xpK2+~YbggIars{0gvWw1kLjtUqcz_l$im-K(PuARb9(*t zW4jS(ei{%z=YKLH%voBxAU1L5&^6K;1v*~~F(9RctBils11dw&0kHKk)6GK!5Xqf& z2)7ERdL;|o#_5GIHiUsvVzLSo6A0Yif4>Pts2OsQzesk1uRtI7`c0$=Vw4UcmM!nLh%ml1bjzLz?55SVS`Bw1cQJx>|` z4}Dy4B#bJ%_?ZPJbyLYa$2YZJsV};(3d|{|b%m;(-4UU@6l0a=>brE7{2-K#fVjTs z)&Pq~=mtt=Sn}9#C_w%w>cnqV!tSE z8ED#25zc_P>_^9ge3p|!ClhBVd{B}6wQ~qMucUf`YZA~m5&P=lSaQlZgi`gxNGUoJ zj(~WB7I3#`|p-(`j^XJ@~42`00i(+@>oNNZT!5V=p z#a}@=g!1fhL1#i>K*EuhQi}l~pXjZZoG1+vkheF82+-12Fp~qxH~@Uk&XhI7VMRG^ zcItAD#k;H6&w*>eM*Zg{a{7LQmtLgi9sErVR2#C_9<sE z)q!j3Pu!?2H6=J1j>h8m1`UFPd#RB&)lIpJf%Sm>vz)i4JkKJagL zy7~`H+}Kn!mC6mLb`Pq&?vz(=-Wb{uRAY-U^rzV}tFuB?t*@8B*^-2m2_k)ML~9Qc^ikaxlbfj`IpQJG1G6ADq-c#Ryi%!Mf4_gEX9tY;Ufo3; zeswoSV+i!3;GdOPk(FGE;1lG6Py#Cnn`6AOg^9~<_tejz&g$!9LqkH42!iBDO(Oy* z#{hz(XyV{LE}ns@*!rK~fIQ#q@{7z}_dUASF6jj30+=^UCogHM2j;9?^a0_{Or_qU z;#sNVxp!5sK|jFCnZBM*AO`sYi_V#Q~j<^ z!yZ;$Jn;udyAxntAynaV|5Jt}W~i(F4@7j3#zX8O432v3)(r@`_hHpB5TqYGAo?Uc zp?6Wr4pg&XIt+{m2ug+rIx?Ry0yW04`Kdne1~@OvToJ6l_PbX2*U{(%DAmgP-gLoJ zvr}4pY%xX+LS=rje;n4AJ4D=6G?}-)yIL7|F|!u^2nIG5?l^U_Qy!{`91&6nW^p#H z*#31_14jnM1J@pSz}CkOw-4~u;nToO7Qnm4Kw9EG5*}0oT*B-_R8I0`{f1|%K zZTDCrNlr}DaXTpmhr7W80FN)j+@foJFWYC}qP9r4`EDYg7Fg&3ReL?yJ6DUZ`Jco+ z2Mwd3!0RjMFjO*X7=W+Z+sd_cTiwb%=-+v|~XyK;D9K%@c?d`3l zV12v*+n#cof_UQcP}-U2Zer8X>VKz*Uw$zOk!_xkCTeo=`;yV@4T7PAl0*~R)jp?p z5=kpdYa2^zkEJ4PjIh~|(OLyV$?&47>*}5c)f?-UuU~cKR7<_c$j3e3ylsi$!?)pA zF;wLB7wc^fA3l74+^<)w;nNy)s5n0vRMOy~Fez=(j1D8AQ0vys2;+AGlSxs!04ZcqWOj57ka6DI2I zRa7J+Wk%(Bj4vi7C2af7{rktDDDAHHK(~<$8bEb+0x@6bISqjoQ$|ik|HeoL7PYL~ zQxfENcAYLUHz|ohaIV?e64zt1`h-^zv2z3h+a5rKQ(mLW`8xlx+P#9j&4g|u&OvDo%&)%|!K9U>xsmX;O@wr6hTrghmdf=~k|M4y#yZ+COs zp#ebB!{ZoczcsOk?UMo{8edI=vBiov864*2<>lbWg=a?35Uv92l98FYraB>~CNFsT zL37o}h%4snb;Qb0o(y{J=W48`Cb8>2rF_cc&5h->?CjF__P7vf5I(hB!l6{0XCvHH z@Y{_|Y{KiC^p;C+H5iZ!3#M3=&sMT3FWw|3>Pgz#+DqGScw`F4&5Ky?^$}dq4mN== zjZ_9ob}=y(USP{uvc8rhymBw5b^EWPBlptfo}ujm2LO_5b zT%bycG<+a!e;>5WP&5SVBU2Zdp8txJC_QKBIx3s$vQUhEzSZN|^rkfdM&r#n`YQMO8Vgtb|M5XC)s>#FZI&c$KgG z4D$Cc1tk@f&WyMRLPIC3_M$=mZLgUam--jUTU7@5y!{aSm8Tu@{SeAj^5u16E2s5; z&->F`@5D9sN`hxMaz;LH(Mu#X?xh4gn10tE&@7>`tHm}VuNu;{98pOp6EXYa$5n(U z(Uo(7_Xsz0G;N(}HbqQybWXX`6K(A$R#x@BydzwD$V;y&Cue7O)@<3t#Kcs?P#IBbc6QA$3AdJs z(T1+1em;?B2rBzNJ-I2@@yzR?>>bVFZu8{}Nj0T?VlAa{2NVMl3p6DTVNn4zT9seE;3^$6Sa z7)3GUM-X^IN18#5v;7Z_lgImzC7k77L6_EJngG31-oJ)j?gf}!5@XydP}$-ZwZ*6J z^|1>I?()phSZywSVWoiNRE8}Mihwl8rDr;>i_-n)uDK=YPdHw1MLdkgEtBShkIywoE7&;u(vkKPj2=2^&u!i}tJf z+s81AZlf5ygZJ0I-P3CrO?>FYG8=mPx!E+gf4pxm&TG==;iE^^Iyx`C^r?h9y1SFQ zO{Q9-@H?hCXKW|>DZV6sqHPK}WLEUwrVMS?T80CWVO1;c%*m4rGc%4bA@;PJ$_fe! zaJWO1=#7IFHYF)Zba@VgTf?}nh2)4B%;t;kPq;z-ur*eAH4!V?!=(36Qj(8P-_p@x z)eS$2ZYG6(N`ek9cH1^1OgR;>3_GmW83Xry#so6I@dRm&!JjuybOca5fjswk!ka3 z6D=~G@}He$1h5fyN;9*e4+c%gr}<{YLxCwirm8RXDk#qfmO0ndUU?`X$mV)OK?!s{ z+S8>ymxqMPxdC|tl|oa9X)Dqhjg2P<4 zaC2IhB+q=_efD41xjAM2(YeWyhUEzF9U+0192;OODPH&^+o8HSx$UXhBa;toEXvX))`+8cY#U~lv z;DYNc+7IjL1?n&%!>{I`fNEvUKFALrL8pi@Re`lEwDWRobNM}|dk-Yl_7|_`l;^LD zihZjS*4Nz8po%-WcZkcmRj9i3`i=5xLgOA|fb0S7!^gTux3#RWVH$9BlD8=ll0PzG~lOX5PvHEXTI)!DRbY zIXStBjPsFb9QFWt@V~;pSAi88*z|w1OS4t#)z#C3TgY*nU01Mp+`3^7p+Jm-Me)po z`N9dN*}1tZPTFG-Iv9iYXtu*JR&ckX4pm^<>rc`j#!;-ZI>|;B2-~OY==PL^#JhDT zBy6g{Ra_Vp*&1;`*^e(xjU|CC1p6~-i8uyC$b}@(OjpEm5)z#9s2c2bbLZLGI_~ul z8r2zWQ&gN5|C?W_EAA=CmKyX;QgimrHVeTO7jd4PWNeOxyHX%gFKA#8SO%*#&{|PY za6lRqzrWOPEfwMXm2&Z;?Myu*8SO?;;|vG<0HsC)(V_oSe!^cehjL)fG!8M zLyZ{HY?o`R!6iYazaXJ{Kepm{a}}^h^#WlYU{h|;L+xM0dC&p|g2);VM?TYPX5}C= zA>^@ z<|PUL3vIMP!nU=X+?R}~L2y^x#0(x#5rqvfs5aD=J;@c(!(VL|!tbOjclASXj9Ksh8wR03=WD zxeP#aAMR8WdrUny1vItN$ktY(lJh6Sv(Q**9J}`o3R{_F1m^>x29?;Z9SiLA8z9IW zU0bfM=yBYu7lGHF`K$w?S-71n*e_<%Z>+6F^E-~K`iRxsU{x!)lO(5s3a$X!eW>51 zl|{Ftj-M{u2~$*?9EzE}1MI(HGf518GH){i6ptH)8LW#9C00E`w2Zuge9Z1q^)17; zAYBCF@Q~f!G!6xHNM5zY7|sC2??Y=77n1-*vstevbpK9EvQuhRvKclU8@qUV`+?z& z9HMo}Cya{2!*u=~-?%JCTu6)$LV}&9vJb@6L-@`(;cNv)!?uXBh1Z*wKPO`Z9SVDc z2O`;YSz=mej$8VI`+=ViMWF3e>zesA8w(2rQJd?lk`Id~13c{=hPJVycn`U>=K5PJ zcvoDftECIkY2scZZiLM)-Vlo#i!2=xbr-#MrsF!+Es%%L8`|jk4~Udsn@bM6t?RIki#Fwpjj6 zx*_LS+g2{Up0!IA1|;Bk)X>y|bms|;4Gp!cNYpIzF|&PxFDY>5RkrsVmg2y16U1c{rS@ zB@Nf+NZ!`U9g@1y#pB}1h(J2erV!~bGBG8QG+meGdtchy*#V`{%YN-Gf9d3ezP@4d zx~6RWSh4{BW}228^nu%8(PGfj^VsIshz%4xb=qcrK4yK*PldVXk~K(odJZsUD5ze0h%DT{Bqmf}O3Qy2e8>b^pkRACVc^ zR0V6(moA)_5K~65J({S^d3580uWuBXzh1khL(B`UFiIm!r4|s9^z7dVuPZ1U3WQUT zlRd|a$*O4Un`K&R-yUw$Hif9@IFIr$GkAI|59KUj|~+Kw;%0LAz#kKc+XH<#aD zR(6h_$Vt80+gLkNx%GSu72(0-Q)6OcLYD6Ht#k(fTN4wPg`48^O#JoE-YxUUA|hS)7*&v(nP^(rC73ml_`hudr9d zV?rx!l3;!DJq%bm``?b-b(3uJy3nmNZ3pPg<2g$*8K0JF(#|BoxMJ=}oerNyNSNGQP*uvPz0=x6W4@3bEU|jwvW%vbhkySsibCP>=GW`Av}6s7JRB?)ZF??^webZh_0kFL8<7+ z==cQR2d`NZQ>RB~D&HImPrYrg=n>&@mgK&ch#;fchY}BI1IIMx(5t5})(Zqks;N0Y zK`rOI)@krA{nP=4doqSgnB-K2d#v;@PBGm>sHJEg>qXWvE9-&U- z`-42eokfV7u&0UVx{tn9vHIOg0s^B{Ly>TH$Lzq|OC&LZ`^r5n!6>utRim5a>0~Dk z9hp8BP>n5p-&6uiNlDTn#kJ{^CCaLB4pTJew%Q^(kvnBIuHQQS>JQVKnCw|7KarQe zo={u6h-Nl7H;0s9q#aE(HLj1xW;wj=A4IP;n$C&rtLPNWwq{QY@f3KL-;H$ke=gMb zWvKUvp#2I~o*rNLr4Vibdu#jEl?@Iap0VW@Z9g&GmAN??)8ZNcl!NAskS}G5ONDT3UTR- z4_pftEyqi51kK-fcHV_Ckq^pIR1Ed>k4eZdyhz6R@=<7p{_<1i(5Ms|l1J}Wh!=i^`r@snoh<*-zlHP^yhl?QE=^Zzc8fs|LO;((l* zmItHL+ELH9B(mcZk|<3J4LlPI!?lDT?>}2i$?D`OmPxH9>>DX7BQ>Z=x!5^);;+~& zoR@uSY+Qt$oE)dxpJOMX7W#}`OA8W%a@bJ29vR_teM1AzwkzbXBMG-&zA$P^*n4SU zXh1Du2VKH3as1{hO{Q@<85!3Tj`@&}DFb6uTU!YE$}2T3jf0;f)XCh3Es*S7V0>;w zp^CD`r@*k7YatS+S8aWzn-vumFPy)4PFB*EYt=#Po_v|-r8oKB&_>|d=SGl|1V*~} zd3_)qG){IhXL0m04aG_CC;8JRaFAH5TG zo~>sSb+cXju^2k}3}LgmqP$WcBE)|7Y?g&+kk`P#K;-52G$Mkx3QN;gf`o>Mcm6^t z`a&1nVMRt+eObnyFIBM4U%8$Toj_^6Ag9oNU0hkYx54-pNli_yUD;hTvoj>ej``_} zD|Rk83R#Lu%*@O%WP(@4!0r_T#~0yJ1jGR%S5!bZVY6};VPkJUwM-^|BqB2M(j`h# zS>2!O&Wp=I0V82#7f9OGu;qp(CUkfuTU#z$@x!mh)6;Qfb;`o|2A778lwGtQtwdFb zeM(GZiYhtGb{Cy&Eig}d{dtVOFdqOk?lprpvdQN+p4c@6RC zPOOWbgR%8BQoZRN2Q8RdpW^R})qHrhE1RE};tvgBNM)ys(;|iKa%P;D4K$vU*=%sa zCgv;}UdTIB`2|r_$W0n}`)auiVe(aaCgB;UGbAV9%t}0SmRqTmYGy8bj^*Lay_&Eh z_lQmJ4gtY&jmp&A=A)+8?G!oUu@5{5u4=QB-K~iGetx+7s8FMAXyMyKBpMR8+;<#d za=zi%Y_Yb^6+U_<`+VTTd)1vyQRAi0xP*Phk9jucFrPbN*b*_gd&ShmB)mb*+-%`W zdiwP4%YEeDMUv(1y&B0-hVp`y8J=bz(z<|Q+W@FVTjH?mg@wHFPqrEiN( zB=N2hXsh@fT8f?fG&4H0o>Sa>W#G%1#g`FkD(7U8$o(TyTd=_`VJ8-;KPDs~LJBnl z0#H+pQZ|Qkvrz?EdZwnPvUgZXCZ1t#Ac%7ak{Q%p-y9<0czWm-mA|LM$EuX=EzkNK zu4(8jj29`*si=r)n(!lzyN)3-B&f;BDybPN;3VDe9F`ueroy%(Q=md?Jhq5cdDHlg z6CpQt`kR2$=2&H^E~hfIrDoz^2>3uaIX!>vTzV5*u7eF}x{udQ7Fv>njg3>}`YYk` zBVN<0Lm~A(hEJ{$ym9~f{l{t-#>J#4Px@+DVlwOZD}uTWE5^D%*@Ofr>TAK zpepa1KdZL4iu+LW+ziV*lUOt@g785KMK210S5ik|e%dEDhy_c34@b8-D<5`*rh@_Z%`tvp`2f{SZo}n@}Hl`&zwe5l$X5YnE ztP^qBQgBTiIDD=7L|(=;)Vw}eA$BbI`TUwvb=H9D(dpSsZ~S9) z&F3nkZ_zSoHC!IEaqN7?#=%o02V)~YCRnz8G=PXcL9IK4;Ze8%6~SAtsNNG8_j3pd zsbZ()Ax!Eyg1a4K*H-J&)@|!hE0gl*2DWlVWt3}gV`vYZM+oL=L0!Aao2t9cr?JCv zBPDH*8&E4>L+PL764S>&srRKUb``!eaF3{{s_NBGUGMmKS*wTBVR3GBYAEaKofb8> z$>kR^SuQ(iAu?R2L3}2%y;aTDV>yjPL$Ff5Jc#9|0+|e&9__O7$y%!M4PjCMVNXZL z%4`#x>#AE+GYfO9dhSZY1*Qq*$6f)>*ciTDRA-D};r7JWI~ujtg%o&dI8&4d&h}OKDdBl$15L zz}GQoIX-MlH4!7?>h?gP?!Fc`PZ1|q(ew2smgctB#)s5a%hy|DzjyL=bo5wTZ^AQP zu)okmhN@rCaI|rRFTh`j&tMY48$~T(k-oP=t;W~29$gbZsie3zVe9%b{`gzE)i9Ck zYAClh-e;9{1jN2I=jW!q?bk|M6yL{%UAV+?qQWR7EjP8vaR2b#lom|{`g43dt}h-_ zHuAMKysWuTfkQMjFt$J6^ik!?UL# zu)3%ocAhAC;+xc2O+C}e*0EEJ78h!gqXq01cL#PP6j~GM*^O>+oqeosW;r~nr6bdAS1~|4hGE5B4+OuEru|KvZxf- zLl*7#0@rGX7dzTTWIM!_Auy zCCXiAbiZ{^r@A%OY1k%*QP#r#QO} z&yG@Sn@^>t(OOuVW@M$cwn|BQ`RN~5TVoaFGsR;?%N(~UFGs8#tFlOWIIASp<)K!@ zI4rbhw{}KBk^i-g_Stp&G6xq+3u99gi=;GK@^l{sg+4g6LDGk{U@J{A*n8>)Zv90u%)ZS7>W z*G(pt*!O>wOp8r=vG3lMrHGPD=qHK3ZJ7vgT!zmjIL8iuz{7nI^5JwJZm*lHJBW9` zl(%oFe)m!hNa>-~W&`Fcb0h`qJ_95cB5o{Z@~bn+Jb zmah*>jfV{}r$%ngzVzI1KceC5P|p9-i`>;pi1FzwuC4c!!q4VY=1?~h?w_7aPDv7O z_AGU5l7Z&+z1g&x>ZpA5;LeWU*2XT4mr+alh1^^Y35%7MqA2k#!g{ub#^z`~2Rhai z!qZa_g8x|NupP0vu9w&N?#r+s$}K#=wsy#%HktfG!rnf(@?+C=qI4dU(zRVn5H54Q z$;MPm3l@#HPh2FZL~=sh--~$>To_=NsobeBG!=)UixTwebyPcMO0Ohdru zCycsEf1mTVKR3jj3FXnv*LU4!3=HhS;SoMxP`lStMzkU-@A)_P+3{uxAi@6Ol)o06H6mdOk*!n ztog#iZ+@L(4oyUK@_x*ZayXwo=Mv}byneGZ1t($FQ#DwqG!%@dQFu?!r(K8as8}1O zxDF#Dq)Rh=R7yslOyFVDrz$-~WQzfNU6|6@`2(IH0igLyRV z{p9A_+6rAl^gOd9)7+2u-`Pa*XSzEmGL*A!|FWDo!+J%=yx50=B~Q{RL(hLBMy9KI&&}i+y=@!w6L}BQC^?fbBHI`Ch{WqI6+@i$ z9K8}N^3nyR-LbZfw%A2aCetafo%?X*$nBz6;{HFaZb9m+m4$!hb>_?&YO0tgEWf^m z+r`DZHpN5!{{C4Tp-X44y)DB9G0%I=D}pempDS#`0T6Te8X_FUc~t4urdbzD#&s_^zePpq1{ErQW- zcbU?!rK2u>>+3ZgF}r=$yImpmjg6;rRp@Wr;O3#dL{U3d*>1d*`P5dZq5`OezyWr3 z9qH#val`DJ>gio?eDwuRSIE6iR8dh`MifyR5vpLDak++&z`ZW|6FVCnBZ7_owU>gz z;&6_t+U~}=H}lQh9z{^<8>-mzo{p_1J6LrPh&G)a+q<5Q{gl(Drxp4c`A(bDZxPdyw8}v7b3R(xe`#7l6@JQRy;c`v zK-6!(Qs#cBz1xO};N)hobfe{GET#(D3m!Z;*K#BA;2Htk=_NU&22 zW@&sSWl#L*^z3X;Pmj8v|H#IwQmJyIC(h#Te%$e8j{>$ftA5E9lg zbS8`9n`f)425m225pk;e{@n;4=PbIrmN+81y4#{wfpmeop>Wx=kol@5;uCA@lsdY) z2^lGE6E={?aats6`4(GE|G-U7PFjdQxOy(y$LmHyOB8B%yN<4VF4<0=0IzRu3$PeSjE&Q6Nh&)V@36KiM)D=KzpHv3isWH0m?Vg~b@Z9aQNIa(pi z`1`DEOxPynOf&|Gh~#%}b;G>y@k*ACNX>-XDGDD%B^*OF7W8LcXz9&}C57QcRi zfWWT0z5jI^M2SP)k}FMNnd(gEjp;N3_}zAluyO3>8|US1GhUU?-*a0mxAnIUVA05#`t-cUR9uQgk>@Bvl4}IPj7cH z8AYvDg@UG%P22gI*-MXOD#F_+6cj^gMv5}B3!f3x&?AsX#`sSwGcuo{7NoVCC)=hK zEVP&|^*Jd&Inc*H(!Hk;jK*)qmAO5qoaEM7p+XGg&3V4vQI)}5l{9s>w+~ccWMOR# zK-pyH7e5>gudbD*O@P(dU?4A5l4vT4NlQ0ZrFSe5(Hi^S4b}$1k{A6%wh)tOg+lnC*yPIZny>#nm?VEkFMXkTkLcH_E!}X=ki0gI98y?Dut%OJY z+Piwv(wWgNIDMTv{`h*1jmH*nwA8DBcwR5yDY6oWZT^MSl4E+D2sJl zkV(I9mQ5y1iNRj-)3tF<={<*GtjWo>f}^mb#zy`u|Ctwb>DcStmywBMbgb83h^vp4e_q9pf2A6p?3(22IZG|)Cot7aRvF=5r$=o|^5UvG#TrbrmJ*n%dtV z(rBR#m2=9Uv)tKk$x9|Hq}d(Sn4bH;b~FX;JF^69kXwKWM;g-1lOspsqe{Iq1Kl+)bU z{4EZ4+xK<4xkD9)y>9XeCjMM3d;vA6`u3Xd_4BcD+S*2U#qN8Q*f6}}fmG}UCh7y* zIVfGhQ-Emm*7L2+&?^EQ_0!Xrq~h`vr2X3Vsel%YX1tEIhp)(F zW#(_yqckd}ZcHa_5x)s7e9_qKaX4PUp0`X*WiUcjg=V1ao}As(qife$jP>h}5{1o; z&$pXbR_YWBc9J6a8F1NCPf*$e(MbZOJ1bKjZ0BZYC5iEUgY<*~NvvCz6Ii~|*#){?B{ytJ!sP^^J zsGMb4cqBi?L2Oa#bUY}zt*!ClrO6*v<)#+KT&yk|sA17~C%}DPP^ErW!JvhlK1H1* ziScX0Z|8jaH2*HNhgTliK29)~LMPZpX^wA+TW*|BNPNxdW1;czrdP4ji@cBNUy{?Z zD(=bnh&B$dp-&^F?7H1tkVD}GZW1AsjoMeqT{c&YL}UsQl4q(Hlv``29M1XKa3Kx* zy{4X(IFxzE1(A@lY%M6v3W^TLUdt~jSa>4#%*nO;t4wnLMN<)e;}=*xvS}?*8rhp9 z6H`+Yp3XQ9cqNS~|tXOuE zlS>E9p)**4Ea0k;@^Gk*bOY6FY3Z8l@N8+wb5G zz0AQ)tzVGIx|}I}yt1h&$~YvHhVOevhs$1HdvmjI40_MP*qFy{^ryL~c~YzUOTVm` z+gFW@rk5Fd2WpkBo;xq=Vp#bxG4VP*{q9PVkE}8xkCQ7SJ$+zs5J05;BZswAcE1D$ zw)aw^qj>b*0V-{vfZI8X2@VNCuCx$gEfl+i21bl&^85;^TI&J8#57H)Vk zJ4j}8(DPE#j}Fd(m*Wy#YC?@wqZG0ooa=**V5=hT2(@)dRCeR@vh8Cb~G!YYDmYQ73MLlp#=p=DKh8gW^Ier z5g*7VWesUI9(7J~E!5=7lJK3^qnw@N<}QE|B2|nqly4sd#JTP*#f~f<>SG-3oaF23 zjuo{3^!X|r=DtkZcQ$t4I{CSZThAtYNK;zItZqTlfF{~=YWpn4RzsiA@gV_=a*DIy zdu7VvIC~!<;5_SfSMN#DW2)_Y8%}?v8AvC&i`1HGYPKtVsPm6rUe@~YV|TJdX<&g9iJWATC zI$T-J@T+$1o0sA6r2YkwXOKNtL42;JhDNncF(nsG`-GRg#*sJ|)YaLQ{<&pY^3dmF zRfcKl=|r|x{LZrv#Rtwh(-+C=ke>CE;wi|tR8)?C`||zTR^aA0BWF6*&#Pi9jrJ)B z%ZDbEs*!4goToGR7X6}@Cv_g(ulP`wYHoB1zk&*I5Z5SHx|)`1X<;5zSRlzbyBYn+ zc{Al9g0owmW>o*vxMpm`Vsh)0o!xiX*6nW_W~$yOjpnA<}L2|mo%{ojL6BE6zFUi z85Jcd@y-or-Sl;T*Q>tWxHvvOa3u}{eVLJiKoPO0$BP6;McotIc69V4TFlAG!R*#y zrJ9N>UdvHb1~tWL`JNf4m3`bh4mkU` zrxlz6vl8<1^7i)YBciT`M9t03F0=VFTTD(6>nO6UL!Q*Rzp=4a#HIEy%*e1H$E`MvO<(dg%{{_| zii-X3Bh%_=+zH}eAk)2h9j+(Zc=qX7cqOe?{lI3 z)r2TxzHISW^#5Y+Ex@W?+kIUWK@kK22}z};yF+mR($d`}-Cd&60s_({NOvRMAT8Yt zk!H{^fI*G(GUxjC`u5r1T5GR!opbHu#Wm-Ii7?<_&-*<0?|#+vUc||yq$D%;%P8B? zkb&ZIzWw2~2)o&^ACR(Vin*_Em`Uf90C+4ELt`XEKn9YmZQQ#|ab629m7S}t@BT=* zg;ppHgK0-hoV7;l!Ftx(2w2_^4z+l0j-*W+m$@uv%QHrVx6F(!_;9flG!%XaDJ)`) z#b40|hXy|@>1}g*3+^^pNPp~YKRQ^3s4CAMY_W~%L5CRfm%MqeZ+5Sp#&)&nSR%OJ z6a?8n#3`rC$~Tqv@!HbkE1MV-7kW2bf|;LDrk&TU9^dmB%wEAY57aL`&qgU`0&yq#hxz}MT_dcXg<2U35LU@l3%VZ`6`IV1vN_{%Ki`-af2a+@fDKk-TB~^&btk!q#8d@=9iXk=Ghv-6T*yH^T(%OlzvdQL zm zw7ZzrQ2)xq!=r5R<~;}KOj{WEf!PXoOv>F5@RezlEt#rJ6TdUz*3{O5l}GV;9p@35 zD=*WTlOaj+oP{De*eEZjF)u(NBm&%EjIdepa|g|;X2Gc%Nj}FSlz4{-8StioVtipC zn|%mvk)qFWTHZ-3G`p!dF(frrq(D|3ohn8~$3BMd`4F zjFprS;KJgN1?|G$n3yOyT)ZhIv{On%mJC-1VUT2z9HG*pqUcAv#!dti3k|W#)h(s* zA)mIXKu;o<#%uNI)`O&}0~3u(ot#~lj9A8zhqcai<9t&D4-sSp^~{|R8r2nsgXU04-c>Z1Q{nU zs0bmV9&SCoAO+1Ku*PPZMJddNO%u?6WZX}ErcX2Wt$p*H*?V#3SAKbRW<@~AJ@MuD zV$i{b)FjW0r`4I~Ax=Du?C-LVQn23FL_7-ae4e$%WiFq=QIx;XaYUec~2|3(Qm1oqVM&A5giXuweF%4hUeJ7lY?|iAoO&0 z!2@k_N;RCj8f*2#W4*Wpf-|P?Z4Me`zeksu37N$BVEe77r!n<)2Hliil}aQJA4x*G zRt^c48XQGfJx*vUBGUt+r9-p72;xi4W05LAo^JI$JbvukWGU?YHS7h5U z%+=-3fKE_L5gO8M48MMV2PFtEDvwaqBp|v1P?^UFE-5f@OCcehkRZWU00IPRsX%!! z)Z_Pb$>@kmyei}7O8*AK!wwGKMiX!Q9c^Lc(J0NpQg(E+8(7le4_x0Y{=`{$B zk(^XweJ+^YtqmRg{Q1(Q9jBPNU-kA`wRj`FwP(y4{{$B|s&{;BthBheEyDyvqq#ZF z%!5K(=T^S-XMNCRBK1aHnICF2C!As?EGj5a%4fxcZG)Zs3JVTvs$Ey{i5)zJb4k62 zN(w1%NYq2Qt0%l8z9tY7!JD5YYybJe5o;DWHZfLOT1fWw4^DIG>;qFHKLTEz<&u*L zSU|gpnwo&ZaC+tYfRby6^<1i)MhNDA#%B4-NmAm-^;H?Ds4TiZABU-fgD^tG%{{S_ z0&W7CxUarPd>#tfT&?h}pXIYh<}MaSMha^f7?5(BSMHWCgOhUl&t7s`qaJI;J8$ol zKQUm_bK{~)rDo8V7CxFl)TMmdCehi4>Z>cdn`EJJ6wvBK(dZ8_6cyT9S?@oidY<8I zJ8!(^-oCNeqL8fJ&?hoEGo~Y6h*M`U>qv#*5WWNl3@myf++Kq}HfJ35G!Ed$E60e3 zRqa?gr;(X3_XQQ?v#G_`Mi&zDyKY5x&h9J4jcIijC@Bmk*F><>s~a7IDbo%VIn`%u zeAD(26kOZcrmY&Vf$B2lm!zcjE<8a&!BSe9pS9YxS1)p(3q5~+laMwai-!-Ym&9Iy zfR`Sue~v`Xbkwv2HqD;?F3V{UJT`uij&-R=`r-P5fzM?#$G(x7_uDER>f6QHvefAf965P;BXKDJ z#rsi9uCAI=in@FQif7166kLuf0st>@%wi{&Z8sq>LpSGYKJgT(>+<`TR#u8nA!KQ` z$0ve^SXe&l$KScpo|<&!b->&oXT5Dj7MPkL-#}rF&|TMol|$3esY4ww9y2Bv0lk1* z;%;_8)fN2g=Eg70m!5NBru;>bc@(Jjb8~Z55%g7(zukE95RyID`CC9uE%M50(JEG^ zKrHmbNGiX_)>R)k2*8{(!Vb!>Z%>fRz1ckL4)wL2sUdGEdjoi9n0Pi%T{zJ9mu^_$l!DNpCrwDctx z(R6`YFG);fk+pB zE!xrU>Z;TX>?hPp@~L1*GuqXyC2bd|@JT;_EUr118jta6KD8E2w-Dp!{v1NZW^e>| zf?XI`N@L)n1%GlC(!)q8nKR=hu=UBzmsK`v5Lsz*oOX|pmqRy;O>fYJbMjINF~xdz zRBSp^{01`3>@FB&_0P7srJ1dvp$B3NY!$RVlUPJSkuFRT)#t%1Mj4$MjmyVH-*gCr00%4eH9D?9qbJv+O&LPuGX0A|kg}wwk|I*1aw6Vq zPptt1(Pn#1Ev5LFC;q^sZQysazS+JAl8caRpSG8MRb`o@LoU%&kL1^ve!!~V0HVPm zTS%Q4S3+d)dmET=-g1+t@ugYm(i=jSp9m~oYTer=)d_tgRn>b_-ZUJwTN`cT^xm~j zXR)w#ap1Lq4u?iwUYg%D{8+}S$SNh|=k#bftnp@8QjdtYrdonlHiV(8Qx$#)BL}n| zY?V9J>rJUF1dI_OPo8kcu<$*4CqJ~CVhQTvK)9Z+t~b=g+CszeRhhIMB`q2nLN3Em zk!`bQPewZlhMD8LM`7pv&s|wCR01Xa3<1=tr-|^FqF+ILC$n z>Z4LkO@6Qh>$6OFZU#mIY-WL(#R5^CZlc%Df?llG{rowwL87Ehmy*kUs zQI*YFV$(xM2-d!g2#qMTJMX0unK7D5(~asz?^Z-o;DXZp)cT4H<+UXd2%VGfOu>*RX%@0ci`~noa2fBFb3}XClK6e zrM;RSTevzK1qt0Rab26|TF+DU(P9aG`wFIuw*PJ!P=ziWPg*4iUekIxa8z7`}z@Uux(aRi&rvi3i+9ZalfTX zTFq8PP8$Pv=wXGoV`N@MsbEy+X(itbV*Q>z&WrnC!l6I^tHXE{rz`^FtPbx;0GvS| zGS;HXqF^s{2D-aCpb9y7qp`6;_P)M0@2e`_w5gdD5r?3CZyua3dFr`5U(Y2qJN7-L z?Hf3*vDP+krN+J zDjf>pc2qu}*y8FkCO136KLV1A!f?KE z@?!nYQQDbkb#29#n|?=r{?BXwJ=!cZOd_CPEQu=1009LsS4nTV$^ZSued+S|!6^Rn zvdSu-v`iCgoJ>;7GKa=@9c}H9{PObK+H7DxID`gIT3Wior*OUsnQD}!Qr3JElvFQ} zFu=pfsoh|M2#SElKOw?Vr^WfkjOy%wY}w3bf^fSpT|FXgg`XZ0JPg!jogN#r>Q*Uo zyRPm1_48fG$iNr1N=~RR;7qUAO8}81OrfZvVttxQukXJC7~GK&Ar}X+n}?_P@{ zyuRBBp^^uuXif<}uU%m?Sj7VzhbtiXG83Zw*cHKyyY-Y9?K4T9>7le;!<*dwD|RUj zdW)#b1d{^jBejS}vq=SzY z#7>w9G81Vmi=ik%HJv~08zJH#;Wje6Y?9NRGKegGRv}@&Dkm>Dz<2=gWE3rbh zzaXN)E8M!Aftn$=dm1ZtNqaHvPFLVKJ2K+r?-wsrReq(uqNBrR>t*IL4VNt0b2>FL>#P00qruJtp;Z|&HYGZs_J64oO*MmTD^My7z3}cL< z=Nkbb;S(ZRtVaP0U2)AGMUYO}FI{j&^?F;ajROoaSxZw(>#(nv(yzkr_5S7J zMdx}1)Nf?jVae-K1hE!NAHlSn+K7B!2Z$X#dQ2y0=cvf$V_|$E(kidXuz3v|PXw&2 zxyi4SZ_fCzH^WcCDR$)QhuLxAo}jV1H){W{%sQb6#5WE>>1yRaKa{dMz$WwO3;~51cVE8@xU4m|Q2o z$jHcsSeVxb)A%g*gd1Wj%1>I$S{qEJ?LWHeU+BaKSlZhoM~c{*ulFtisQ_#EJ~8q2 z>T;eO}vWgWDf`j>$SDCFM|q809y|#`acTVL$j1iXjo% zqB)b6EKJetDmWadn8aXRmt?o~uoE+}7H$&krkGR@YS-R*`|LXC&&Cb@ez3J^n=gxr z?umqoGZUAywFVBf<7%h)^TZt@1AWj^eZfUkF}w52(OpDwcc$Uhb5l-}Jx&vqa8~J{ z!E|55q7eM;7w_LuyC0R8R$-2{ok1!V-o8?b&2^g0pc|nnRR$PQgr`V$gUFca!*hSoisi<_CRTSTv_Xy$PdQ|3GG_oMi`qDCfEe!fA zP+()HbgdnIkWzd|AtG(`prh}*zq*FnH{-ujTVRkPiiB%X2CpWyyDed(qtD$XAc!06 z*!JD9zwu(MvCcI{gYGK+WGc`SZ0}pr9o%qrZeg@aHow|GP*KsPI|n<6g=pvO2ClhU z7$5sxFcsu%te;yyHB)T5oDQ!NX(Gcq*PV!EK*(D_dV#J85}w$=Ukzs{!d4 zsr}FQ(IZUPQ6s{4S<19+1}6{tR380B7J<}PQXrolO^I%c%i6d#*l5GU%F4_vCxMCz zjG&eD^dP>Q7%fiSmEdj+qW@vtf-=L>7xs=kf>uilNr2qo=n#7;C3Q;$@Y1d4QTqRy zoFwg%e{yxT65sLRihiA?yoNM4H+M?q_-vZA#hlN=$gKp^A8hYz6g zPhgbgQc5%KKjc6Z0o6A|G}NlQzEPe;GLJiT!o**f3v z{l`h(6>0*I7-p@D?5c18lecDP^Z&U*?9Tg{j=6$rbLDgAGN7&SMu#S{=aO}N7Yg&) z>$nUZ@?c(er}BIwxDx^v(I1j^CcWKkDfZTFO*UuZ;T zN{i2(c3){-`XgtD2B5!wf+ez>5OfqZIwL{NxhSlUD|O7dVI}CI0f2jab=5pPbz>8F zn3EEWj5|F+7nRD=UNTg%Y+i4%&Ufj2CHW_!{4}Lq2RQF z2ovbIS@7AbI9toJ*kT9Ik1golvUsk?J@7=ytH}0N%J8z%D7BiJa|Z<2x)Y;D`aMqIHY`f3p!7Y#y(l!g;wnv zxF7rzERysK8~g%IhG%?;am{kN@tyu{PntGg&p{O>$FmdqtkmL@UUuz+2Sm`HGjhe> zr67HwiJO$*+y8^~eDD2qeY22t?6%(>&T#X+Yrvi6wywthQ!NfY4dvg_)52TyR7BAs z>JO5%;1JC&-H&j}4e~Ydal~L7_{3YP@$WyNNqQ}bKnv=>(NfnYyVS%cyTx1o9VP|c zn{AjCmpz@7p6GngGTU zj(__exBmjm>wkLh-@pG~AAD~3PyY`Ez>xp+?*Hk(cyRDmfR)Oui|TnpNFT8 zxi~iCy^wb&i4_^8moXuPEQu-U@u&7|!+cOXl4Z(gtu)|94(vdT^(hZ{*uJu zo>Wl~n9bl1XzK60D25@kSkB4pp4hm%v+&oRS{#h`ZUIf=_gYmOEdVtbgjq}%H;|AA z{pm4WywO<| zDUd_)GSF(lwHm9wZNC4YyMsdU1k_1F-WCAU0;@tR)LV?1_^x>7FA#YGtII0pl439? zxUD!;|Jq(r__1*MoC6UXm5g+ly^YPw!)1dF0GBFZps+LC6Nk?pYlLuDC;F?a#aoBF zDzTuv@xpTOM~fiqUUmRvMT<5P4J<~81^ERtwQqmpzXKc`&(<93!2}nhB0&kD+24AF zLWUujU_?Y8d3AU>EDyG{>Ui+6H?^SoFyw_O?_NBKU{-DIX;?>p7iJ&PnVzMk)vGnE z_}^dHsT%xPo^Q*TnwbS^7USdLB`3#xP7aVt2egn@)w+k5H*9N5HfQYWs;SCC0oign z-P0qfdV3rA=y|20<_bTd26IYjp1Q)@*WocJ{qWd0i|WXU6Kqt8c3pVqF*~t1Vdsz! zotJifVz>*$rdCE;VRmxGQAx^Vetxb|fJt`}SEWf$QsAlP=Hb1jo`&2G7wM}c*kN9o zJ3uuq5pzM>Mwn8Mj%!DM;qpxJns-@bfVDGQZ>MnuUD;DgZtHvD-Nx|_A#yvP!5jES zHomqNIoUF`mGLd>b1X$@Bp*%^2Q&1cISyD>cbCF-)M3Qo0}mNnw60$}zvfb?!wT*^ z#CbrI6@s||W<0Et5VvU@7h9(XUB+oSima3ZjG6`pt@QA1~y0}7_ak6KPn&INu_ zuWJXQG(AHn;kZQDW?j@==pwTbs$rNo#8>nE@G<82eOFSI%`S7r<;B-JdFJ1}CJj z7XVPseeU3P>owD~`tC(>C^MRbh#0J^cAmu{1jQy|jeo?h71 zupVCv2p1CVIq}SplES%#31Co-iH%E-c|s31z0{cEV^6Pg$myU5F)chnx3H@ikoGz8 zQ}rD!Lv3`{pRu7QVw*Qo0PSB^#S*aRFxYmyIGcfbSMrsgn_}L23q>NpP7V?ssOAtW zl7P_6v;UGA{Q{&qN=o|rWWpm_W+pm?mB8S&ro#EF0TpnbA}>eIl~Lbw7BUW;?a%W5 z9zwgD3I0}2O>;xT8_=61T9Yym0G{1XVPNou;?=wCxZcD&Br+Jdw>M{RWxpdbYSOFI zXw7J~->}B{OS{6Zq6LvAJw8$M>9nO5youtAxHmO77PLqk5xMD44Tx&DEj!wkfz3N8 z*8h;Ldbpi+r$<`#SK)s!k9?Mu9rD%RukmfFUjLdSnIu+9lUHhFg$E)poL98DzNS7- ztG?l6YESW$1?d7Df+^*J)CA(VRr+a#Cd_uWc8l=6{SZQigF`s}!@ygV@-D94XMQo) zITun=9Tk<^XLDwyiVpUhA%` zRkL-i-MQw9gF5V> zAyHMNk&~0tUncOo@}#IIf5W2_v;e{&z?eQklzy8?^raM<6$O}pM0Jhc@w zGdBe}W;ArP=JPQokUt`ALAdpGc*@JWUw;LT4*xpOqLPxdh2uO;9p>6Pcd)PA6i7o` zv_Ser$FckL!6{Fn00L}_Oz%N^l8bTAEa^Q0t2MQAgZc~ObPoM&i#WD zN3o*EXyVSYI^Ydp8NJxZC8shwN;Ki7v zO81}#L{)F+s3L@1OZXdC1BVo0DX_@o6*fxZH{rwQ8`GHIiO{0-2$Ow9_+S^i^q+85 zd=|u0L*F5TTEnVj9ebo5MYG=C_HhRK`T&{26wSiIqRYz1PjfPD;Kqnpe$mJ0)4Y&4 zE!oo(97#r>b;~XOYQOfKy@K;CF~4}>2aA{n2928P>drhlJ@^R;Yr^GG5mKwcBjCtP zLrDp7HSmb~3Iut_xJw<2Dr5+F!*JZ!@9%7G{_N{(@!2v4 z_+|0nAh_+Hp7N5D>mlE_ft#_7sp$#4XWx7%aZCkS@ciIp+XC#B*n0J+>eD$tNT&?W zII^{zgarkyCy?@ra(J7+e%%~nwq+mTIxubD1!6So#|W_UX*%hs0>sp7{|HY{Pp;7q zpydYSJyD5)fT8M*A_v3k8N|Roc));U=F;i7I-(@q+X&pg^nmyxN{2Ut->AaqmvsBKT1kO6cib! zz<<_5XkHA)!i_SxZAI}}LL$O`L=(@yHOR{>*j*<7y}4s#Y%bt+b+QtCcH%bvQyn9N zw>7i$bfJJc5UswXgvHA%2oUDN>Dv3<99(tj{I(RqozYL$kl<|h4x;sXakHz#znx#m zJTfr+b6}{rm;tcdoA2z29!w6g*rG~B;9I^^NU!*vkbzX>rKqUR-fS#e%9~fhFQHXz zeggrRjI_dM(gpl!D5}YM6v+StGmCm8CyxdY{pBEuAd!4=A?b$A`jNBH$?(40PDmFf%YuPl7F!3y0x8FmI2S7Wo2K4OUtyFzU?-DEN7zJO*!;s(unA1{Uu zzp{KA*mZK|aB6R1b)I7owqdXOM6IP9v9;$vv5tnTBrmZWy^mD)QT@pfDL}DLy2oQ& z7}fFSL+t#pHhR_#~oh3{tnW|F&Scj z<;&f@jDouRBf2dr*Zzk1{I^#zKz0KGhiz}K4=0x%JO_j^@Q+&C*jyc~C`NpaXm0WW z;eATg2(UC1sWPYK>Xuhg8E+9ozI&JNkL*+g>8XuDU*q1>j5ualf9v9Tlu6Kj8(woN zgXrIY9@q<)WlGNQ1W{?o+S(dv%Y`_AAF8XVgK-p6M4eA}>L1?u_~q9q#N}e5uSAEJ zOBN87puhov7D@phhw@0 z?>9W$ZiMyS{$^p3#Y@O&^acR-@`j&M+z5DT1`gG_yVTUWvU74IpbVc+0t1>MM6uuKjPDU55vkOn&6uSu&c2Y zMA_2XxC0y`kZ;Mev_C)J@|IKZ*m;pJNkFd+yy@JZ6WeWfgWu z@?&;JtdOs+EK-ClhEJBhW=k$+@8QwRcxvN=2ZxLK*zsf~<+l=1-yI-c{siL4B1W^^H&9&3KT9N6bv%74JkM9I07?y z`G|?9c!q;oKaOS-ppQt!B35%zsIn^YLW=|Z{)yr9n8){2AjI2TYvbmw8A%tU3Mc$Y zA%bV@y+g-2*lyPGwcdtT=-9j;#N99PST>u_&s4u8ObXly9DePp?JdYo7!Fp6q4_!W zTbY9FNnf))z%L~>-X}tfb8r;RKoiB|oK`Q&vgMMGSBSdC`DHoc2u+O-G_xmL85?dR7GHhG`_*q|k z)@m*Wvci~z6ed`5bV{s=tqGT-vIj8j;1UflMp0o6{ax|ti@t)Wg28`U($wz5m=0mU zX=#z5g0TvqV{w!I52?$%PuAE$cdO1GA%h@c8XVd3vewi5Vq&phoM+!}Cxx8i}+zI2oDdr- zu@F*)gUgfb0Z*00#sPU#c3_BNn*6(D$$Z8nVO34etgn zI?;v6qG=uV|CaE~j=~U#OgPT^d|FUGlRE`67N|U~VQgF$Be)MfW%!-0 z9hrq|Yim>3xRxM|7t^8su*(Y|bnrQTG&Nq7!BbcMRzu?qL`iWTm|C6Zm5A*uRM@_i zME%Ec0$^xpK7iy!cq;N-{|h)yYs1kJXN5W`Xlu!P?F_aa4C2vB;x^e;DSW{L)ZK5SEfzQ<;!7D;kFQG~vK z%VJB;8f5{D4tACUE_h+}PNI9;v}Y(X%I@ z!OM;!53kd~^Yqa-n6kIrZW8(93T;inz^Ue5vb_JbS$)@z8>Oys#PY}FuK~I_7u^Q(vVSNaSvu*If)m176Pb zE~5c|DovL4G~QIPcILqZJzgk9ka$7CVIz#r|8gGi&EMp-E_6(eK;uyK;&=0RfAZnJ zz8k2{K2}wo|5n^I$YLC#7UXOXM>7G%+Krv}_9`?_;l|d?cK6Sp?LXV=Yr7^^Ry`|+ z>7ILmfZfs)s>A8W#s2#iNbyfpob0C;DX)SyN zqz#3y0q678-(ep+kcU80a_kp28}a`Gw;Bq{^R2`0QoI^b(Gjn}K`FGhvb1_mMQ*YS zm>P~Tx-6YF_c2%XgsFuV9NryslE>y#&$eSMlPsareBhiAQjfD}?t z4gv~cA>VBp+@WL<_mf0NoYe^nplZYMAVeK{o0b;mdj6{&tWf>etNYl>eA6*t4T*m7^AmN^60hQYNeoIiU8xGe4OME3?+abfa zoQ6Uw9s~(4Eq#w!HuM=47&L)?SU-_Lc4k2j2vu)Kd@6NBBiHlwdP%MtCsqzr6ks9+ z^kY{?$9z&adv=y~bX4wa{HVer=y}h313hB@A*q2D)W_?-t71b4Y*G`xwmS z8MN>oATvf;^pckWGpmcc2OjkJtj1tQlge zhYv~m;!|cybJtJ5e+8EPjtf@Sv;5_$VT8p!9B!6!IcaH**b%qEL`UDg5P-AgFWJst z|8VquDDyWGr@3&pvNKJmza%0lzW#fHI#wl z`9*qjZIa>p%;kBkl3p*(wCx`FMgi4JM5`e3HVy)m2#K{p<>VF2ZYZ4?t z($D_%d;$GX z7m*+kf^ek;Tljez(wWEW*_TAfGl%3iseaI+0uX5>F99e@6KIGL;NOk`?ef*s_5fJZJy=KNomKPd39l za2*LZ5$yZ;T)keMe(C~{Qh&7}LNwWFTk5hM2mqssli4RlKE_}5I_wm8mBQfF!F zxstgNjjblQK&l(^CW7rTY;!RR6&Ch4=(budCPR_tRl)~dHbAS|xtJ7}Rw~y_uN6jdyLXC*V^J~we*+dKstlNMj7)bzuSMTX?t(1h|ik}qRY9r(3vsIzlP4v%%YH~ zqTRxI&Bsf@Kz|IPAs-tA_Z5Cd&J0CI`=UMsaf-)}l= zU;a+t(V6$to{A8*GwW@*D_e$+9$8&o4dR_XqENfUYvhtwa8iAElCw0p>do1}4Hzj2 zvP1MlIOcaVS_Ti5bya_X6U?~cvpmbgPN`fRAm?mN^nDdL8CC>U^bYs;LuWxRH9RFFd zD##cOs_6VfuE+8JhFs6_{0OvgfO$vkHude^ROP+rpWBFz<4(pg=-9=MaL2Tl0aW=U zs@@#q;?3zE0gMoqwEbD1+v+4s>1f}aeti$&(%Qjl#c1W;(zwwVMU-!iLDV7=yqZfP z*nA8qugpb?nf8cY?PP7K;nRID)w(EMyFJ%hM(@K$#RoiS;4*0S`=KG)?fBYurHpb< z?-=d`E-HaCg-dv6Gyj%&=9i~@4%c2Es)Nbwz~(j}$OyV-f2)b)YJFCM;3;)CgP;)8 zMx3ZHWtbDHtE>AQ9R?c|Lw{ZR)!mePuGB%D`BUqb{1p8VtI{ipUol?$A0js<6PEnW zu18IJ(9k2Wut;7Y?X0Z&K@4{_p&03u_VFXgjdiDjouLNPq}-48<8IF28tyw6yXoFQ z{5W&enLBBn4wf>AGqF7%74;I$_%DykuINC%e{YLtacmmz#aWoY-AA+$a1HOgY@5aTgI6M)W!wnGROTU z3SCkZEptn``~oI9+1s$V6iK;5x2J2ShMB$i2PD>{;hfQjJSV z%^-2*qGpw?JzC{iNkT-P`Z?w+E9V?2fhLjSm5=#Ye}!G=JF+I^;b)?KLgFZ(!yfh29KL8+JTOap2WmtnVb^5(A5rvbL-yRi?~zQA19zC z{etY1OONuMalh zCx@uVgk|fPpNv-W;^GLoLLZ^Oy#jDqbE_Tv0K2@_84Mch>S{nb)WRb7){wo%%lv;z zbJ$z|Hz7q#%vXl{s(mH&-Py2w+*%HUs4!NlUh~J^Pxzz5lzg0-Wfh9mo$gR^3EC+-C z{+4YkFMwuxJL29gO9Tqt|4d2P@fC9WIpEJhxpV_XtcwUb9K7?A*yyhh{@XwP|Al+~ zH^?;MK~*I*l&CoHu<&cxlcmzGHnj87o-@%z)$w0iUSCg-OA-L0&H&#IpcwM>@-%3S zFLy#H>na_*luLbba}%8Lcob+K09_Jy#cj}v3-v>y;+AODQ1SAvLp7hDtKUTtJ~AKH zu`{(%*HWtW?mf$o2<`fd(i&&;y zoerBQD18qXLL(!wRc~8CDC{Ah#53~}_->5?TH0FogFpEp5El9I{xy?D>fi8C4odEU zh2NjUiJ(RgTqV#nF)=~C6>mnd6jbxD(Q}FMRsCRSZE2~^%uMf98Rug^h2xBJu{*&J zfIP6?=@5(Y;RETYJBE+{JB*k|YPX~W*(_34+{Uyb`&;e0dW!1YTZ4qyI6sH|NKp1c zR`&!xbgpG(y=U)0K|u%JU}9Xo*7(P+w3Q%3$vlODfgvw1Ejc8NO_mlklFZXPKCh#w zXuue^zOj)e;(T$o*viYxo8<(4jlTorrgpZsq#%6bF;;eJ-_Mb(V64=+*d#B<61WYl z8@0fH473ELy?Q`d&|MQjg8Kdyi1VeUC9SUud?+a_{BHeL_iL2koqJe@?1!rLRo8i+ z9mzp_Fmo7xf_p0GNj7D;m^1U9%jM$nHxWJ~L_bG3-m$VDe~q>6xs0>k_|8~US)@&5 zUqh|iSbv|s37W8<%@iEx=1N9cjcVR168W{xycq()7t{Jymf0w5(~=U1pV7-!z$)Ut za-@jdDJ^XctUCDS@V{>Z3mIy~LQg*=6{z4hhHqN2RQ z!sNIlN6_;DB!3NHMD&sd)`n7fgn;{x$=W*eZF6ld9nj|P<5{k?G(F3IJw;bZp!}!s&@6SfT(Wkte{hLDJm$N@%3L zeYCyZvA=q2M69Zzy!=~Zr9<9GA_1@0=bDHTK&siw%Ufz$${a0Yx!1H1yGx5$$e!eaIWCxu&hg4F83!JRnf& zBk=RPK@SSb$)WAzi*ehV@#7Ma->QP{laPLp+1uB@xLnluz4NERW!6c9&pq=ka0og9 z_0nmEe_=jD9>r0baQ)SbR@^&aI#^S~1B|A1*G+JJvn`mX*72aTVyR}**RSW}Q0)Pz`0nI5re1cdzkthxCoXnf)cf7bsLJ13{CzOX9JOH!<(v>(%kGH>^Ivv zN0l+&@gOGbu)Wg_wmt#C!9j~2xPzfG-0X@K{1?7H6?YY4%WzjfKMW0)@Kjd^isS02 z)c|;{sh9?+l4xHjFvel`>w{fce&;*d=~J&}tbu)PPEI1og1v!HCVUH{NRAd~rw#pE zD~~zXpk>FZ0HoSt8s`zb1}xd)r%r)4xYXkCMVmT zug}wBZUCBL5x$dMa=o}-0`~toG8-if-`yn#`>(=}<~EXw-aMs>4zcuYfpO?)fIse^ z{M$OZWv5#XEo3XA;>AG20W&-!bd|!Jq0>uYP zc(>Ep!)KsR`8lj{{tqphCrC=armph#`w)K-@kphh0adB(?NQOJl3{mPFX9`I( zu71iun*iL9=kIrU+;6bPLy#-jGa#OxxF<9^-*esRS=L&8P2RnZ;aakPvvjJ{b@S|g zt84huA4iW^Vae83_iO0%umYUq4EmJqhZcX*7V(A#gCuiC#<{A3GXhT>h)Tfxh3ZC1CjTL5b;n3F)=oJ`jn8YQb=uewL|>^sC)pQRSJ#uHqtUUkzQ9HRfIzLJB+yvf-L(}IYyjLfcqY#N z{J9B>fDR>dNMxF5_$!)CS0KR!p`xyS+9yTX)!oC*%{?c?U#x#z{DwyPNEQ2APtdoR zU>mm2c^YCWUowzuL>aVQy@wuFCOwhfhdCPH^II3n4?}0UNJ;j(vf=-Q`Ec9v^90Ip zIDqzXQd(}eow^h73b;lidsiYd*tdXuO3ZhAAB4ROu1Ea!bQf1Y=At$-UtX4w%_u_O zrputbe{T3cKs`3bCg9B%8XT37(g3T_{tHz%+Nnv4T&MUa>x*`jMpg9kDI*X}&QVk)7nrdc4Lvh9 zHU~Rh4dC3djFlNzF4g#&FsxTCqf-pqGo8!5EbBGpPLbAuhJ1Fq8v!7N}NXf6r+;TwC z!q3{;UMTutB>7O51wMGxk9jIZPMZZ~qx3QwlerVM|ITWf$M7vwwbkjEX{{y>GBGiC zb(!-qf<>NE_>j_BFLtX1|4di(6Bp-+zolg4!_47jG{^)PUlEaVb%;IV=l;K_d+V^O zx^M4$Aqq-25`xka(k&p}4Fb~LAYD=-(jXwxAl==Kh;(;%w{-JNyieTs@0{nH_kI6+ z_q8td+HCh;n>E**bBysFpRd$~mv;nkpJSHgQ&v}(6aun2zPoB+ZCOvy+!ZSG6HqGy zgUBU3Zh>uaEX5k1$NUW#hn?Tn4U8=(uWxc9DK)@Y8Bc!7J82cegMI^ARe@ogbx}g( z%^>`~Ek^<{een@&0nLIK7)w!;>C@l){wb?Fz9f$MC7Dc?dU$w{eA{rPI0J>P-V zZTWsbAHc59-yLAW!v5s6OP(mXF`kT*edQ@Zg$0OJ^_N{!zyL4C7YGn&X*=fU3~iYW z`y>H24HZt$XN%{n^Ks{_}^$g4RI zL4QxuocDdPjf%QM**WM5RIEx1`(MR?vW5Q?0~+Iit{%X2DMpC5+}|*>+}1JBdxgkg zZ@9lPH6Z5%MAnn#hyQ~Xa8pv{w^pF6W@aJip6L60$#oBC!^A8Em3eSyN-Ik4aLkZy zPK|P#v$5Y*YT6VI)=tH(v~rl5Z%>FbgZdz->|NBTb?yB6$^FG2e{1_y;+6WMAPBS- zBbE|1UMJ3ea5cT%Qz1XQX@+|fi*Igi%?Bikc6OL$;`e)1v6Y>z0&5*w4AZf(Is8@- zr?pFW#B~1DY%sX9h?HO+id;^m{}y#R!8UY8Oz!3R3|r~P4dx!_XMcS4xXR=k&BX%g z#NR$30>f4p&+VR?vUYm+RVfNX-oqt4pTI~>`Vso3>4xL%?2nCQsq!~#VmFVDuG)AK}7?Pjhl!0{MW$)gtUwVAR*I z9kp}JyZeVePrAB61y5F1wzJ#zvBxFRc@+kv^B(LwHa7s9Qp2A8W}AklD=rQ{hncK7 zrtJMHu#+KR^T6+vh7l4nB*##Ly#dew5;IH7kS15P-^Q;29kBB4f6xKt#RPyZKOAjZ zKifIRr~A`OyGaYjwcf9wrTdKh-GJL~>~ykU%)P5j_s{}z&>wKAy7xJ9PE;l98m;W@kM+sfvKlIEQ`Tk1ttE^pmA$wg$adY?tsVmA(3Y zk2G-x?QT?y)|T?#Qo#Uog671}a5(1Gu{`MHD>Ei$%7jGm+VVyCo{*(!jT+_)=VwjCgjN%W2ZDs~B|NIVy^&8 zwo=-=4e5F_WtHXuA#!m1rjOexh@%7*JHbvmfQoI+Y&;-qNnbyhLbjx|6e#(nvfBWi zu=52vF2V4LV4+qBa8cr z%Nq)-IU2pIq7g$l%D31tG(`f`3fD^!Ap1`6=W6ImedyK!mQXf^W zU>yYtH`tozLMp37`A9!AZ~FQGO! zn@O=1uJ&n)YJz5yPu!>-J_`EKK{cn-WszOh9l+XyB|9J|qu*a1IDL$;$rde+|{(R3;T{& z=xC{SZfi1%fr*LgeDj-F>5W2h>)yMormEQSSFZ{|@A`#{i;qGH5WbD9Cs-fe&D0tm z0n?X-qYF^31B$n3=bLwC61EBoOTY(-AMf&w5JXNQ`T({!N!*2~=D zF||v_sqK|$V|F|rqc4y|ZjOzPcK7#_lDND3XpPZR(-O8Gk=zBf*MGAX0L_o99tLpl z)leq?ZR>Z{1-vIaS`+-2inO+VTTAd4MujwZ|K4qLXGS!~2?7fzl@cx>?a*}JM_p23 z7L(Tk8bo2H(*RmB`m@L?N=#6)VZ4tDBu?Gw{WHID81xvZrLa_#=jBm@Tm8_?EVu4e zKCr^O7GFAgR7%SaxRTEH)z`E*v$I|$QIm+t^c!x0ef&8qjkw8hEaux@=J`323g&9G z+P?0_k^DJ~kn?Nc#>>JsVw^tT(J2wn>lEvq(m4^p3`%~px3SyHVHo$yM<*-A>Z_jbtwhx0bI=F*QHNJxlJX<|pnq z%>S*;E0bO3UHOS0sDHkkuXC9^%!)CZ{PgKu+tEr^LMp$oP>=U7@nrsW&Q5M_sqNn` z?~r5$tCNwt(ssT&D{DnxRh(4)h$?by6WzyN>_4s<6%oc)$izjpV*AzZs(eJ}o2!xhI(qCAw~_h8@n3J{Umk z3fo1IcOGnjox)l3rPE7ldHtJbli#I6V4C$odP%GCy%}JHTd++JPV#^@?N!gO_oOou zi<;Tnmh}VR^c<2$5wB+=mu)jr`)b8uF0nfl{qd?C`0Chw6Dc7($AMTtKaQJV^}#vpgb=`wPm2=QI~^DOL+XzPF7#qlq`sE@oyov!^4SO|psC)4*j82#0@-_I6LVkBkJjFY_GDAc8!)=6ZOR z2MT=*yXOmZO;FY_8b$CGH4hhKp#oF%nwzigU#t1myH20RXg80ux&juWuyIaOIiD6p zUmH4v#ZTaBon4g@N(kEhfS?(bziT7Y%l@BfBPThB{2Bin(&sYKndqa1?~|mE!PR%RcmOvQEh!j|x$(0#0 zC)8d&jV<*{L`cas??dsa3GLn*WsmB#m;bx;0_BsAe%|ztg1}Jh@Q_@azN+w!QCtGV z2epYfKD}>unYu$xoAS;dT~yg6OifLm7~Wjga%DOUwT^sp6-($|nN>Ez{J*IhyP|pS z8G)gtPfg;+?)PhcC?22^!<;4U;j()q6?yYfI7oc5=5cV$W<)3v=S&4@Qb@t>_k=DO zIS_2;x9usCqgv1dvRFt+h}*k~xjC)3Z>c4PW~MJGD9)DaXPUZgH;{WD!9wqooiD+f z2%uiJdbB4~IQ=Z4Px0eHx^z?+-(?aHAh{X%+9z58K|}e4H*m6JVP~%50XSuzp6BV( zLMIihKFZ&=as$EjxTiivhkN+=C(PDc`f&p1Wfy$>*&YValtTu(>)9p=u>kguwb9F8 z!J5n-S$Jb(3$;rtm%1y4FZ`X}Y)XyJr87t@^;JrYsS+c*18XUsRF^RByRb?{5NdkC znGX4c5?tg6typm;j2nLOjtylTZuI0~cPT#IFkhZy{aqSeL4cltGiPCL{Zkx|2_(K{ zWPaxOU8f3R5fBIp@$-{T+G0+Rj?8#`9U4~bu~__LUkmVwHi&Tatl3Ne%N}drsNWX6 zEh+K|pwCxU2!sIF@6|RT9nkz58CXaO1q6(Pu#YnN!*OGJw!ipBnc)FSVm15(C> zv*z#NT~6j8>H41*wj@S6)rrd?SVLWmZJ<|>D$78^}yC{YwaoBPFB&!L?of6kLq|=YZ!iObr(1}$qgQgm+n?I&z?dJn(NlLCGUd}C_u-L;n(GTDP$E_ae_Zk`&s4zZx zLS#b&U##o9`??HVSA!8yqO{eX-^&`K_rl}yKW~ca1A5;Vr55X^T@n(Y_5y~hLnoTd z%tye#rJMfZ>YUMN>iFI;H^2Vh4Rc@CJUL<5?u);3YbYp?ro!?#oLui>4+1j&zd7bs zWdO(A(RNwps^aHh*X&BptOfX3G3zT{&2P4bHeTy0oa-cZ!92}Z;@%jmF&h0q4#hlhkS6Ia6pK(SwxQYYaC^_LN$Ge}L>;J%X5>7J%ej zb9%;)vlf&&{Hb`?nC$e?7;I@z7=%+wa(?M3Tg;qx5b_WEquV>E2F_r>MKr2(;TRwf@xVw5dGc(gPUGLkzI=k$W^REKMX)A8Q&4pWc z;F9WZvR8irBEIe2Wg0Am8jZq?jM2cXx4V~?VJYF^*OwsZ?Chu(7m_V_*RU454#;O5 zgrUB^tH6<(&qjd*9mFQYo=%Vv6n{+iH$z{m_+MA#K!gLVzXD8twEVjNgyrolprHmG z%2`Ex)Ep}S;>YpfSjhPt>p~h}EqL5e^W8T`o14QyA2ATJ^{-DOm)!wfBZHy}BPLb2 z!lX@n->u!}CngO5$!@f^2B3Q#9VVNltKhxW6*OL}X4yh;@2TPC^abTQVW)!teh=(| z_Sr2@2p0R7e}iPHabHr!chYYV&}@hc@hcrQRn5IfUDE3WJ)mE-?XD_`x3{)bX~!;$ z*g~JNYduFpc-R6|3|64d!vC6H&`}M=`_FW;M*|wPB7@-X_3W%CFSe!r#ZY+4(cB7{ z*(ozL^!>ju6yCU%VtDpXXy2>J2>>LM>Ma1B4q#W^XT|#??PtKoTL(i41;Wl%7+^#D zg2Y_iPhK|?blU^iOnS=ZCIO2}+sE1|b)QmP(4XEbQA;E!1PXK%vf4UkR=-w%1L`;c z=+h8ff#yv*wVJWHPRnAmihW?;4cb*UVbR$<;s73P`^Z0nb<9an1B6iYD zBWAO{K6OL!I~tqb@1#$DZK`3qe!MD}1I#k`iW-kk>-NKlm|y*c#=CF=P)-!0i7d1u z=aW_3ri&(NAh`x5-V0!wceqc{+TL*17q9t~>}EC{m)pq#v~C`q5HJGjV@%A3o$+0G zL&YC}pRLUwfS=xO-pX zRoO0JO~lE;5vQ)&6U%MgH{)VY*{QqfsqN{|UN{f8s)VvKUU~;cG%FiR)a&8~r1QGF zpPBnfT1Pu~?p3jSp>OIBhk_6i=exVRwLN!NPY4=8-H-3~*e)VoXuF**UZudpNUpE@ zi{>}j^S3CyAl>MkUF~|AlWPlRKw$QmBWqVv{w+!@CdV(v`%y6H3IP1>5kt_E4d&Bl z+~sZnGXb6frfdy2B6B?L@^gSswr7q9a&vKU>anrvb*|?)YsTGYu)Eg34O3qwpCCR? zWOEC+Zu$KAQs2)*rP?=$!%aYT{+{diYZe@$udwC*O(V%k<@O>K(Kr-zAkQ{j)8+4i zR@d4(U4n8v6Hq8S1XXZhZQgaOnVdm5e?rU-L^m@VY?4(39?)s;F#! z+WOw={O)w@W@nZxp%CjIm1AD90@He4l-<9pJ6^0jF~c`g3}OmvPv*pMD~-M&hF?&< zj{TI7PARxv7sGv6WAkEo7PHg;`qFC{Ol-KY#aZn@E7z6`vmdUlP&ku`8{fv6zc2`&RbxbF<72$xO`2Zyb#bwr8U zGD?gLw=en1VO%Ds+yPB?6*@lqQ=G~Ko3UsZ&$IqpI|_=mtx`&r>e`y$7axI{4uA>) zBw$W%Y3X(s&{3@QCe{FT1|V7h024SfJ$*fo%ir*tnhhZ00m!a7>%Sr%K0*{QQlkB> zGATyKX10#BSMDW?aGj5kW?qZRttSYlPe^Gr7>}?#4hGOk7$?W3b5+{9tV=t(mk|bn-83{YHBvYOdOE@oc+p(U+Yw7 z)VK*nivOQ}&UG$C@|kUyhE!l%aGPOfp6j%KJ~uMc)!GBBh2rUzR1}x1#4{WmSb?Ph zXkOc!f6VFSNBFO(z2iZ`yO-9zt^xiO9;M&2OP}6z_P_&7>mNV1V-D^m##47o%3r(L z{-<}d^PPeQXt5*8fCVBn=W@JwRMOP^a%TaQdrca6RMUsKP*d+E-QN4z-#!&VBs%&d zX#~aQzaCY{cJ|7+rXWdtEc|b(M$UipOD2*9{>>mD`3{tF|BG<(824Yl%)h-o_^Uas z>))||z+m=o50Iw$+vER7N=Q^xWhIzv8dYw1y373{q4J|;N?V1(?*ecZ|D@uv8C(Hy zux7dQ=6hq~a52!X?(6Spb$2m+H%-;_ue`jlHx*|Y=hbaQ&;O~0gyX{kza`Z8*rRU* zc=5__zwXZc<}m}8mq-B6suYdsDN;cgkF~b`UEn^!>6P>^`p4|{0yu%6BA!5`nVE9U zcpQU!uU#c#`)}ls5NK`IY9vJlxO6Id%A<~3Z8^$6no4&J415Z=XM`B8d=KD6{>i#Q zRY!h(weSf+~&Uo!Ojx^_W56|m2vOmM74mG)^JcFt$E_`;7v@#hPK3bO zc*gSM@JfF5`0`RRO7ctRk$Ju z;Fj-i4QA*0ab{p!4_BPv?g(xsJ#>&th+i0*!*@Lg=0;r$`LBDZW|TybV9 z72V!(2I)Ne$XmzRM?o1(jz8i_De|A=NIGhT&?bU|=i~Zqsg=C4Y0~wjOk<=`Q*nTQ zWj$v}Wi5+I7xT~8ZT_vFhrVTA$Cs?1d1`)jD2T?0oph;z=hL;?Pbd*6&fF5Qiu=e_MsWX5k z73oF6xer&fKu@XS(Fs~!BqSwjd0v;O8gE8+i-sbMjgMPfnKgRcOo6I)g!PPE<3ed^ z#})8aU%9KXyXv36ofla>`?U%r$#KtsrZK9bLM~fNT`@8)?(i%?A1tiE@XIdb*}pg; zAN<`3c@zz|Da&a@Gs;X~%Sj0zwG2bBp`gdT7 z2P_s@xm>6F)pN%2=&n3;%t1GHCpHa$PaJQSaQq z`wbc~?$u^HvHdd}Ca;(&p*vB(=+w{^kpPqj5P31r*?4&l-J zVxTnP5N_nIDJ^cJdp#<-YDj)Y8(x^SI5HV@@c}fKUjUcAEi-e_(NKsK(hGot!O}fI zCxVUbXeew7G}N|w8daWOue3fiH8WT@g_B3L5m9RJGPTYEc%vwd%D8>?<^ zVv6WXQkiRJr>O6uCJsC#Boq>WeQ4zZNPTzy>UNCz!!=X{643m-JUW_}U1D|eUy^5c zCkdM=zyc5ec!gkpjI1LV{`p(gJowfG-f|5!H8~9Q+!R|6wlME;YlzaCk-m468RizG z>LeorB@W2outD|E>;Ie;M_^Vr`~KWNN7qivFsC@yGZ4ApnzFziIicie_`>kfCTPjk zJ`UYKrwpK4-3pNOYoehOTRu3iv`SgVsn4*~VZ0mNRi8(&w%p7l$e$Q6`{C)BYrfTq zf3`dGGv5z>_uAgW!&QpC>O|vqBq|y8ELpT|*W*=_lksgY61@4LqF!NH8awl0uT1jPORLg0}6 zuBB;}#>l{+{P(mp7`nD$B6`4z9n=P14ja{<9tMH#$DYH#`6FM1CVWOU9`iKm9>N6u zJ@Hl7*(oWLE`h!Op^Lmb(AK{%4PYMOpbdh~AK+M^Xe9D^>aNTw*+yE^5^7F{#KiB-b>_nQ?AgL$izMs(^MAw6*6pP zhdB)VL|qdLD{$zq+a7J1R?5YfL#)UJi~5;e-A$g4$=*3N2kr7pnXW7a^ zBk`5p7T;@o{Ap+O(bW{x+Xfp0){-xZ{ed1fISe$qV{O}8wMJS#M!a>UlFsx#SvLyu z32ruLE%{F9_L;egXYvgqufoX{;iR-YO?KrLL$CF5_u1`sotvzFyt~mQdiCZNZ;hq_I8nb^E`LcW~aA}&-Ln71}^ey)$x*{i(rnLL-3*mev zoAm4R7abvhn3$P?V-n25N#1j|c%;1aDk3YHff3Wot_pSNV7m=j z<(0NY_Zp2#L0^BrdYPG_lvH_XsYLUDZrOTInjDp6^c{o4;ln4xrwg|{T2ml@c}B#6 z1R<{XKk>LhNaym>@2*U2YopM3bq3HE(ez35)YL=6!^td$Q^n1TR#to4>=qg&7EcaX zpT@+h4{RRC#o=gENcZ+^+QUJgBnNb!i=0kJMSuG|VLf}RqN}@T-E>WegRcN=ISeg% zoaP=-L{6UcGqSTgr79$OJ_fqJT0lRqbLwor;{h~4CXb~Rzw>+(irz(xxd7r*9Dv2tFt{$yUTl-IDM04szMGd9LlZ3@@Me$ zLnOS)Yd_*=go)?s$mD#NmOoZ)hldoj8=Y@2mF2qXoB(_jR7=gwwtyYdJe^iQPxf@1Zc&`_`o$96@^e%DcY~ms zsKZhPZ{&s#HaiF#(H4uN^*_Qp86+3U$Z(T#uZ9XOEyn?ca0UTSNLAVn@M z{S46@K0J-4b>iWRPrT+xkJg%X^;cIqKmn+qXclA}mL80EOw_yW^Y3Z#6n{WlNN>+} z2=ySXK(d4uIy0W3_FKK8@3LAL!CkGdbA2y-93%Ycs#PdcdJrUI9Mvaj zsw=7;uFd@>5FLlSa=(?>^;@5hALB8hLb+WCd)HO+3zCOJ`lLxou+Uj!q|jbQiMQQ# zm5r%Q5ywt#;8KEdw39Cn*CVKwB2>bx_5n@ZDUobB&ck^gSF`S_oDzC9)~E#6xAHQF z%ZSq`1<&5eN092C*1^B}K7{4f%~R&SHvK&>e~Q{s(_v?uAd5}bJm(8}C$Ien;=(;1 zSN`FH2M{2wAR{8fcqM@>>cvIp;`_D!SUi0E7QK?gqoZ3+`-Q__h>(uHc~(Kek=k`D z+#{rt;_?Sxg7Bxivt=~w>{WoxMn`uNojg8Y>*5phre$){S2flI`Vt;qQc-5TYrMVj z7(zxyW*n5)WTQ`Q(tZC1pdXKOB8hmbwcoyNGb&3>Bf+JMal1P0E%XePPP(Ln04?2e zzEW*L5(5*XP_qFEN)ru>V&IvVN$%8PsMgp6z^MJX8cpy}RJ>WZoh6^AvoOW{jRD>a zRDMBW*5Ryg_xP9v>gegHw!HvQs*{?stbkK$8gqh|L^qY$xw-hL0ZlJNoJ(sJ?^j}dwYl38KL=z+`PQ?{zUBSOJ(p`YHE}B?;;IIl75d+qQ@v6 zzxc!zmsdv*t*jaWm79;;!}T-R$M(nXF5tjuhCm^wr`v@%yuYhWUQ;kAo8IGON!L)= zMNm-CRDAGfoeJhsD%3nO^G>wCIBLJV)FwYChs$$opf0@{NHXD2DCya^w|p@Jeh(H{ z80r~eGw3}4cc7S?)5&!}SS3z*k~@()yh>`tbL!XUm#dL#csUk0dEP%?(!E6RxHzBj zgwLbyeuP0Ua!SCFP`t2ifkX51MP`2aQwUfN>!tmiuy|`P#4xCnmhy7RJE7M|lhgAx zGpX;-oT9vffKcb7l^@6uSidVu%ulnMjY?#QW{<8r6NAfFa=QZIP_jx&N^(m{*tmix zKILTR8|%;>++?neL0`TeoJtQw9+m=Q)}Og~G_QKX+z4z=TAq+JSN&OAjeBRyxP1j4 zdBv;9P?EP6Pv*O_adR@BUS>q9ay%~n0l9zm>3$BSB1%Vcy6@WtN9KAzP5at5Yr3D2oWUc|CAs6HPc_j>;bBz0n;OBBEPnhPrJei^Ap>D6*)UqlwJw-sYWmYJO!h-osCQ!ilx^p9r>%ki%h3xmx&CvC6X$!3(vOAC6V4}#d zp3kRLahA;XhSDRVOnnR4i11Z5HpuI9V@5t99;D$G=CnKeF2>eS+GpwYkA;y3Cx=jT z{HHfbDI+_aTGG+kcAi&Tu7U(f9ltsnZc8s5CO1ghZDRy*0hH_S|O_6q$id%%|n-^d#gI zWMq^M1!yIfuiK@1dpZo5AS7Z&vvYs>f)6Q$g~08V%OB5h=u6D3_tRC*=2`L(XdU8u|`88Ba7nc3=Y}x>QfT^YGJ- z?qPqKz0V>>4@i-mF`t5)S=v$)0_iQR!U}qNnu2^PeQIj+->d?UX{dW>tbVSvy><+> z;<|4S8cNj31Ruc}1v7zLqkv<>J?@Xge8zq^4=FKCnL=DlCyO8g_U(aT1!SI^dO6Kl0-sl=hN@I zm4K!T8gzg1ubCKMx(2QF>qc@%%~5acEWj9ATf+f0sRsp*TBV+*=6w;u1~(fDB$uOP zXo#I-@exfRd)Z`r1Jm24MLj7j{P9Ry##N)^baTg7Arc_kXTplf-HL;r)60@>4ZC1I zmCII29jZyKFZcEHR)FM8DwrkswPdE5Q`7UO^7heEm*O8E(--VuYRg>9Ym8*VA(fQP zPYiU7f1i6^ot}aY8LS%kHWjC)N;$t^6yXQoLL}{o-RO9W+hC-6vl6AyY%30nTKRhg z1cBR4I5hBiWT($(cV@fWt`t&A*UL+S_;-gCr&ttiV4Xj{SkfG#=@!=bqF(##`Ta#^ z<>77g)W|a)4b<>fl}TX|^6bJX!OpsMUsnu#omN|~mhA_ED}+Y7hf_T8M(W(o3a0yh zt)<(!TvS&E)CDj19a0g}E948844VY;2;!gO;laL1ZoKgkyh1EmSlkp(8+`bn!#-1F zZ+*5QTExw>1?}g$FM49dg?56!5tOP7%K5Xy-S7~~&UBsLDm5_HblQ&ysC@V;ng9=d z@YfMZetCL&-m|pRBz<*uYc9Lmv=&|vk{1md2jDZ!yFR2A#~4%KTa7nA(`3+|t06sI_3+QKK*-*|2gxa` z4R*CcLd?)0wH(NO=@`I4Kj?{14u~yG4BjdzHJeK=us%Jg&9>XxR*xCT)~cqg^NNex zsx%gQ`xXZx5%Aeboaw~s@_GTXg0GJU2O-N&Ui6TI8yeZ}VP2nXqRR+_rd6T~GWmU< z6FPvC@4!M}!On~qb>bFfU()gQtMBRW%`GT+K_uCM)wScBC$z`J$@tBFx!zCK+7O!+ zAqptXL3%>Q-s!Q+3E56B;vzf&BOCwYlz!YI)~`*`F$a(EUNob_iIK78iF_y0IJk(A zfQ8s3kB=B*1u(Ho@7#S=+%-d*(O{S|^xX!^%xL1LsIkB`+^OAqdq`rdn`Nkt)WFMD z!00OgYCe|RsLD8=xgVV^=~|uNn52BHuB?PR7Z}AL(CQ7irZ1%5MFK50IDt z+plpE3a@g@-b8l=`4hj4zlkO4D$VqMtaO2L{Q^w2^ zAphXt7pEF)Y04lh6U9tiiDqwXv)n(V#)oHecw-dM@3 zt=hFK%#)9dC>>GnsS}sRg8g66LfwxjFg9qmWl%W=aRP5(^T{_f#_3m8>p&k_Yb8dD}2=1bON*bHB2W zGBBoj#mThGY^g`w@yg|VOL2fVY^o=#s;D=A8kk^OV^>Q58ALTO&o@>PQQ0XMJs(|M zKm%niN+epa75VuIawcDkUVVW7EkO#hFuYRmD#9b2TbtYbh>;NN8(sb%e&d6mKgVoP zLBQ#TiH&h|XvpJYi4$;pkzJOTmLxJ10`S7PIf=rZT`nThGwVs`tV76RhR@SL+!Vh? zFo1olq$0boNS<#O+E^Kig08#SR*}~-%h21?t3IzGs+3~7z3~8w?|nr>6V>%6mhd-c z=Yz8I@hk)o7Qra7WVj8sPgOk>n0h(YwtP1jG>G@y?0nmX#G{ zQ;}$qD9Q&o4-}YhX*DY;MPv=aJioR4^qwFfu6C_`{x%SCS+9k7w(fMF&vUl=Ltw8S z|8Vet!p!{@;|4COa2N z*x4&jaFRqO2Bt6}+u?aOw)!Hqwc_oerH@c^OQ*I-B`mCzGS)g;B;2$sTt{U}r&91v z^ja1sDO_-V!>6W!);>{ZaF;bT08}&BPk;B_REBxZ!txNhzOKbg%Z=BIX!MK*yIC~V z`5!+@PI$)HPhMpWZ)&&EXJ^*pLrO}@>MmI!A?}D!XlP%(Rrl-5OE@8SLwkFyJKI#o z=E1?i?rs9;qgFlS1+q?xR7z)7M3s)G5Zc@(GThQ?Mx?_GXPaJ$zjk1e_SrYR4M}jl zBA+rYY^}YzHm$NsN~YFLv^#d;As1$AIoZDpZ;ajBBWe-E(a@NMo-@ACGI;*@e!_V% zf*vD94iY1ff9T`FqC2tdhpCKj)J5k9CXk?^;RxtC53BfF8JYe8AxTkG$hM5>+|3zl z#SqgHspU+9+z>l6mFYgtbPyT?X%m3PfrA>ZP|Z!5d>jp%a!Wz+|0&c%w|kyEq`tW@ z)HylM%z%Spo1JI;5E6b=Q(p0zN*iL)m$qzCdv;gbG*soGjNH3cA#f%P?R6rbCX z_?Ob)1FsF-=<{g!Sq=z7B0p(d77|TOe@R84<|dJps8R()35vpX^PgB-l5@@QxX+%_ z=$`6-EPUWq5)$25gLr5AOyA8e3yjB6(@E>-q=xH&tfS)M0UUg!IS5je4{zFIVqJYU zSkNfiKX*+tycsO7-&Wb!9vFy_`k=38W~ux9qF&Mk9s^_f zhL1pI$CBdWqqQl zJq*SxH+QOKAIE3o)?PwLvy5zSuJI8B8njM()fe&$EZ+s9j&VNO{*;y+ea74MDXd6G zW_G3ynUHYca!nA{FE1zOh#rrS;j77%#Y+zB1l1E{y}TW zaIwx7q`;Nphyq@cp#5|&I%X%X*T}tmP&fjt7aL_?o)Qac$W++J`L=^>I3o*V`yoHL z{u`uW@%{8z25qo!`on|V!qf`WGihi<-wrcAV>2)F*?$a)A=M=l-Qf$#GUYQ|cxFlM zd|0JAWO;;r)~NU(^X36Weqp*O+jmeDi*ZDI{G$n4U^Q7gUlj$f%dyW4&L>n9p6;s9 zUf7|V-*?{JV3#=D`NYE&g>gg(y}pzsXu9?fC_s(Md@h-py&ZS*^mcYf+nPFOTRi4w zUFEFUjM`i1(r8ncyC?ge-E|GBSN`r_a7CWD~n+>y^#73BfjlqR7wBVRFjK=HxN` z#*2$LZbFnf4t$m@tjv+&=r$#_F3az1Z84yp1dv(2_MMvIGGGGj|IWR&$UboJP;*(9 zY)gFAWUG*<4F_RrmR(1@4qH4jG&!k;x4CJx2POt~c6L6#qN5`k5iKq?za;%4ZFtSs zo+uE7R6@P``kob064*)G+S=mB#qqeFzB?Q4SFxT!dHj@mQ43_afM8UZB)rmUda~#a z;!5PtV!k}yt-2|iO=i`$m3b;|=BRA@LMnPX8aj&0N|m=?RajArBhd)dG?XA{w%!fwo|$7M00gRw+U)km zPi>Op!JBsyB!dS7=!bS?T1_7wLiq2vT9mJMdwHSy5R~2?F&Ib`H|)hv$_;`zIVvSJ zH8oIPaw;O4mrD9+L4=hZStgqz2P-N&$j$;743W`UliUv5lavtD)h)54*K%$=D>MH$( zC-@x0EtB&tYONY|A;=0FV?ThUM4kR=v0A((59}wWukkpTryE~CO%_qpaMaoLRU7)@ z1)8Pd?5&~l9Ip)ZX{CJ&lu}(cXxe>>1jlIgoSORMa0{Uo7*#GVHDC<#xSh-$t`pa< za#vZ;Ivo&GM{(JWCB&A^h4oOq1RhA^A@NmK*6vLJT{~TKWz7^@Z?r3W-R+08r#*nx zaKjvtnCPH$H&=6nGch@t%zKsMYI7QFijKZmPrb7$*>q<DWeVLgc=Y z<&ejpJ;lPy+c!4{18Frn_o?45{bB|$l4Y%bQ(tf2h{Dr;J9DhAsX2Qd5c%Pz_Uh6B zSIc`i2@f5(*iyf%(c2u`^~}l6y}i6jZpL-p81Qxn_vM|^lkJ4GCWmecb8`alZjAcv zA$Z&Jl)hQQgu36-NlGqPQ#Fj3#sTM! z7~;y(g5X$bg^k5~$=^>TrVBDvTY5kd4v%~#qLY$RGWjuMJH=Gu0=CX$IVf4fVS6gp zYjN4sUgXYe@S6U!^~WUV^Wzp0;XiA&p>OQf9eYPZKUut$56dwv)nRb$)?t0{#74AA zYLfN}TjkSMPKpg%BL)BcFD|Y|eOS~D^<A!$(%Pm?4(+ zSflnYmELJe3yXWWI@&rbtfY-p(Wgf|bJRS!&^$ukcjY*Hs;{%!r!*E39!}RI9{R!c z)yj{i`dDnTPLHi=TsM`|BZA82o|3GO5h9unF7Gse-Ki&I6sx*v=SIR#RX9j3kti6e znwax^6cn5Tb+gQj3`V}~ z@&Se+;W1A=*T)Lt*$bb@9>PKBi@N&+NLn5y49*wV7x#Vsx<1~{E}I6=Ttk6-)!szo z>SYBDsYr;U-F9Y1fppp+H4P1z*z{7u+*uI{E~W&$7u zIzEQ&`GALmLx^mAu?)v$206JRWf&-C#x;3r^SFZ%$qNwEo008zi5z{4AJUaiPIu$O z!_gHpJbPkgG`J2A@YgBKNM0TR|5kX(Dh!xf3=hoB}TorxznLt65 zqsEnAv4;c(d)3fbW~a=~mc8uWm+ZMB742J1d44Uu5B35UWL3W2EeiH`V;MG)>q8qz zcQD?*k6c6#0aZq2wWP~eFIdiN4$cFn;u8xkeF(q7_6^b@tOhusV1-Gp`OevEBh!dxK)9SoS z@DdA~^+pYiGkxn_TXYyo-Va233n|KND?DMUu!ZqT2yQJpa;`p!#X8t6f)2(p=0Vc- zH9tCd8){O~unU_}eCRPD7Zp2Ymd~HvH-ExR{k1C!A z^XCO%;Yi8|NqeA(4z+wt);|R&0sh|tA>6#Ew{Nu;k)S7~lkeK*mctTyTzyH6-FPh>0 zi7Nl`%z%n8Sb}K?_$c1DmH!55aIhZ{l}WhRKPIQee0`Z{J#yex!%W%P z)pba}Vq0U}r_FQF@2RS%8B!Xt)TkNH!a4#yZ~FfJv?VGkN?BD;`?T}^g1Apogb-Zp zezg`O8^V2$+m?m+VNLX}@A+lob!`pUwL@OP!NJDtDj%L+2KOYed0bB{l40bWjF#FR z8qr^;R8|(!5WAx~8GEbXL(jv-w1^5dw+i4q=^rvHlBR+LV>t_Jk5n*8>YQGH`y?eF z_}-eSb`x<(JZ>so?{i^S@plG1Z zuRk;7E=_?-wKDPg@OG6P3EVvuWE^CvB_-j`-;g~pLmwTkcJ_64cXaJ7iQZjj5ZrtY zJ|kus^BkDK<+naK`=}CA)V^+eND4mMGCi$eG_Q_*zlmPU*TBF?|Md6fgJd0a0qs>D zdArN6)=!aY?VHBN+H9Aq(N*goGmxQgJ`u20Zu(8pjxj4Z2`1jcmj>p`r z>&Zb$MF}ttGh1L;!NqEL5>oAa@|K@}&{#z;F&6j!Ca(C!p5}+~dw!k|DWN8KQuMqh z4DFIDywjkhs<*eh$@XS%d!cyd`T@4#s;=N=7%^tYmHm0Kce||C!g~(Y4%Ive+-~1p z-uGB~<{L9%LkpS`ml=6gmzPfLjBM$h-MGKC2;s)a=HJW@;EM_?XA2iNfL>K_ex!gNcNm2!i;Zj5@_P(Y1C%$kx7GjV`_Sl zAWRb!D9OdiO-(J~EePja&AGi{!nkqV)s=hPlmC4@W!!oem$RQ4;+UF7A{d$#FW@b5oHZP*0C{+98-IiY`kl zh|8r8>Xq?`c3mFx5tY@ptBJLi_HX7#aU7Ef?CHwgsy!~L;NKfmzI319lQd5`bJ zzQUJX`}8KkaK3->hMn=H;qB5Ly8A%wZRh-{bZcW1n9eV|UeeGY?~KQv&HHCpwax?+ zjL~lIZ9jnkfYqGs8jlEXc!(WoXWnfyRSm>-8n3GQ7p%!@epj4rrjjdC1ScKzK1T3G zf@pEJJ-68_zTSQ-?Y|?`GuPkT+_Ny(H~n6em!2&~!Y^#7Y-A*(YS0rheI_Z{?Lq@? z7x6j_iUWIehVNmb|A)D^j;gYa)_xyAKthm45J6Hp1qBI7>23j~8wBZCC|x2TAPu6V zfYQC_5b4eZ(%rpao!j@_dw<`l@9gu(8RIzCa13h+>$zjjYhLp=g(#%_jIaWMd54va zOI}F@$DicjxTB~}I5Egd%ZDUnulsg3S5bb=GT0WbRw|BumEG%lQhtpvRg(snYl6Mj z3==ia{Y=z%&c2sv^Vl33uXs2(mCTli=U&pljLXyV>lU4+k zwwO}SI=_7Rvb=3AU;vS> ztS=wiJq^#}zH!|ac%y%A*E?Wq%SI26R>h|PYTNONxj+np?PaDX0lTqFNeV5h z=>OUO8?R+_(s`$_LpJ0Ri5K3J6GBL{-!(4!yi^LUSW@;RGr}CRQQ)Caq;mC8B+hQFm!|b;tX-0 z&HTl{i;VS0#m|nbKKad8@64Qp1}BF%>(~~ufmgqZJ4sjcq{n`H!j?zaO8Tk9@O;q0 z+q09GwQt4JRWO>oXiiSv9-q8L`do5-=6CD=lHhd_O!wUG`F}1fCT@6ER#txYPvF63 zuig_~b*ZIw;CbTxXM2dmO2(fw^W$$jfMeITV1k@oeyq&Vt}L6oRa8RF-4cKq-h;d< ztE@VAXp5otpSfAqA579}HAS$`Xcy7RUh7SiYC~yl3z``SmloHAT5Of& zk;bwhRS1{DK5MfRAeK6wIWMOuIk+hT>%+E#{!d@DN>1?=g81gATm9)xyouUma?jRW z+gjHO-UWg>+v}`E9A;$ZV9^hE?i@Wp7Gq{AAm-=5i7`V2_lWHkolOf*s;&fGAH$6w z*Xl3$1s;!ip^SK}s9<)_t&HoxW>EaWbU8aU!8E}0{HzpgGLBl<+#WoErJAN8f%f;$ zgW8eF8w$B|vA_H=Esp#?@pI(0AXjYlY=3S??TxvGMKOmG6a4Q{@b^K1Jemu*+=KP20_pf35YgFQXHTiAvm&ugW?*6U& z_plz0y8W0siMQlP5xB13s@_$6Aok*0fe(Fl8n=KXbzg?qJGkgg(b&8}Fqg47*5s@G zi$iCRslyp6Ost$E4^gXC#@z)gaZ*Zvg+ zD=*HGWJ8&wUU}36Yt}b!G;AL`pi}+o>FIM)G$oGLh)`6hKkwo0WHv>Z{0-*%Lhft~ zpqgASw?kECkuje_u)H}N$&*v5@Q%wiYwGp3mY2;ZZfZQ$P=p7;Pww}JAgU0xDCwx} z>fY~8EO(0;jCS4yCGoV5hMRpgTpd=RY+_&tsv{-Hd(QSeJXr}-JkzmRDE=Geb5pQz z@lV{~U;mKp8MDe3OZsee)wvX-zk2b7%h?Ddf7VuZm1eym2}Q7-tfPS5m0fYw(_+D?-#2xJ*(uryU(tA}RX*G~UJ}xK=v> zBlvW_-!QR@P(Wa0-i?^^>}Y$K(2t)U+z6wq55RE5%MiK`PcvzJbGDqU7#%(9Buhy1 z_*qty)HF z%R{0KF;%;>h@L*PxJl$;{z~S-=Iv zPDfVC3Gwm8MP|h_HB5{g!--D&Xm7BSkX+-5d3bj>Bg@~+5rQcBoc8JXO~bcuKQU?h z#IoyhSCy^5W|X*Fdyb}l4Evte@Z}4!3Wl7bl;3C=G=E#m%d0S_v2ROg@!x}z2<7(n z$0BV?gX_TJcG@#seHAIAzMi3eu+ZZR?)D0meH|hG=lbOyZhbm(p8Cz?R z30j^!XPm!9mD?EtbGY`pC^Y$MtbQBvpnb-K)%{6V%MoeTH3d66;9~Nhf1sBg*sl5V z=gOuZME1=CR=DY{lY;KFet&z&mIaZ zdB6Orf~KC32(~u``#s({-1+N^6yZV>ZXeCI=i{`6}rKzZ$1uspJo>v zdatCI$TP+zh1w158taf-f(z)aureT=s2n|PrBRLwihaRoYAYwHukR&mDHz)J`lnuk z_86l~{xFkxkHoBlDd~j)PP2^$kpQwoWZhzXh z#SE4KG(!C;uLi8Z7_74J!w_sR@k|fe%g`MSzc4V3T#;ett+oD3NwXD zj22fnyQy!=?PfY@fqti85CZP(KNdXuZkafEF&m3(ILafUJv}{t?)H|dfv?clv@FPk z_`yBbGRpm94GSoh7>ZuoLlwX{hT#YRm>mRI1QU@|&ElB0uGXa4UKDk}LJbXNv^ub^VnP)aDSiI;Ge07eZ-zUbTy#v{+T#Z4 z|8!AkCIt*a;ORP+O`V``Nq4j#Jz#eD&hczE9aECu$EVK5&SqcLhc2OhQZAFrK<=By zontpcCmw6X({PqynpB|*i%VMlS{1)=&?ym<(R5#wJ^^MriUrgN8nL01~`K!OcO4wv$X~T<-BJBV!eQ~TO z8@#@&{P&QTYbHp-nqLYL1}U3#D~UMnM@>9^H<8L=w;8GPIFN~CJwgaYt2ifrKD@L| zNUe7w4G2Z(YcQjuDq-@}Id%B8H^E!u{e0pLUsZV;F9{jDs_02qbjyB9ywymM zpb);uj}MBlmZtXmU7gy<(zWc@>M+L{_8yNo^A~zQOh?%ylD^Yo+<(1aI{;N(hRPsS zlPy-rE+o--C59syaZxZ}Pjh*b|%zjxgi&NxLcXmT9Jtfe9f*(? z?s-v#6)r4OnS?ZfYmpnuC~`~YBTMC2#w)5#TY>4&)o7ADuGw1D*P5D7o{s@ha z5i@!b$3vd#2hMk1lC|IZH7-}Xfgmy<@(ejXI=EXqQuXma)*SygkR)AHSK1cReqm?H z`+8loS`8|R9BHZ=MNsA!f1s@`C^E6L*RPzpHQb9qqZ8sr)mzwUYK(kq;x^fHY@RTi z9-Df1hVUq`d6G&(e1y%xP=4Cgf%GUSDhA#B*%TH)d;z$jfb zAFhdQ|Jnbme?|Q6ys=|niJ;hnOm*WhJZ_XPF^Ci=NO5t6-a5m3tq6`xN>Xiwo}>%b zePQ^o`y{)nyo_fF&V5(Aj=Kls-5HHRug{M;*m^SX|gWx~uSD`J({hqx9uoeSuotUNT|JCxK%&uj| z)v9sw22}1>3*TycBNaz~x$PoY<^wd0t4FMSAkWa@NbVI6IFnJkoe}op8GmlHhkM1+ zwU8j!o6Ck3OkcE0tkXSF&j2{-S+{tIqI~Ss+=OP0iN;`&+KorJ8_E!9BT6|vBGWV9 zFAjYEjp&TcGO!yLIRyj;uRxCGz~6s z$eJnz9wU7&9A8%d0kPXi-Un(C+ehlA(nCbQm7}ecGxKwEzkqG&G){WcYcMf@w}+Vl zE80mNzw)f*r+mwoY!CDq)1C&VNJ~~WzF^gmUxjQpC0>qP1VC9dJxka^#+x)K@8A4& z2}gIv&=B~Nb%97Yc)asYJC5pU)!87<-Jy!REXDPoC*$HY36V#%s5r97wF6y51^bmu z2NP12lmP=fHH8vHI``JMO2Bu@9vO3-L~-@cBm2V?B~9;eZ8it0Jrvw&8*6iYgAy?+ zc|ELcv;c9hMM zg}JoA^5l#G$fe0sm^H+Gk4Fn!b@wBVg6~i|O$?o#zNHrOrbIk0xm*)>|CEq`LSCKH zq{0&8eZ+5X0nq_ux^bGrU*ozLK8885z}l|%LLT>e!q!$O`i=LcwB1gkMF=~ialVe& zD9`oT6Kr2-o)@{e9EIbIKns6XQm?O$7mQ-BPBw;0RS64{^pZr6!FfQ#YyUC1fXK-+ zr&&Z0HG*@{1xgE7$A?P^li;#LGVF@K4iAr|fS$lMWSaxTz=LX_Q-psm2Zs< zmn%gfVl<8qog1AG>Gnq2zka%iM0JU0{KU)no14x5lbcP*nyBX0?}fez9WcIi6na2J zPLet}IoXX$yL&AtCdlxZ7ERJ6DNeQ9LEvInpH%NpO2zK)^tRJ^?*)44*X$i(7@ z-nrH6SKt=lby}j?cF3}M&?^OVa`aJX_ouVz9SNqy(Nx!Qc~i!M`&v%|MV{#}9~5|N zNr8!#*H=#Vwi{6jm939EvJ=6Xo|l?RvWI)L-xFzaGl>HgC308r@DtU{WL!DH_hk-Y zTd1(sTKA4R-3av}I{9lot8q5y1J8NGtx#zWlypw$DUEQ8IL~-aYGgOd!$mpvm|PaK z;|}YIp^2gKmzAauVX13CwPBuCFwWfA4Tsn*F0#SsT7Ej1wu8lSc0TiXvWBET8 zsQsTKaa!16XA;98N5&z+;v8_^9{|7SD}~L3kBB9bOz#2{EHb43Q>kiHyFDNKDUZXDktJx=lp|h!~ zW)cEqw`a?8QslVeE{(n${rbi3f8|O6;|>~PKyUB$*HW6zoH+$7k;o+CpR_N;F5f`5Bj<0{&3OB&*?6&%i8gA&NzKh`q(AeWuRC8 zp(!rz%eyA7pN#8n>xt=Cn8N1`IFWDND>QX=F(J@JKy)I=$f3ceUsPl^=ePfVw#;BzTe)1x1H+|0y68Fvj>lzsq#s2gB4=q6N>Y{D=4Ydf+7}>$iu%=5DPu}bOs0)fQ2zH432m3_lft;S}n`3eJ+Qs58HRv5F<&R>2tQW zTi)m1Z0xMWPF1Lk1~p@KoVYtw&hBL+aPY1NEJYjvX&Au`AD~~ zFY4Y~VThEJlt#oWWA2!Md!8)U6^K$m1Obja<=$?4Yir<=D|0D}e(C7$zI$SL2Aesi zK()&aHmJ7buyB``xvmrdF^DZ#Y4zl}qLRW>*ajAiNis?FuTJmzuTGy(K+?6}+5t5K zwa#URN81pzsFpquYxk{sO*CZ1{gUAU3(~iaNb@l9Q{s+@XG2}C&UXE0PgM$A+d@zP z1MPmjEAzEypW%ehjs2e+(ew_@gW1lBHTo!z)i}>7SW?Q(xy?sVq(%>X$jv=qTJCy; z`#{X=r(2gl1H<*WCi^LdykQ2{bSe*5lFv!!+=Npr9&Y3d31BG!R8Y{$SyAy!Q0xG? zl4|I_y_6~T+&H4y;G-K z)u_`lP@fufV&)*P=k><1D*<0q{tl!Nc~sZmLO(=0S~ognX1jt8J-tUWhSSkKTk9zI^VzVB7Be0+ZMKPlEmpBz8zCqefV6a!0Hi zF5azi7=(jzE4bSwZ-=vp@_t`xdO0yj3~3OzqOpj|-hSvWihM@Ve|_rk(sZzBFsOYQ z*-8(3e#IJ*Sf^Wl%=fu znrILR6mTOIekk3w?EnQQlhB~63r|mUm9+HSoTm5%HB4g%Pj>_m3w(!^K-3nf6%tQIzUqZr=WVL-VJ=kB=gsLh~ke;b$1l z4O9G*Qd0Zvf1N^2Dhyr96GM+5v2s0g+ukcl>nAeH+t{RDSv5C)c0I7B-D;Uwg%KKs z7QoMPN z->K>Sd-T-4jK_YP0QJ${3IOOrEv1NCaIwRIY}S#c4vl|TUi$BrsX(P{P^t(UO!{YU z?z;(nAt}ZTzNpP^82}AUzYQD6BQYP^^PIb^r#z`UNjA4%0-X=G)(fAO^ExmQ-hOMD z-a2>Rj|V2!TLE8#K1Uq#{T7qFom~pyb z`2v;>ZeETCZ-4i)4{>w3)Lsf7{4-?%lk zQ`<^lgT4a#DINPwZwJgcXQqxygSJS&oadmprSpU%JJvBA; zx3V5unB&RbND@r`-+gP?Ll7)U^VGHime~ZRv*3qFTFA)w`p%}$2st6@SQ{e6{I0H> zZ15nZ6rQU5AJ&`IBlTzNAXMxrG?S5$dGPFq(CLohM~#5iDvGkIs{Zi!yo67mOyIbe zj*HK@l6q0v_*hHrJ^Y~k2s8m`GDnvd6fh^o+!tN#(zb)>oY(1;@AR>b$#M=0I`E*BO zYXcd^wZ5?(rj`37b_l}SkrE!TyX!cpeJH4+382?bDWdpF$(5gbv?9WByj<#l8?6OOvP@GMrAw`t~-IPJB^1RjyH6_r%|pcCR16XxYISU3m|Rm%NsT!9N2 z!#zn4FIhon%<9nJDMRlHw2ZW4Gm~g;R%O&*-^jUsEy)(OKQ^a|Ha5_Bd(sLZYq-2+ zgiwk>9l#n8>rK`p2IsFs4!NJYJJ;`XVG+>SS-u|me#y#m^k)Mmo!jzz*nQz6FXX)1 zev`7Ppi3xjrpbIYdG@dJP1~YEHoPO(IFflwi`=TW)=+?om9N0-KQw+ma%UNS z7|_Cbb~-Mu68O&mT?9CA_4Q*D2C0w*VUMPs06aO&%%A7!U) z7#R4Wo=U50{3i52vY*1W_CI^bPw98_^lMfgKto@=>ab3|L7#>k+E)YYD8_u1!=LlAYrXiYvR zJo<4MAiwnt%NGxNyyL577~cL!eWLummUHDsmmvIvn^GLzi$B zs2YCx;aR~nMc_;Dm;ohxq60nE>^rF^dFf|7*7KDn6YWE_U7z`9th5tX6+ioF`oNtM zAJA)B?@j1xRG$%4xYM5hnVwI?)!*LsxUWMK*}D?=f|1=W)!m`Sd@91JQyB`7Bc#1A z>T+)6Qr6JWP@0K2Q%v00qP9oboqY;l+Dxg=Da_B`p7I!+ot1oLYr)RG1r8fdqlWd1 z^1SyGTc=L+STr>0>Gfw|7wqqK%ydlvdX)7-7?~oxw6Ku4Hv8SXp?)LS-!3r&8qgHs zakT%E$3w^5JZKrmPWR#3%1Vjh5u66Lb8vi^A?UXn+t#;Ps#|(3Z-U4I1MOBKzBP=a z`(IxXrd7WpDvbwoa~Ip?HL>mxd=~f60emS9@dGgB_l)Mr*ZllZt+)^@tPok*PH^I2 z2l#mGaTwiTr1w*jKan&KZ29=GE$l1Ukp=vC$qS1cl@>E9US1bxx-8n204;ZNdM55( z(UB*_!m^fLk7A};gCvZM{F<9v=6sHYIo?hrF~0s2o0Mcy?kI&q!@#UNotd1zLnG+h zQMQMNC;rTpC;4|d1s9$U44g7WMLu3vN@>sB4m^%DH8mJf4?X(HupS1fC4p9^v^n3u z4Un}pUPiu-I#?r}&keg4au8YHim{=mQyad17N0^sq_xu?C(Bv^k{dNu>da*AY=d{o zoMf+1OiIZ3KCkA=3IdM)h_#4~wFo?)%D*SuA?-UZaIlAlJ7p1pci7Vo+ zRVDRh+}69p--e)-DxgnU8gM632QTKPxM5 zMopKZ!kcr*AOQHV#UAQEJ*@c_I_B0F(Cg>#pt{}`uWf|$IXgaUm=uP=X3b$z%`zN^OZyRR=jzqj7=W|kI? zYP~81oP!3RBwr+A1aRb*SX9XK%Z<lUi`TL6a4^^M_x1c4&MFU z)zv2dwobC%#IRZSmBO)$3okpJ;;8U&kdWh03frHid6D8;Y#h?&s$|eDW)tOn3^Z9j--$RCj7D!1LfJl!IM!1&O}?q*Gh=fA;ylYfXu_!CigUE12(itxX9 z$|3MIH+THkwD)cUrV?|!9`K=`o)#N2$VKlCl_!C^)&8T~i-RTCJScGJl=C1nZ$?tMAt zcvn}xQj^QQoIc`Wb6N=Z(Dl@QW>5>K7lNUj=ubt~QG zc2&$NFfG9{stCGBiW9@#fI^x*9-J{06Qo#wsc z&2DH@^8{ovtVyLo4pXum_OHtMcV~-V$Zxow(~Wcw9;Wgl?_hY91}=Tb{E;l&;of=N z+MWL;>?~VTDHMD2>I^I32j>i%h6a<;he@@qMV~ES=GXJNi!A5C~1b_oPt8ZZ69%6+CAHUJk z7$MV}syy=q+_UrDAx*<3z%=fxch*ZBkGKnkPE%sP2S;0 zU>RAA>HOR(G&4awsq`{X4CatO6&bzpgOM;euk{-`@2t+z`Xf!iEp~EzY&d0pIO>F! zm7i~YRn>=13#dsi>%H$mvM~?XK^OeIAG81x32egc-rv7p0jCccS&w}9M2WGn0Ia0@ z!?LOUuyG)Ih@PGST5~oSJi9xUkeX^}Vv?Ddc-cF!QFkM+&yLMe~UWufDnmfl;3=QR4gTi&Yu}}B>NRS4yva3l5K`_M>3aAZU8Ma`D z7C^KuRBB>p;{oV70OE@4e_aSLvY@kLc4`D*>=M?m#5tU6-m2C<55xeIyz*4jkM~=y z%gXo!Q^{z^YOPylXR4hJ$7_8z(4mh#%PnZ{Ip6nlm0*R0K+Y?N9sS(PPI&|7Jd`iF zJTA&QJoaY6yO$Ic_&cL^Dk?Dw?qZ|_Dd;I~8Q zG>tk<&kf~hA<*J*{KH^z!~WzHV-{PX z;sMsL!DUy^-I6M_t)TGxZLjNNhqsR)YugRJWCf%haW4>FitTAIo1}FI)bwS@pde~N z(9(%U$GW(an#BSz)%OWPwRLr0ex^MW`~v`g^w{hbLa*cQ`w6DHtsoD6M*+8KQ%M!G z@SnbTY#~TOUVp5!AM@WRV&`uZp+mv~03^5$xApxic0c5PEaTGrcl zn0mTBZz?_%e#eItK(hdX^H#uU(Zq-oqUAlNJVTjR@8Abd~aPu zW4Ve*wO=bih&ue<$+o1nJ{t7o{1!R_AC1|)3HnHR6VL`q6Lhfw()6+BFBtE|MQzuw z=RwU{LpmdZWb=yaZKzG#l;DLZ!bU}8cD57ikFtq{WJpav#ba-oyVXSdb2{nGd6e`N zOYC+-2%3b!Y|;0xmIL**HoQHtG0#VD;Q-Q)RHN^K?@1x;uioB&BF*E^PW~4TBj1XI zN?z7iz8X3M_&qw(U3h9J3k13^gTi!G=z;StPAOjZ`EzMGdnpEnn-9O=3e!NKOLJ!a zthO;V?Db$4^cQj|`UaWbEZ6X13~bG?+0)@*S9BX6}Nb~-39 z+8a3Q4vx6t05pLRa>!N7$H6%;ys_sTNBv(x^5*y^Mn+I>OIYu`?0~SKdUKWCBPzxv z$ouZ|z^kCK5CdHWq4BLGARAdLsa4DP4m;h*#G zk{9VSHjgfQdWy$M@kh{Q}nH3bCM8 zbeR`jDI+7`<`&~-x4izgG+Mkz(UJdudIX$yJ?<9jZmhgz}|qeKv2yks!OI$*~n zky*0Zt#2hq?uy2ev!DC^UJ?9X0{ldq5{@6)QIW3Ugfx@qCt*YLjJFP>Ec>z}Z0n@c zuV!>7sHgX(xnxhrDwM2oe?cD~pcKyWG!ab|&ZiW5k6xdOOkewWfH05p$)l_!I6OWc zk0{MZPS<0zV#BQS!_~^#$=TlAO0m7Ir7Uwi+ADxcYm{G6Ns_1^9!-a$>~na8y}}iXnk5pn~_l-nom}e zktb$0eki0ItW|c|Mdm|z|Md7!%Brp#8ygL4xP2pr3A9SQKBJ09sxmI$uEh{C3tolpM;-%UH}VNDFKxa z0%S`P%Gb@Q&EV9rvE4$KFjmQRF81@%w*iy+G}ZML^D5P=WP-hRQq-IKhl;H-A>!~) z$r&zdm$f>UyAHaiBD7AQuvFc~uE@RF5}5agX7Ybz_+b?l+E{O_udG>0jkFJ0Cb~l( z(zr^eWqKg+SIZXOyzp7ZE)#5rz+~PGkUl_zBeh3Pm_LG zxT$M5@%YD--j4tV{5o{Dbop2-NW$iMNH>`R9zP_Zu5GxSXA!JVPfl)u5pZ87qhHGl zHS?;lidWRUEQ~ViaRL(^<=6Zs8!U8Ol#6!pnV$+C9|-`GHNa)o8r9m*UVOe*0cIw@ zMdXys(c0K{3>1W!@s}Pi+psk}5`={qGV=!^d5mEQN z;txLc&FhcP5&qXU0#q-|>dbg?^)-*KlRq*P*0_|F%XCN$+N@~CA{Z8joJ6j3#R&G| z;%DYNt87oErBL^&#Y{id)&{?S|6alljaM$1QGB_-p%v7xHs4pc9fx=GEIJuPG0WZB z92)jBX|#J~u0S8VxoLY3S4rDV8Ty!%$6Azsp!!N8TO-_Yn{4G9=d+0f^Z>~sKQY`x zm+1^5CW|p+ztFq-X1zxIPJ_DuZ6TxSrZYFu#l^gUQK>b@L|vV^;YZn*G|%nqq4o1}ncHq} zcIu*Xs&XE&S9)=vYkR&{*Lr*0gaI`V4xt4I!EcNc(gVNYK55M=N<)mE#XLFrZV=0U zx0h8bWLMFVr-X~b$!=RX-*Mc;RT)e%-`1<_$uMb;FLkD*B@ zmZC+^f-b_@dCArA+k{IlwV=N;H&^(f*zOYHT{rrELmka1BjI#o4GoTBrG&&!6&95s z)dWNmEc|=pu;yk5!y878C+3sRQ1=VfnA?O~NXb=g(Q@fK$rSn_#hGT`bwcNX&wJ*@ z^XKo*0c)Yy>~XGg=2O#U>o4k?h$T6*FFC!x6v8tfruTayx4%{RU!}VSgYxDmX04Yy zb^t&Z>o8-{rJ#lByek_v#bDrvZu?*4hoW9dF_g!BK{qx;FKr3P=f;9m@B%l(gdOK? z=_{{4$hRM02ZRmM`LKBK1@pb?{(Qtj_*g*TMaFgiM9G6=KvsUrJ3c+$)B5WM5I>|_ zrYE0bNa)+VQK<^+*X{}b`IUN2)YqH6C|^BaOUld@yuNU&bQiI_b~3K`6d=e9RtUEX z`}_+`zF_pnW9PkKd00HTTbLuWjby)=#pD&_h=@YxqIkimqO7cMN}tI1D)9M4i{;qu z(Lg-0SL!xUvt_tHJ`g#-NS?w=b&5)hh$!hLa^Kopsp>dbXc`9pWr-T;&M6p6b8S~_ zD^6@^qM|9EPPdPacC1LDvdVHDPexDAW*R-t`Wx##l$4Zd?%xLh``OugAbhG7j9%cu z?X0cuX2?i1C{KT&kTo$jJ|1FW3$qJ0SQ1HL1bwRjBLsJtRywu@bxIhr)@?khptviI zXbLY?;TtaLr?>uJ9Ui>)0Hfyu9=5H&l?A9vrkyqz5clP?asfC znO-P%f4$lWBnz5>cQK{~8(z(xheLPo-Q#sS#ec^}$Q;j_fbeHA0Y%5#Zmfm2RA8@3 zi~6wNX$;1!1+HRqK7B*EOlzJ;oU9R%IA5Yqaz9FEk|my*txvv3oItn;lVv=R<;shBNE)kZcZ<^2>75!h^Fd=B{7W2TkOgkHGgEDxV^_i#sK zm_aX2JhP%ld{ur)A^oQ7$?*w*?RkZ>z80ySWMVZ3=-5oyg|eYxUPFWZ*sm2Z%Wu5j zv8Rlz*y&qN5gt8O$JT$~46hmUOr)hf4Qz3tTLiQKP!@WJ1*>JY#{DFo)WzT5U-Q_P zs6*yf0P!s2-t{*rRTC3)QvVGQJoMP&W?MVHMsvW8%aUrxt9!Fk=!`AT@9J8BJQeTx zM=hpUobDrpz*%mVBb7Z?caEIpFts!Tuqe!2Q(0R+P=M)I8X4pN6dzCWxjA6{RAttL zG(pQCjP5HsG?qX%{7Vt+OJ93Ns9iRci3L#Iz@BzhH0&uyoUB+{eM%%n3y2P6hxBG@ z@5*uBIJCd8Lu}s8b|y zrC~siAG;avlNUG*|C+J1-!_Dt-@aWv$z22GdAh=(oSYotzt4U79={bytRA@4ufXIE zQ@+3!?g`U;a)S*e3N+RV#2Rzbxlep0X$|H+@hTBLqt3jSO+u>S& z7{b-N_4SjbQ69QN9*e5=ZbziGI7IJz+N9d^01WM`&+!bCxOmKGff^^ojYG0q;IM#7 zCS?PC7MN9|aj^qCWXA{PZ`I%)t+F3g*a7Qd3o)nD%v2cQroR*22kHi#IDg00n6E4! z<11IZJqC0&_CRqesprOmL!ETKFt8uN#10GzV@Dl1^PQnay96=|Xs{mjBaxZkTK9BO zNCL>3L(m9bccyWI>sk5e#S^dFeBr)5UuQ05nr! ziy_yS3K={A&%sFczXU7_oF_f`FTi47Ysjtt0xar;BiT~km&I1*El+v5ZSg=n+7(yI zdJM>dze6TseiB=XGq1Bjlrx?isrCEZ6TDSI9-B;j+Ubecr5mu!pXOlvMZ7B;4HGW|ro|e#!$2XPx}5l+0%qDa~>K%oKQ5zBj*( zT%tFA=;Q4@@EP=osoePd0jmpgK7^makWPKVBqQY5HTZY#m*nR5!_A$W)$ZJhR!#8B zNS!Dp+WSrIefYvTk<|4_(5|7MHfzXAgOuoh5wE=7$L{;{UlJ z{D}K^#X$Z0zKWlgCr?E-p$L>;XqS>QGdeLrGD4S^$BY&bnM$bc(=Xl%L1Fr6-R5PH z^$XG8Q$4tE+$1h}U+H@|z683@SG9Z$^v>>xl-FfOQ){os#)!$r0Y=(r)4H=umRMBf zl7Om3?G?I&y;40(7QJPOWx0M_=ymM!`pjhg;Jv;ta|TnwM9B(ak)0-7z(wUXNk%ps zf(5DZUA_4Hv0XC{O~P2amdQn+X0Le7f8wHmd~K$`?WP^pH~!Y>&jfrAJdGyE*BIz{{RKF|G3&XD1Cm+ z*oGUA^7z%>uls>sRa5QFo(R*cX>Ld@E|dIb86TCx$~z!Qi^PM##Gwx+{_XAkNeWiK zx_|L%R}{az!t9?vy#A2%!9Ry_KxHyV6Z~bg<{VVTL2u0G$xAgG+%ofsx)S}EMMU4t zNp4(SM$i<@-;J;+m-xk3P=2jCqEd*pQd_=P^&qgT^a(Df67vW^eR7NVT){qMOnNNr z|5#+e$G=mc#TMUF@uR}v_&N^MPXQ$gJRua&sZq@TEkGi(aNrS`?UDY}!{#7vrKxEy zI$9a9@}gzSe^gu|Qrv**iqRas_5QhK`=m6!kkd=hH;f(t>N5SQaKMTTRbr;Tcdt~x z`t{%fn5qhl(4Lbllw@485AAGi9FRMN;0mmEGIQUXMpG6o^f#zx2FI3cj>B|AvY5Q zM^@(L=E@r2kPHCD_`L}n;8d47oe;G%jj7myEm>-hKoyMNvR~}YVHCg*@AqvY*B3D$ z;2(o^{L%&*FDb5!oSYSqC}uo%S20cEkfF0r|4qsx8lLryw_rjb=$R&7V&Y~g0%#p+ zdSF!pJT79l5s)xheCv>g?f;56EL?8Nq@)jt)v*RZxy@p=;cy;Z|qKFDjPzqxi>$ z0jU{jC`xF`#!^ZIZ4A~LR{l{mDt##l+&l9hBO1~=!N@(XGozgb{ZjbW6_l+4EZ zknzRInjX-RxmC+OHpk0VA?Q<5LVG2g3^0q;CMNVuOo*wmp^)pVWPF^Dy4q;!OWSC4 zDCHNwR@k9e2Yq@7an#^jR|a+-^W|l0s?9q8rd`z4oBiN&Sq$ubC&`h~{c{@{dD&b> zK`u*#zja}~*HQ1@V@yn!!^3&FqrDCEe1_z2hhp4IDbcIrs$>$~2f8DII}d8|eHQ3% zRQZYDbMF}r4_C$XJM4&BPAzM*Tu|sFQKV0aMYffY=v}P-bUWEL04^)ev}^E^NxQtG z@p(3IX42WcYa8rm07$;Q_ehA78Pq~>%1yL&oZZ*b-t-dila9%9&i2uIulIaQZ&f>N+|lg@p!Rir9-S4fulI3xda(3p>84nbCZ!9E;JFN8#tO;(!1Z7%xq!9jz|;yp z<-oOm0R90e8Ty>jcEXX9Ej#GS%nY3>BV$6uDc!$bwG5xo{(8uM4g8K8Jx=yopKHI- z2#ig*8m{YLGRA?y6yViei_6Ea@jgTJV=G?iEjiRluUv*$BfzCDn&@e2`W^kUDR>qG z_5gKqiXkD@X>_xslo|iLMMG25O>+t2N7h$YimSCS#CRW{dD%=%U!j^~)O{mEwQe}0 z?+k$oO)sbs3X9DPi_z~|ylhGhj!H{A>&yh?*c0us(g`Q)<*P6w^#kwg5aW83Db>BY z>+`m)6KX~!(58+S_fr4$>J93S%N~DE?hM@>Q9Ljnp``8^H-q5iN<#h`MQt>vEKdwh zU0O2f9})A6jAZ^`1#a#hdHnh_sdRS;f=G*WPP#j#4LYS8q#LArf=H)ycZYQ6dGK3n{nuV+ z@3qg^W1MpwbA00yn3MTV-}ia%=f1D&`lY3&5T`!Be0f^zc=fDer(keD(dOgCuewfr z1bUa8^4O3NOFcRJ)e(mm=yz-VMdXrapl3VTP@cB%1JO845GR^ryP$Vu`Ae$KU2uNU zl$7*a5U#4d-j_Vz|La#%l7F3Vc0oZRP_7jfSsEFM18I$j2y>}+ol*z)XSjng_Wx}< zWbIUZ$ntWAoq+^kG&&w1j81Taq;jgLZjc8Fy8j@i^`q^5>H)nEaO=?0^1Clzux7pu z|1+0cnjiQoEzC_%iK(+7}WB*d$0W?+;ym@2^Qw)(9rbD?|wQS-pS00qr= znAd3!ErX$sTSq7f@1Ge0op~Xk`iqIdNq_OeAN`N2>V&Bo^O9crN@v&&h`X#RU>v2ReB0WlsHj+BzJ_cPr})P2F&}?Jo`q>wPft!xj#jhtT5DYe z{D^9FY_v>@fY)%e+1;Hd*jn-?UjYVSt~}|z%QBBYWqp%kBbhy|*53_12}o1^IfVd; zTsEMi@0q!I8aF?xdyu~4=4KulRfxyY&Vcn^LC1N$O^Bb?g&hT$DbUz_TlL3qa8{{1 zBx+PL83oU6vpfB1XN(5RWz$9T~D+f?G&{hEaxHgZW4=>J!w=-Zq-;P zJbpG}YnNkY-ZNx+8l$W{HAQV>-~er+dwPjc)oQl{ugtFrd3tG}=ww~8xZ~g`)A#w! zv*UIAlSAgPFgYP_g-9Py>USK7RZKDDiu8DpuI_g?eBu6@X_9aYcgPTL#zRK>zE)`@ z(X`Bn%+8xyb2P9CCy7pq8ZjDiz1kI5WUmMaIGAG3@swN>28hwn<~ukDC4xU#ftHa1 zfruL<>#dDuzF$#IdIf=x1%mVki3MAhb@ zI=DgBM1d{t?raj~6_`O@TXH3jj!_XDxy#SW_ezjgM7AdBH@Vm z;L$h1^R6i z*tc+TaRoeVz3=Cnr!2=ek|Ka0$H&+IZ10bazJ5-A_SocPd|DhH@y+&Vx3@#N-5LDD z!FnoyfD0$(wT7(831%|R#e=_Jb;8Gz6)Bdc?i%eugxIrT9ZvrJU4K6w@Sm#>TZ9_$ zucbtJ0O#cRN%^c5tEqh?W00N&MUM`>I zd3=dU~2$>RoS)a)iE-aA?lZlOqX@?hictKH;pQY~TYzec! zfB&%BF^2pGC2)0F``@rp4AC^7uN6-KZp*$7GoAS#HJPUB!hxdrx}^<)hiWc<(;uY7Y{9lSkx;YOUplRLkU;5_e#>!0m2f5*pLd= zdS8utS5;NTv-os^kSN&dgfK6!gsME`om4>nOXgp@-Z~srj(?J80rP?Yxv!(c@2EV| zMbb_|uN2I~4`(~D-aM7;>KPyE8qFYQ-mr4alOMcOBiH^MR28#z@EQ#dFR@;O(qAzJ zcswp7kEV{3>-iXnfkC8EVeMsRlmD61u3mvr*6VE5QI&Qh>UmUeTlQTq%DCR<_14cL zCZ{RSQ!s1p=L6f}Dp1Ex6;ar#wP)}mvFbh^6uq#%-Bi*rl^Pw~s9B3$KdvTR5p?X& zqlU0-)*vMYZI9e`@)JxA=T+X6f9bS=l6rkCD5@t8Z_NeT#fK@);?xK&0O?*WxkK>>IzUF$*nHOv$9G7Y49g!hsuhI%*@Q_Xh=eiy3MiGXaC9i z^pXC2Yi9pkRuiH->{BN0E3Vj_G7qMUva+%&B=mH2M~k1E%4`;-=&%3uNSn}v$mI%q z;XZ-@)zl?WucOKKn=K3+oScjgk2AgO3il*(xS1TvYC}IXG%_><>$l^L4-i%4o9POT zO;2}BPaB&q9)Pi@v5AS~M=d^fJ^kd^6bBl`m!lDxZ?AzLMBlte_ob4GQd?)|T%G4J z;DWR^Z2Dk#bd2wTiXR?DwN$$qi01C7-#k8N?i$>H4i9yT@A?F~yaea>6zJlZIzAA0 zzj$9ps|P2@WXI=yqdm8&yhHn(nG4VbhDKRByxz$*?^Wam=7;)6TUC^0zlx@pZ=0i_ z*ki9`Vy|S>^+R1RQlnl~;1$DvnqVgNZS(?x*!8ZC+$?O$x^&8_%Bvej{>`omO7ZtY z#K1g(P9&J{HZv~vxptFnMnSn~$SyW;K)9cwK|ZbcI|E~_w%`qDg?1ZCff`jqO4Rh= zm`~wF2-qrv?kHG2aAK74QFDrtiGf!lk3|^tGnr^Yel5K-Vg{&hK+C1&X1;TA1><(` zAmWcJ#FGQ{kd#{2GPO+dbeeN)JU}3{yfA<)_66Q@j>Q0`LPw4Y@F4&mQCt+k&9Wh=Hj1dAri2^#3&hye5#|^AHWg=#&1DuJAb6zSQyULj!9BX>j1Litbdq*n~ zy^7x2kdT!xuDAxBuSW{=7-9-6&GIeH%!n}#EW69MlhM~?CWg1K2VVq5>U$j|ne?i> zKBjzU%yM<(@J>X&hK>FBXUJ4?BB3PK-#%DwW=56EI1RS!n|Ul{L?fsLI@BT@lk_W0 zxV*ZcL_-(bQ#plX#49!`1_45^UD2F#e?-21sT(KMmMf$($0e)5Z!{sgZ9in5UI%q1a=7tbugHSib~GG z6}X3JR{x2Pi}UsdkXbZ*p1hjo`?5?4jcH#{;yVPYDoj=Ef3K@UT=)tr2r4Lui8(#M z%#3By5i@)*3P2?MF88vz?DNfc`Gx5CJhkrOI{v;)%kZeEAxv-uDI?ns8=s1*i@JZxjV%{tA1A;_)lRh|9e)vW|xu@A)kFDaOK$`g$}TAaB*{R)s#8Zxt?+) zwVU5sjB!-@g(uJf=-*voR#5?XWMp`qHHQmWn*PtU`<`&P<7YasyT%TCBOGPmNCY?= z`i-!d=Xl6{0W5mc@IS98{l2_-AVK%F&uv^~-#_BQG+>4-PFQ@(45kHc)pp;D~pXA|oruspJfK9)3${QW9c|N>ab*4t07WVgksfie`Pxbf6=}hC+U5GlGY-tAP<>gOdo0FNN+*k?jq*qbPVJV;1`yt3~^=M8` zB83w`GYKrki`W#sJ%4V_@W5FOH0&2k-N&URc*@Ur3b*8TPWx@cMfQvbh&qe4>>X2d zsROnRm74j zNi_XrckH^^I~=8`C_TTd;WD`_BkOgoI+&rz_|n&San8Ha`A_K%?YtrreV@ zWdQVYr^`HhSNzd;cBa6U=dE_x4DckCsg0nB+Su4AR(rw(C}(&*6Wgm_PnyH4GdCH{e}AI0!@;3@g-Ytmi+TGhJRZ17w(NhkUZf z>VZf;aojW6_>Z|pw7_@E=KBALQ!h!I#$s1y1Hxm!o5>8vSk!XM3!(i=rPk>qlNCF^rQv~}v5{5=k zo`lhApol_+-HAvv6t%PSvX+w|x==iuT5z!VZBG57{klq5;4gl?3oTq71bT(CaGP^zQ5S!z^$M^;))-RX}YV&k= zv!(#aZopdSMQNI3_P3c-7S?wk^Z~h5NkwJ0$@PUWw{+^;nrf%BgDq;{sG4wJth-MX z41K4g5B6VR>YkGCD+@N~g?Q*QV;#Rcf`=!HIE0oOq4LW%eqm?d9e@hOab$heVnTCD z>Q{S$DdW334{FQa>hm}|F8-B)xuEE1A%AqXP*lcdf?o?P-_H4X_AZc;@)a#jWSB!j z=z;>MVv~TE_Iw`=qNiD{oRuXN&(b0p9xBXVme`pZE8Ix|))!0DW4#a-w-Yrpl7C%D|Ghw7 zq|ah`clSsg+-;n->ev6?Z3Ig8fDkz@DJCk0=>T&G@|q$74151a)_UZ?U)DMbP|PGb0V8PJ z#imL%@X90OvwoJLG0AFbaJy@h&_vM~ghBoreRj!|Y;wPTia`CwWfVVfXe-Cypb^U% z7#JuUiCa42mw|vH^}!ak)~4FpYxvjTL=NtBc?8SKV_{~ZA%^Ux)Jqor`!xZ=aa0FL zdIw}yYkUXNHDwDr2U>1hfW^fQg+$J)amNk16 z`XGSeRM7tD68F^xM>!VMbh!4pc^UYS#|>vce+^v%ED!J`{N0B%`nD~A#uaGW|88_j z2M!?!1}-5M2~PLDf`>uyC?p`)s^6p^=f7zXwa|Bbsz8OG|;veIy-^Brtk|9NbJ{gQ$&U zQGhDT=`Q?Eu0I(B3L>R2X@44-kdgAJfmIPWv7N<#f!P((chdfzXa82!yv@NiC)5CM zlOe;~Q_@aPTbq%C6A7b3Cb>#?%MEaITJYeXLXeLgT+6in6Yl);z6bV6ysGA$R4Y)N z07xSZ3ld{oZ|MJwcCMre-&>jL31`Q;#5vZ#{Dh|ME!G1%eOhf%o#54hkg;zfm3u@? z)Ce~R+^5QDvAc`AESiL^eaNTu7E3dUKa7U?hEP;8#v9=pg~`+%;91C&d!flcN+P za|;Xp=$ZmXBo?;E>;+9B4XGRE19>nKEndJ zj2>i{(cMa`j9L|Exq81ODyap15fS0OUP-2w0`(mo&_jY%Qd9lsXkUm74MGd3$^F9d z$9fX$>)n7^KN1wj(*ITf02#r*OR_Ph$RNN>h;t4O zC!q7}lF{{nvw=sz3e3tcMuE8UCbU+atr=wlb&8C*5hLvFQ#~MUZnuh`jqm&~;$|*6 z*~_8bW3WbGs{xl{Ma3{f(Jm*46R<+G$W=L^S?(rRIi4iQP=mI3@Ey7=JGyi^9zX)? ztK}AdEiC>@Upan~p{1h1;QFG5`&u8PwC&@HhEOBFsxp^s=)n@AI^DxEBNZR7uC6x2 z&B1~3HF-Ka6pwmTCy0iI4M5}LQHk`~EZ@;v3_Cf|u_wvm{iI{`Y}(6BZEyyL)g6~z zGQTi6Pvz6XsvW{f{9Fih_Oe#;H<< zrxJzv`wgwY6VM<4r&4h#NR>dfN)3^t>&#xkqB%308Zu#fBl3CkGBt(z>PdV`3L;1b z(9ph@+3Q;wMr4)(9XsEl@gdq}c;99-I}AF^JPGM@&o$WRh*u!xsIbuJC5!SHCs1>w zzCGz$m;I|VuAd7!8pgG@%7%RZZC>H>KcLLLfLakMsV=TDwW0cNN?~FMNL4W9;5%^A z*dKH0%Cj4>BGCfoV^BH2p5ul9D3Me--rd|!37Rkp>4$pC6NBEoaFSYumLs3DwDS`x zG*2c!8f;W7UT625Z}UHTfNg_-fawO;zsteP9w{r*;MX`hYR_>HX$Ix@v9cYIMqpMvR+bGTqKbg3L1`Pmjd2(eq=?e{eGp(u z>6n~s>*~UX01PD=@ig=!BD%ahVQ5G2NNeFL7XHxL-QxIIAadozo^0FM;@#X>;X8+Q zCZO*B@c#Y5*&zUDBSLV4oh}ydz^LinBmkaga#A;_w3+o?z0)DAvzoqUCi1>AT9Hq2 zC9GuC`=FrlVYq`6)as7wC*5*9OeAqEW%9l9?li|<@r+HK&g)1c&Y;S!U;JmAYMnX< z_Z={DV*=s+veUm8RZg~ap+deug|?T30W-cREAP`pY>NfU;_0J}(TN%sL3vMlk`5$J z&M~XOr$Y`vI@&egog$NtiqE!w)BT1lWe3JMWfNNk7g~g0Q01+$Fd`4Gikr&xQVEDd`}lg!E`T zA3sT2XcoDONq+;{y}=Yvrl(-|+_vsDg`K&14;FSA707?D57RiiF5nJp7eUf>~jN@XhhXBFOPTC!6*5{$y@0b~c7%N@VIBR^^Q?IosgA zF+ax-a!#H-3wstO2V{6Br)YUf+c&-oK-m1$-@;~Lf9=gLq+;x&BXNCCOCN1@#eFC9 zq+UgEjZk9Ibemlw4I1jCk0CpWZ++prtaYpk#sZyZNu@E45Lw1Rw=djM6NUgj!hih)@iz&!&88Rd48i?q0A>b8IiiihtYhbjr)>nV826FjKfC*JF8*sVDuKVH_I{*la zaOA6}6`&QTAL#VVS)I31;L~E}OQ|3oB5!ysBnU7a?L)o1IK1^TA8|9cd=q;YRIptT zz~?5&sU1PTS-1^(E#ewcd6rjLz^I=dgbVq>dzwAA_VGQR;^6!p1mon$*FP!hp!v&< z2V}DwFXpANaPup|+5qggs`Bz5)A5tuG5uF5Zb3l-M7K4o8_UbeYySF^2H>euP-d+V zP=^|vhkg9XzMPa%O;3!+ibZp2HYH+W@1%?~Wc)q1bYj?%q>YLIx{5Ty>w6=42EtU+ zI47*VKw+p#L_~sIJl?*sVGQ@Fy5w_EK|Q=8D#^05CTVFI83DB(GUC(^6tl(l z@49q!m@%@4Hk3X|6r-b@+2wJ=%cA;Eeaj2nHj-JaWbiASD~f%n$KR$v^}#$TE02OP zG;hI#kg(cCGcqKkv%boak8CDD9JC^`MLAw$;k}0GZkl*TSb5<>`q4oT>ot9d_koBI-F3 zkFHfRY2snz1OwV5cM-6gZfDi-nPF@=R)5$0muz(A` z!3KCmNLPOk1cto)rO`4YROG@qJ*%iyN$K%}=QXTr8W{G#SRC@*24n@J`Vqnf4Iwm;-ogi$5FX5*cYbb$*yf=K!|bUlPe-F zjyUuO0L3$;N#d$$iyWR&gB6V+dCn6IhIx{&@(vcmOJro^z8933nOiGDq4F|clT#BP zL71$C+LRvs58!Sg59i~(_U5qBGW8;UZYB;ct`9Fq;ouAm#rs*NN_M^Ttbe$qvp>Ix zQ=8&reI5__hJn{FjLu@W0cMC#()nDZi6ruO5h1N_bl@Q=0|#64=wh7$SwMj&S*Qzo zCvep5zG4CBprnhS2Z6;_6WyR-6U~$5-U~vueuDscnUGKoGhWUgfsmILEVI~zb>X}W z3fdl!gGZ^VubYucLWTt94z9yIkRX93^Y=^j_Pzk0xB5?{PXn$S_bYBh$og4Yz3rkS ziIHr1QF58zProvH_}MGCmZ-_^zn|xyvKw`=1FY?~*^(5sV$4?1!o{N!(n~>z;d3J> zD$Lg(fzK2E>-6+=?PLvw>Qm3-$Mf8bC+my?ZmZy*1}9K#y57p9`3_;z@o~z)=19%L zS+jFZB_-Gmeg$2>1cx|WX7#c-orKb>{I#)CT zA(4gOKBCz`c?%}eG{!2Pj~~e;R897bOKN0KpZOrYlMA8Fz`8_#s-!%E0SlM@*SW_? zxU(azzXECb;Vm5X0`91Q?sqE@^e*XJbgwF;WjUz~=6#pGwfdLGkO$lV-2e^Sx%DIMB3K<52;)H3wVMRez|6 zX&2C!1T=$)6%_`}a?cHGkaIX@F;S}1KsxUtRxsIMG7%J02>Em!7-r^vJREadw^ajj z2@kPpF998!SM2p-)}P81fL;r<({LV0=rgW}W4^sB)pkb<4S}Vw?#vKWTyTdxMZY$i z+m`ntB%ZzZKHK9j$uGP|xZr+)MD=19=S+tUQN(utv-_BGRh35Z-0@cFNbYrhan(ru zwrOFp(;(kADfv_69t^lC_tMu`Ufx==2Csb{iT)nEUL!k2PH@e-G4G06ush>;NXX4ipQn!qi< zwd|7}?Qv{iK5vy<)29HS=vwlep_NOr(Kjb06ne?w@%z?**eH#sJXdq%Bj_lp4i@+$=DcrO8yhq9!kmJ*98e(;zRmR<=wq)_>ZD$@%fY0cUUb={q4!!DjZ}9# zHd&!qgS5s$l$31|n8n@&;#<*e>$Vg^SEaBrI|nQ6lPC0RQr_kTC1H2EYrprKad>(C zZZ`Ti(N)ZA4SYVqnKG3&>aI^n8;i>j6R=E^aKL z>WP9-X zK+^sOnXa+%0EfDulh)`YYcYuw`;X?5T|^$$V%WBFa@2()Vh5nZP|uhA-TXYA@4rxOeMX7cS=H#D1be!qv-9M_!wOphob73!cd zwofR^sb@QOHW!_xUcU`tnnGsRZu4dD{9sLWg7mb?!ItYefnA+IU{+Z{d1D^=x!ij3 z>~3F4&mcGjgC;jCl$F;$|6E#+9VeC}+OU(gFH)>bFn~aQi-@c0%L|`3c4AlbU0o2y zCzg|W&`(puLGSPU`6a??gssAFo^LWzzmVs^6d^})4s^~qv=h@ z%=q?*tcRK0v8(GP#dN(M8n)Bsv+1dx%t~-9kZ>+d33-a1ccHX>dJl&NWTj$Cs;Y5m zY5ub! zV07hG@4|RE_}3!j7|B8|lCF}{($a652fAQ z+{%VbGMSS_kKiDShlaW>H7<@$w)Ry&$}yj!Uc#r#&=i*x^z{$Cld1int^An*9(?kS zKNZt%i!U0_P9rGakVMdigRT`d#?usSE%2a~O$8}$pTmW(-qXdLbqzFL+}O8oFgITr zg9wc9B}~kj={?aPR6@RrPUM8#@jjcOrTl@+7kLq`_L-bfRVbwJF{h3bS}nV^W>~5=wCFqyR0Hp~?W2 z5@W#aot)&M;;?K-VV<7h4@qMwgQeejr+Ei4z<**Yv)s}*goi*JeSm0%>>h(cN-oI8 zG*9B;V_;QPijPZvo)#CSuBgbki1LR-`wE@#6~a@Y_UQd>j1t2 z)RIxOVT`VB3d+jVn6gPiBjW?lco7~x<>^(j=N)T@8X6i{%S;pxhr~nAOh46ZF#~P3 zskIXVN*{e}XNEIns0H*B4-c zb?8&)R6>;hw3d#xvfZ^xd7S*G*QTaMR+>uj{*Bw1IrBOtiZq&!pByvxt$i@l*3sG9 zV&C$=YK5eh)S85t);+cQ z01HX$qI)QL8Te0FX6s`*JWOx>;F9{i4%bvc8ewUGF6hv?yI+5Ff5_%s^WBIAp}%zA z-qRro_paC46o~?M$uZeuU&Q6FpFBc-bd%AVQDGu)b7^E?WHd4Rl$XCjOzhcHlzkf$ z6B9kN_LW1hGas?ZP7!u-1`u5^+DLIifd5J(n}~%^Y^kUBpv|<--CwA2*-zDGOOW}s zs<^?ATw{5yZM4L{?|CrbPWF;HfyK+wPC+558PouXChAZi|Gi;&x>Imh}277p}u+>!v$SkX`tw;9t z;~X1%^{U&OOwi+`ORui+k8yKFf{a+&{eB_%5jy5NXGXp55tNymTTDs`&$D(qibRmW zY9?v+M7q4bdqXyX&+{T6qQrX7&+p--+5usS+a{UEi{VVF@2mZH#eT$BvdqnAxnx{c zY9?kzRh#+kOG}vJTwLw#;@mb=R&(W-DjEeezReYdrtDFmRoX~=gtC5&DCT(D<4OA) zQCbH#FeIv^GnLma?-m*_VDPyqQ`*{j9oAfZ=N>q?bb;to>voZtpn3H1Dzd{ba`{VT zbX(;HboyzrVSE-uY|<0KG)b4)p?>IPmE zEHV@s#YE%ygh5w+XqA{V4t0&7qAUs-t&KkpwF)gZG&f$_+}&H;G&j_zdmePcLo8|V z#!ON{<*kPO@Ys|7oSfX;jUBS(y)6r4>+TWmZya1erxN{qEjM?*tH0aw*tDPw8S-hb zuntcZmoP}{35?IeA4OZ@1@Cn28VzE)L#bK|{_R$MG$J>A02MHf_O zzgNY*d8df3`jWWJ>}z_8m8{%Ace`}<>1pCF{tI1*_LtJZ%iXEZ*c94Zcm8{~+Kf5q zr&`t9Vs^f7-cB$n`Tg-NmRkZj;4v^`%Vylg` zc(9vghXewGVpR?nmXGJRF`1nq%3D?Ehkw}ZhNHQ4-1D=tvZNzRNuE8Evw&R>&CY(= zJADeok9knFiy$NE5^d?tf;UrhqmyMVE#JXr^d-hq9DlFA$-}RY(dQ<&R|~4(fhKsN z6n2;Mnmn1q)jSFAD-t#h>zLlcyx{bpVVPRv`gK}zigxtgWIqMP6R=*z^SEvkmBN%X zH4Dq7OxV>esMS;GMHNTL#!FmW&fm+y@)Hu`;?AS|4JOV)i_iOi&4AR_ zr688-z%pFPBOW-APxC`bF8g~$r`_$KM!SigB7Sy0A0;D-ZX>Mf zvhV*kGkpaf`f=WK-I1b)-+{|$rgV%T!R_&5F3ej56#)}d6aIrE6{ynl zXpPPaTq`HlgwYi?+m!;Ox-I@%t7hk{b z-m{mcrT8xS)w~V&xMa>ZE@e|oLh}0P^az@!s4{mkwBDP$;-C@m_{ns0bKd>&$k%Bt z>RHu3F%&Q1)T6tO21*~PM5F70Il12a$?GjcZq1e4j&rkn5?_hP3?f!;?hP0X16A}^ z!u0LLz@YGTy!zxQ6|eop?>{qJn%;9uySr>A^HI%~S_@a)ait%Zd9_09+E>7=!eY$Oj|mQwnA4&*&#DEKuy1DpHNpPF1Gp!?2D8c zaJ?(m{kIfp3j}i7j4SSI%$@E7-ci)s-p$%HTgytqrbb6CEljxG`X(>snUdM8IAm>eXHr)D2ZkUKuo)GuvNzMvKv~#)dd4piu=#U~} zPI~&%W(4I(&LF;kVP0UMrhgF)_3h6;mxuR=rg8nkSJb^9C0IQ{2St?8$d1OT~}n zqmN=r-{NG?Q<=zwU7w9;lj3>qz17$9)Gm%FGqW|#0yoT*ug3wbiebC=nN9pOg3hC> zyr)&1x#478y-g#tvxa77*h0FspftbkwzG94;dbSU>t*a35l3qDpz`0VBU!&fhs4k{ck+ z%*+9lEP0!I#ZL6B-1&neG-3gdrAs=4@I=+AgP?+g$&qBF9 zbB|iZ+7=aLCcOIq7Op$cscOuPI4r$uvPWun>CeLY432pbJmhuLi?+Sh<)2#jczA2{ z_mZYB3Hi-%{eM~ImGZl6Z>IEVGL9~5Ek2(Z|7A#+~EHqKer)~wFo)T#JO?P4g^q)m0J!7Fjr z&qqhOB@>Nmj_D?L8~ILCZV`=~2(2@6U}k-V4MJ0do@W}?BI+qw>NGLkC_USEJPB7n zJEX-7ZOkSYREw5WQj@SuyEdvTf0rxMKGW&TvobBQFcq@rJx*W_6Euy?c|~uEjz6{F=wBRg5B1DjTq3GH-He@r1<1va_=bo9{jt6<3aq zB3z$i4TTU?Oy5j_Ch|rsV_+a`c5+e;;8l(XNuVBQNXPMivbq&HW;ej42z)yp$EO9- zc+AgnaJV=nvp$$9C@jqjHpCYPSXsRhy~;1*G}`9Kx!xZnnSr`)T~5-o0u8FHY-CYi zh?9l4*2QXaMn-Fi6wLWu_V7lkCk+#m3Pl8XG3)6c*86+fVyi1NogHsy>4!5q!WgwG zC&Bn8VBYKa6quAj4+v-rk_8>E*5XnpMn-mi!dJKqGEpqs*R5pI6x=Aiw}n2Dkf7Ms zeVJzw8k`7#1ma>u>xym;gP1QF39zI*yr_cj5a z@nf`q14*h7Yo@C?Wc(z%%etD61bX1;;H*mkP8VI(p zIW&^XrxwgbYR)H{da}sPG&pq)>;xbi*YnL23NZ$Scpd zF#H3*7g% zcYO;W868yiZ23xHJsVd7g?vvD1shwLgRRP?r>ct$MSYChN>4Vsa?IL?e5K(@HI)z1 z9+Y}uKLDOs>>?Se3|b0l-tXhny8;5NwZfz3?u#@{FUU#d(*mCD&)9+Ob-v{A>}YhV z^D!tRB5btVE#|c-%o*q#&gRhRybBs*Qk^zgFze(L{_NJ*9mXx!^57vJZ3Zri zP>RCmI9pO;L)<9KBSA6uj+7OG*aP8%#TjtR)ET&fvpbDb z8d4?1#*N|e$_@=a;QNT`-3N44?Qp4S&R`6kFL-4NycY`3U0`NN4j4$|{}pmDpkJoO zfP0+^;wo4vEBN_%tL_d#;y3U<(lPUTY`!)#dxaTFzzw^`a-v?h=EytM=?DJc0I8th z36Ept9UOLcwln%roJ_r=Ducn26i&|brq5niCv)Gz3zL%ea>biSh`5D<0!pCL0bwvl z=&=)hasOwdN=4m(m(-WW8s>DEpns{n)+_U^sI2U8KU1O-D%TMjOpyV0oK~9Lg*TFi zAvd*oNOZSz+VS1m%8_HKRBy;JFd}+rrYEK{OIrs9?!e8xKt_ZiuGjnCDQVgl`YUL* zI01A=NoiHl9i%NN>Ip|Wj!!{sb!aG>BQsS?Jg-f6kg71gkTKgUG&nIaYH`F5X6zMh zBeb6az%{y3s-+~jzu=Cw&`bM0g@m1rOVH`&wSxou+Jb#7d7L;dcj_e#0MNzT%VD`}U8%+>v?ab*?Dq%t>7sqp`)ZP(_4c zB%vhCAN-1APs%>pOI32c!U!ivr`JSD|2Aku-_Ir^CW=r(BfMv6V5y%cFUHz!C9vZ9 zDqmOa$F^xM>9ZWIXGsCz=5yNiwA9&acD{$D;`_D&BLb+K43?ETwWQ>mtL$#;qiFXU zknEfsK)j|Hhtc-QE5+QJFRyKiVzS6>%O zMCP;w8u=#j!j*>#`rL`_(;1M^1J@!2CALL2xtK*ZwXYFL{H$8K{Ohi7 zm=Kbvv%v z*V~<;@nI6*4Moe37}&>{_Yx|gLGnJB$c1WI1x_1IJ) zIIu(})F_fA);yFw-&ao1P(caz<=2u__Aur3y{UD*O}r4cqJwcq3Q+ir@2syI^3}PK zqjMUrF1N1R2X+|^KZ=vYp1#a!QPGNWSWsJt3<}yj7bZTMBi#Anm04|x71Z8egW^IKPUHjboXd)CWtht%HD2u*O`BfmpD|1x{S49rYcVJ!_3>SYO@^% zSveH9&+cmB_7CZNsN35cobN6NyI1IXWo2ae+|JA`cqYEw+>E23pd1ChgF+YD+A13x zLpU}OJa_)M;abRa>Wfkrl=;3<9xF=yi7h9bJG3j+s-VQT4MZ@xTLsPw$Y0btb`vvEXTxea&0_O+25pZ zaAnW$MQQf?=VtzWWa3!g-B>sI;>loVe42Y_dw=irne~44*C_hnc5_Qh48ov3hN2$n zU^LVV8Si?1c!X4#(8!jSw({YpC^_G*uC^a@YG!tNv~td100?aU@2Pq~$qcL|(vgEV zJjdxpm2costD=}y6LsX6;_{gyx&z;Zk97R-85@f!#$(%mY0E<_1bq*M<~}t>CBPh` zCC{1*_GlWn5f4dViRxVoOF9vY68oU`O;Jnfz0$X&q?F6=6Fj48h>+JNc09>>uN-7$ z`ZYB)!pNq8S><)HXs^r6d;u5X>XVdb-xWX7bdY%ArH8^ghj#)4EhZ3))R>+J2X6Mz zrj4zyQL19H{C?qg(~tsRg*NvwztU2Q9Gfvf_LYV=`JslJq_}LR%)F?WsL=T|v%EPB z33fMUKRiigbvxl#jZ5cId3h9@$xSxHW=l=u)_#;N*0yF3B4ySG-0hE|&1{-(!wQpT z;-DVm>vW)1(oV+DCwQctG-n&>ZcvB(2woQcad4`a-`0)nyMTMAo%O7?ww@m3^?be0 zuRmVRwsRpjr@uHBu^Sl{0oNJ-D&vs^B_qiKYtBYWKs+n52F{O;R7Mt1f6~em2Pd+a zoy36=TJdM&CzmxdvxaWdvg7H^trY$spzHYLlq6*iJQ9Am z?5?q^mTH;a!ha?#gg`cFxRL~&TN%oV5inDP>{wP**s3rU77GN>M~%OtYE$U-*KF~- z{jM*SJv;z(^3-6CWj(j?@2K3~%QMX-t=3U$4zXPF ziJOdo-NGxWXUD@v5>hG7$RVvT!G=s48LX#1lyAd?9BrGwW>T`J9l$)@-Ke5w23f^( zI5|cwBD?GNFUp$3$*yPG$Ho?fTZGA3s4CN%n!bWyD~G}6mr>nH#xC!#H|gJzp1I`{ z`ra%2U+legRMl(O_iLjfAg!c=gmfcPDvL%?x{+>Bx)D)n5Rh1;bc3|i0zsrfxfjpEJ%l?^$Dwp~JCUdHv>f%{jl{&oudX?p$jtW2^>0(rwpH#Fzd) zlaYnRtW_D8jx+TOpbACB;F0q<6eSfpLxh@##M&G)OdxA)fT{NL)2)zOh%hv%($kZpZzt;wusMAGeyeG(&d?EE38^0#+OSUB)+-|Sp1T%hZ` zK(T4VVs6d2P-SVQ`VLJhU?@G{vcA2u^)e$yMu7NLgn8SX`d6vppE{=gA)l6qEm1nN z6mBa4r618!8(Y7$sZa7|aAM~U^o0*O><@)|M@2=!0FI&C4j*|qyDpCZBw!b^1|e1T zPPdxH>Z*Ben6o7wk9`nkz<8SWrmcib%1nZnYyjXODtXO)Vyj%Eg-ZA2J)Vn+d3Q9o z-brkdOU3Dg5qI8V)S;yCG@@)}xh3J(&J7F>cV0}L(|=FheJ|8mMx${6Ko>ORIWb(0 zwOP#7HJ7f;w!0T(1-g~?HH&6Ezi@7T*zyiG7~4mr{7tKwQR#OgZvPtVX{~*x_c0d9 z6eh7{RbaM4!b1|+eCZ0J=Uy;NPD*h8!o@55tW=5RQH!|}lKR3b&26R#8}mli*G~ju%ypu9Ty4X^|+(r*Y($N5|{=@xwu`iacsWd5P8XjS!QQnlW~!&0wK5}4+Pa|((8I^Zz$lrUPlyce;_cso9vcDyr$TlEfyWYc zXc_^2REaP#u=sX{g_e<#@)BB==i&RC&s9w%c=L?=`+Kpm+2>56@vxMO2gM&cZ4Yqu z)6voC8Js8Ov9b}pvfRhAZ}O~F{oc*1<+~BZ@!^qSU6JOdO=xYZ(~Q9 z$g5K7Juf}W$d3umt9n&4QN-o4!DKuMW~NqMof|o7lDfEZ!NJ$C@d>8wM7yL(1)s@j zh$HGmWOT}BIi=iS7lHW{*k#VU+TR5dz4*mH%}fvTJScU$*VotX<1OKbj+cL)pI@4{ zUzbeNEq2HV8dzOB$`m+PF?bzP6?LJ8PKL+hfXD7X+#9*2#1xyb`=kY5$_D1&A%`O4 z+AbppF;%)paap}OEZ0nN#Km1MU=Jd;4ed8ITMaueM6a_@ly5c}mZeG4G&X+a=2mtfUeH;W*Z!Dl|IF8O58y8I3igsFgv&t(L_|w0*Bj z83Iv5)j@8-Ll)0d+ zUhB7Ws?iS|ePN)(X2=H2Qeb5CnV%1?TUl;h@gHv8j$_I-_lmfVjvg7E)p-75x`NW| zz{Yc|gPC#naS=qgq9SwEk?K-4^kmquJnh9ddtv-V+qdTcuRlpJU`$K#^pdS2<+Yl= ziM@|@>HS{y+Id!c`=GP=mM=V>KX6$;S6Cl*dOXn8?CTA)x<;Rdl~N*)_JZpW-{!XB z)jx~P$VkR%NZY5G3S^5@@5u&AOP|S^`kg*-6bGG!7P5@SbLTXeXgNaK3?x(9`locs z4)hJIF0iB1E7`}xZh17qi?E^DBEY`vg<#X&ps&_`glNq^r_RLvKz^rZoLMJ*)4p6i z6tTINpkY-O)6h+h%URgQbRta^@10w(K~0g6Kx?&bWMZnQ7ccObKhlM~`h~|j${tna za`D}J3JQs*((-XvkYzvgQ*l2X3SmOV5j)DnK4>c!np{>V^e`Q|o{xg&``yJ}8r{Rz zsBqfv+7HD#c2kwW>5ftLTU){?EW+U}|3WD^%=-$Q6ODRj1($;lQe!Aj?;EXB#}R_V zvxE*JA_R{;=+JWx-yJZhiXn`7PZ+*91bcU#d^t>5Q?tCq!)uDt4Z`AQlKCXZt7meT zIx#A+=!xz)5bf|VvV0I3dHc2=zI?xAGM7QEV^XD{lfk1`!k)n& zL=h2|cd^^mQeW=9{epo3`qs_LN_7ZDAXYg!?}2hXf@xQoS((#Tu>OUOjH=(s@E^Oc zxz%)b^E&TtSMS|Gxn98}YBhWHv*xS)s$N)FI1!OAwRF_c5V0ks2Qe|rGJ=|e?Zj8~ zQ&I}!BI|VWT@HUyX;v#LD5S74BjxEJ6Suaux3{xnW#w#ZYO*jh`dm;DC7|0kyjH2D zDqHI>K~C&Ub+YJc>6n(9eCg7a!NK9Zjjg?vDVPkcs-V!MlTSiIlGYKqGa(ueED#o~ z^~_k8rvS6eS>`s`S2dI!@ z^%o!0u&J~&Wvc9&qx@`50gL!xR_YLY- zu>1RE4HwMNe-DH@8ZcxvDqm{qGwuYU5>w1IVpQdl08j2~$9DVN>xZmu`0hQwGR#Ue zyNU<6Xe^Tz*SdCAxL>y!`1wm&t&T`PMM)!Y#FNrWtHX*JyK!A-Hj#&DiTCYZ?Gapk zTe2%{#$UDAY=x9EG1DR7arkH-e}|&ES*(J{8r8Pohb;4k1Bv{bo7=BZR>6&55)uR! zhVT#US!ztm)IkM7$AP!8jZsk8#VIbp9_$$q>zSXQQ9RH-G?a@%mEZwsV#qHj z(NEO|RhqTcMtU}FPF~i`#&*@=B`hMYz%~OVh46R*w?`ZAJ>V-Z{LE4MnT@mX=hi!g zFn#Fqy{Dk8u5qgqd77S=rJc(9tjQ6&7`m%CuxWBdw;2@9u-%Ss<6MFYRSgpk< zSlfT07~5E;V7PrDc-)Pi0`uq`R9sak=wsm?}nB#oGwliwpzpo>*n%iy!njRQDMhvgC>Ky~iXS zsD9bm4W3WyS$^mb2{t!NAZJbwCt+glcb=`;GucHOjb92@B;M*=yqkmFzAw3(s{Qz} zn&TjGUKK;I*w7k6ZEs7aB|LAwyTP&{{!}2M%4Q?+?OTW0E$CLS@9w^`v5Mj`-ECDV z46{i0J>BYCQY$%@#<~?f)7G!$xaJuK8J$IJ%*T(@RnGD>6-vDOiz=sE|H)>+MAx<6 zP#p8eNiaLH@+hERQ>ALwSxBhPe(dmP?4H1<3YlDm)9pHY%gP-t>jEqtaoX&=2PSsQ{S^LuttdutFe793!#z{p;CXJHU7=#Qrk1Y2`Vu?+w~<@l=L7m z!~ek@Q#a8(v-$RmB0J5W;8yp@)R-;B4Qjw4iu z_U=7axx0VTGOp;ZjVV#y_ZYY=q1TIX6|<$KsZ~toIq!Zet&r&MF{?|fp1q6~)ZqupB+d-ghL$^} z`9}usnhZ_98S<4(iaxVlqP!i5k1s$s{liuRF%6$gnWq_4u!XWqLN z=}r^MTFj#CC^zDUtnBhvi>E8@28pKwEQX?{&kN<_lvJJBn4G7+Ca0p=H)u&RJ5G9f zGPhmD+$fpddLWFyla@Q1J?gA+&*A6IP{Pp7hN^tS{pr__J?z4%-()@%-)^g2v!_x< zZVvlIxRf&*^dr@*MV!12hrHV`_77sB%2RoTd#^1}fewU;8lNvN1$c|*>Ze;?r^jzQ z=9pE=m$y0uQk|;|#|EU%<=&B6$@LPP1(67wwip^=lf7?#yLBH*dNhD!t8Q8i`N#f1fZ0|OGi z&zAk|f@;-!pX@e!E2n%>MS2^FWm~g{`)H9!r2WdTOjT(~G^`XSUIKSci}q||IIZJg z^VEQfJ~MMsLTVcn6!@XrT%+V4yHq`%gV!WBBBG_0#^vQH$|9$dv}yXNveMDoKsuh) zbO1DuOt}{lJ_Fi=k#S~?Qj`p@W8RE<>3_q1f>w2uu-m!Tr#eW94?Ar6YOK=fCrD?) zLgeK%dcPKLZk@S4jt*b^0(pRk>tS~ltDn5QlEVMs`{;X`cYL4sm0rg(O~l6K+6i6i z2s}bIEZfWo7YY*#i-W_T?Fe4cHG#)EyrKyS-t{^lf=C__6DubeKH$r3n@*#mA(+C92#SFuF{76*2>B$lGSkrSZr)CK_O{hnt21n% zyMupi;~PVv0P0F=>N471T~XsqfA#4^doX2gD2>V$w2hqX2QF^bPtDCWHTMIeFGnyF zHk=UGVmY|IQz3`K-tm!N`GwxN?*-qIlWmEQA_SLMbR1N4bPlvCI0y3^F8Ns+<;w9+ zFXBrf_6LpAh0kGentyLStvqJJej4{65J`Cck?D7T(!EZac-+pSCR{6PYbQHHyQ8w< z-ZsX&cik2yg<%BZxpNfPZ!8GDY`jG=gxX2l?9#bcy|;;em6VOm*So|**^~9m%t6Nb zA6yVloXnQw6rdY!>F8LRAoHlXqr}ZkG1PC~O-c!}n@{%8zxuDeBIk-E?%@OkMLj~m z%(_Js&&7w{$o-PcOt3Lk|5nx`rWbHu(6Uh^a?y~8fWRHBCoEp@NBTs4gcc_o8_Ho~ zWMz9hG4TnPT8H4BpYI5@SlQVvO+w60^mVaTqkVjumU*@!2aW7c_PvrLZfuWNz`5_7 zQ_{a%NzQ#jzwP4>8e=x&O}wxf&`a!%Ir7r8CyDMZE*OS0siB^1jQ8|>Y>VcifY0>D z?aW&O)!VaJ!6{%d@OgrX$wINO&#J64{WA;o?B?#cdmDTs9y>EujxZcSkVfX{s5|KC zQyc)W{Be8zZq|ZN=;|%L zA-<|3g$q~gFx#byKAHOXOA+vG;b6x;bo*&AsW8p=_k(XJ5lGr~UOwI3hLw`g9hO$`1p-v6RqgQ4p=tCve^pfwo|NXGn z>+io#{Zupw0bLOmb2V}QR3uek`|81!E4LT!1B$tk{B>V;w)x?u_d|oc=orFQW!aep zN1#_)Qex)j=KjNS_xtJIzJAQC5E(D0CTDKCc8>$Ex3}+Ra9XJ#c1jwx&q~aYdOg(B zVZ-~{+1vO{o)&N6)dR2|bcG~l`xi|oXUS%KTwPO(;j#2-X1+np`D!5Dr&+QjTT4St zZgh6m%#76K=#BXb$>k?PTxxlh*$;T`pTO5srhf7XaqUtqO8a80M%4`rVcCo#7D1ZX z&4tO@Bd%&bP?KXH#6C4f$u_>ruAN;V9Bw%mQwddjyK}_-3d<;jgiY2 zgupalAw3%=X^5bh5@(OQmsrJHIz6Lj+snH0Vl%8;R#i6L%6pDU^)Z6xbS%P((nIBz zzzYYXU!M}+RMK}EQlJHLaB=3*GFLgwD#J`UNLMvckO(si?zJ!AqM2l7^)RtS=~*>o z#1atralTle2kT@rMqodIyFr0dy=>L=Z&%gX z4*;v9>IwRDr=fsPAFkSW2=wP_&c6HtTuoVHYbYZ}pT7tkl2m#Xz1NS?W>zR)UF});724;JduJy=xDWwJMjA7Azxsn(@YOHb=>WfQ9vDV905Z zL52p$wO#@{PUoPxO^?6EFt?q!M`v--&CZ$x-3xE(d}yZ>t<+oh3kr0Yyq2aXv3c~0 z@_iOS+J1Uu8!PWL?z!uNda>F1Zk9mtlnMJP81YiV0`lC)g`7(bi-``i3bWE9noRH{ zIYY4iTHbJVJp)SF{^4(K}&^RF+xLeDu+snMAEiJypM2l+E zRvu@9cGsGpofMQ)*W~6-$`^R+r%vmI_XS<5LiQQh*i_bAB@s5zn~C;O8Xl*IpV6WV zH?Z9T%D7XCwY`$?P&Q;MNCWDInICWz1i6cW&^1SigpDFIHT7U%LPPj+*%cR$Qp;z` z)6YQY~3NZSB<6g}+=LZ`aH9PbJ01@Qa8(oqPEpZtNs2kX%frYGQ`?fy1u{ z#Q9Y)bWuV7ozrEJh;E&^pRG0#G#Wwt){{32pHYXUQfo2OGsbbcXJjCSpNX9g%v(3B z`P}03T{s9Tvw6tKlb>6n8dS%McUw)xhrjZ8Sjxdr={aR+FKzfD75QJ^^qJIN*cW{( zi;h0qeA(asDY7BUGG)x+M?~~*9pNV!(3pAv7p$@MR{BOaLWCN^9gjU5#|S-G1f+_7 zI=Qr#$%*2%&^Jr!GQ~;DRB2O|SXv90siQ3C2z-)*2lK%r{Uo@N`A$#skm5bI+>DIH z5UHhKieL!hVEKvL+3D#{S3m{wA6wfQ+^zVP&7{H6KV^_$%2mq{*A5nfESsLb8M!W_ z^F->~Cv0`5jxLGpdAV6asG`?wWf-aMi3PR}iwogCE7IaSqEv|_)HYQqP|7Mn)vb>zbRRy20>mfYt2 zK+F0TP|pEUQvTo@yr^oBa8etooQ%17@#JG$1DDgqOsje=ud z=R5BLwyuw9mtKs9TF-H9xz0>~R%T#|5Jf!Qqj@{zTAKR%ZO1bHho5l${o}mQxF~RY zvC!UoQ?D~BU2)&J1}`I-5BpX89n<_OXo<~iC^Lj_Y9UvppWgEIkF;pm?jdGF`ak3j zu+DA9sh3APIB;1kXiDTQ!l%@gLp>5h@C=&sJqF+C%;c=F2A`kp9(8qwc;YXh0rbq` zFez0_UiX(ysc#*Tm5~YZb~!#gIM_3vGYs&53bNJ64(Ry&n^tN#W>OhRlx4&9U#vh| zz}V3>OFnI5m>(Oyvi-9m-pk*65%*?SyZwL}KgM3ez&HQ>XNsylgSN!Piy6I*T=w=- zGI?26*`8|Hw{NqPXJ*l&L&peix2aHD z`qRDUlx|(>Rw&L-QS*1+3&tau#v{bX!=IitFcdIy``Mn`H7KQ+q?{RhglT1C7yrPV zEK29E%Y|{ET>Gj0wiM2VE8AjKdM}f-zI~wfxyMUi6`S|&Er!kk75ZkulrA#?{xlay z54+4GYsbozj_Oe=-nyw#*X)XnvW&tdyY*LQ7`89zeH+SC%%4&me>%}&TvjGDUwZ7W zZTma6vOw*}iC>_ceadxIkMw|^!73848!}qsunl((#v;S->~12nl$eU74GfhQ%&1Le z8F-_AyWoGn(I+*Z`pc~Lr9grbPT)!)Zr3-m z@CAr+FRHZdT_7W2D$Rr_h!3&m8;-W^!&oBA;$}&buD?vz4~m%XokYMIT;4CPi^;>llpCO zv5d3;@+;&@iap3%R&tV68fMzr+m;d8R71n3MF|E3`P{!!o@?o<)ANM}$(VSupJhcF6Fy_GyBW3v}= zuU8wBX8I?YaD&0L$>YFL%t>D$6aPsw*chx92tDXwzqCp^BtN#Z+L-@J3chP?fiGVi z?=!>x4Uy{X-C%5~S3kPrPr{Me#(eA6x;gAczf7r9XAwO;_kbMIr-#4uU%esDj3ORN zex$ehXcZb3IgnhyZwVTQRbsA}wLN9NucYTaEN?Z!{@~pRxQ2daF8;tC;~-PoQtBKUKc$Z<7)H%_wQFIhy?qL2 zE&G;alt6SFI6T!yDnkP~N^rTkYXjL5p5*mnPhp7EUlwX!JIG8|^b53?H7AM%5<5B7TPB%1ro;{|TE z=1)0C1=DJSUEObxvk#7T_n$Foc2H??lD_B`_r4p)#bgu`6uVKYnk{{v?8U-6ZR@Bi zoa<;au}n-rtu~$rQcK0QboY~UnM{6uj=QjWNDvUg90R5uzR&?Oo+!c&l4v$lY_GZn zXzW&>2t~fz*|ANNAt3>FfCeo~`RsC2UDxdFrw`tJ`XR8)=>m1&8`ph|j%WU; z1j}Y+B=T1H5+SoS^OtRYmq>oEzpLjxMU{kHN5O1><|R_@^b2H3P5K2;)QReOnca z+0Y9gEiTNgMvQ`D`x#|x=5S__Z;TevPKf5xPf1klBeV{M6Zl`>Qh>oZNy1G$%&q_mVwMW++4Np_+pb=LEL#=_TR4U@!COFfunPM2|? zyV(&1V$g1_?-KL7J;XjPUl`78zOHxmlQPf~e%~JD87$3~$_X`8>HYu5v40!A_hQA6mX9cO(eF#i4{TDUWp^jPEMPEu% zqv{O?y-6w1g7B`sWy|1Tnk6Z?`uBf1!-Y6ZFA%T09=(CPLU>DZm#(NClr1Uz&eh}m z`R2}j;+=oxcma4Xa_Fqsz4Bob1AK9t)`gL)N(6rO6IS|cDhm4hIX%g^qI-C=a}@6X z_^)Ib6uQz< zkb8zINTj^TU@==58n=IBnofE+Ge74af;%2jWIq>P8N)!Lk0-fOXm7KDb-fUKc!_v@ zX~`6txS-;Is=_yzsr$$6NzG4eY7RX9vR_mXG2qPGNhp06N(@0|V8ZiyizR!cbdPO! z7h`*Ulh=Ck#b&@;v^Xsu60OslV8xMRN56>rlp!vws_sC~d7pnr1auz#`fvQAmBa?% z1V@UkZ#XLO`A`Y(FOCenTreRXP6CAMFo`^^0Flj#w~dW@U)h*W%eRE#D=}YX^EeT1~ii#icr{{_^TbjSy zEx%Qvpwz9$xp)x;2rsy2AnSpL{`8Lk#G1Nz%wO{x&0TlCOMED*#z)$98S1E52;dJ)W6Gj@HA0-a+ZkfUk zE*==`QiZPGr^-Hv!eGF@7Z6Z!Iso#$T858Jdaw}8QGt&hk_a0lUIr@+V56Wtz~sKq z|MB|YKX!g2Iws90%Lmw_5>GR8GZCBTozUq`hXp(m>~`Cq2nDM%Wo0r{yVCC;^tQCM zwTTHB)i%|8?tehWJ&{P-h9160y$2LX|cpJ-${g{A1$-i=V|@m#s>>d%Sc?Wq0@J;al#W#8yK!0 zn$q77Fq$B|Ik`A-#!Z-7kR&2L>rlwzbcf)A?tJKolqsTg`(3FI?LlqES%N0?92OcH zjgnCCD-Cno=h~V_VdyDT!dOp(-=(xA-{;?3s{83;_FI^c2lI4qhIaC1<)Nt0PW(HMrz z0nh#F|2?)vj#uh}nXsaep`U(QNS^qFk}f`{uYMZaU|YBS@>?WgyP+f=8by%d+Su)| zao~xkbb)gvvL($AeT!3&qLri?{40Rgy1ct$sclSYY}Uig^2|l_Yu#|X_q~wsAJICz zM$}+r*XjvaRY%_r70Vi;S39NMgJ1ie%ZDVtibpSKjm?06QE9c{FQ&)$k7(5v`ezXZ zE(n{O{~P#%L*p}i!OY`$sLLc3R=`~*{%75xJ%{+cmDB99l%0=Abg)shx(7UM^?%wb z^_21C$Xw%)erVvKROCY?xbTy|tk>k4uXKq2kltJvtHHA-%=x5Pr^b671GMdLgeb9A z_j5$!QWPv3ez|;|MxwVdW`7Jq*Sb?wRIstLcBYesLL%89Yx(d52-<31)^D{?ry4xI ziw);vC|NmPB2%{T@=RL#b-$2@WOZ~I=PFb);J`|b_l+VEB-)oQdEq|)o-++d5q^Ij;_9jK+F8*Y#w5a4zW&GM=WT>_W_2g#{dn*sa9 zP1V({R11*LXpBMhD1wxAOu)S`$qY)goxtctAn`uXtym102P&rd)$}1~d zcnkm??fQ`w!fE~Pss@|}nYg-b_j=*@ir`yHT1uvRW<(@J3sZzLS>rou9H3sA6s<*(*1;x3GxW>-y5 zNv!1HW#>O^U)d~}eEr_kdZ zeY`<9`x1oI{bXO-S8q}3tkb4mJQF+vp{9Ju1S%ZQ^vv|BcGq{;LM#M!?EE|3Yt|vG zY^4PfOwYaiqK=g(5bP<6eyMgN)X;MLAT+!8Vx}n8QN}Mm+dc|4=d>|pkXj_>hZ?+k zF!KJO+&V3Atf{j138QrTs^|Lv^cBG`Z3@dIF-Ru%8N%0kvc@KxJD`P9*@}pYQn~bQ zJ19UvbZ_sRh!&qMzraw_psr1i4W`%B!pfOu8ww(jQh0g=!7fi69jU0GXrmy!`l+1& zNKRT>7Vunf<`KO z1q!u0-i8qbR%n2kHHmMW|2pA|ct#xA4;VDkjJ9c-&7Va#3+`bS6MaQS5?=jg2S;eEgUiJE{McY4tUXdU@Bs+cyA;mu z&X+S?x68R}>jWEi3wO3FT`s_tRc{yQdrrtyQ{ep0PBC19&;3Q!|1db$ppJ*d{rsZ) z!4tEyyUYub0p_s^*DP?7j-y)m9 zpe5k?CNx)L;cL|7;wnE>ZCmGZhygTaKWIakFyZOI=SuOg(o$6W>!<`+kqcoez|Oj& z|IZ)i5%O9}&J)|?bnbMF_obwL_jcBn2nS?R`lYDDGy`~;BU%7|_D&d+la(_so7V7& ziqADxY)_GwWqhzfB$~=@`HpN+b!*fJ2?X>$PXGHk1)mk*8?#)X+4goxqJ#^#s}?$cU>!WJhxOe1Dfl|Mfe6q8w*gbbJe+jqKkaQ_yORjY z2H`?`7g9aQWf3nVM@O)EVe@F@iGq{X;|K&7R#XyWA+n$v_c zJ1)izom@GuoyWZ2b@C(KYN*AN0D}3%N|q0o@tIhaV}Q!!!qjQHqL4kaAVqUalPm)L z7FGS_LF7VDpTpj6AQR$aSa2#I!-J)`7~WDJF}N6-fMH-#KULM5KHsY_<6GU=+s9&h ztd_=++t`!;yz`*~L$4$X{Gz-&!l6>@4L6}t=Jie-@mOBEoQ%HAeH;%LFIq)8#6V7h zg?yUrHulE$4jKlrBv|-6vr01ff7ewV2niDS5%M1!Jh}etCqKHo{3d^ zAkMrVj=aTf%y`rK6;O&vWo2Rcsix}V)7aS*uk_Oc;U7$taRTAoWBd<1gdi}ytm!SM+U!^DU(OJ%zW~e`MYk5=egW{%&$MxpFwJB zY@Crz%g$c9y?w2*U818hcF0WxgP6ampZN=A#d9UKLikisbCJ!{;jIF5e;ZmA3EK;@ zD5a#@kcMVkR)t^!;ilJ`#YK$cN;!%=0tzrUKfn9zY8o}My=@Bv;t^6__7<_W8v z-rUAT{K(%yaC|T)z4W@RfO4X^X@*0FWQse|dwXX;ii8j;l}du;ed~ej9S#P@`x4$& z>5G~LalP!xB+WT@yGzmk#Ld!2*!op$BUQ0X3$T6PG@Q6^c>fYZ-kFG z4t(?>o6$2Ya%6h>YHKm`pE2!6K-GYz*_;?+11nCMQ*bZPFFK-lWptGePOP9Y9vHa8 zfrnI44vQclzO{}N-N&OeSl`}3N_zkzn9ChP!V^6`9a=UM!*M8O@y6ZM#Vt&rq@l&p zH`9SkXpE|fh5{NwdIBzMON^fdr^e$mROig1u!UCid$NMP^Qh zw4}Jp5Bo@#bGZ1R|FwQ;MVScrhAw>BDWvA zNN7ODBjsH&O?az$X5+h5HVr4Ue-6wVluV+^7wONIqBBKiF8atP^nP$<-|TbaZPWL-$aIbT}`T)HJP? z_F>u=KC&J4nmf=jV~w>{DnaZ$EJ@BuwNYI7V7>y{Vd$3i4zGHW6B=Y_1ozUjKp(_} zMk@f8rEc@bT((rk&PcTb1z%?CQv@40tyCCrVb8mF;(C`JDC< zVy1a^G;xhM=T^2Ul)E)>q&JfGZ|G!zk%snYR8^}8hVYrln`Y9(nZA`V+dCZq$On^x z&8(^(KR$SW6m<=TY>t@t#*JTe-5-vkm3`i-z+jWLg!39kp?~a8yXfv%f32~z)I08A zmskt|I0T!b$v*uv1lEc9P_d&sx@MQha|jhbhBfN3LCGf9Rax!bF1;0+U&?HgQqsD;oa;ic)aobr1W<0A-CDr) z`dDX6pr~Vj>6uttd1#eUG|)FDzW&-cpzPp4N?4@mk@-WlU%f{A_)p_sslBUi+kM9S z^v~p-uVf&|dQzA|7m#Tb5>nod^K!#dEHxr#YxBz@2>SvH>`o|jLXHue4Vm~dHOGdL0};?yIa}rp$=pH0W^oq1BD3iuyd~D8LBBO<31O5yp}82lUn#rD!A73-LdoyP9n$W-JG}McPsOVbn$&d;;BH9BvZte!+~z1!D2DzmSIDvmYhaC-;EakQK>z9 zbLWFgMb+iW`5p^XtIHKvp%#eHt09ET%B~@>_r~4vtM4RcT7-6WUl9r>-7Xh zaQ<2QEMtGYGZc>eXVC6|YkW3dU|65`OY?5*QY8V$G<8#iL^OL>s83_>sPp>z24v*` zFNR$PPNy_dv01mCdyxHl@8R$0GVr@O^d*-2N_wwZXlptG2!_y^FgtEdb>gMId3BD# z#iHgzr!*5;ssRCCoIpfN@#i8kE)$Ba#+UHUk{2$1CYregs8&$GL)^gh(tT00hrZ0M zhrl~49MaHexXNegskfGaqt-i-x?^{Jv2JGL3twDx+8PK=$crS-T17nL&?>@BZ7sI9 znOInY_@9u_Sl?y}Nd$U$tzdHu?&Vy#|@%1;r5*!5hiOTZ}no z4@d$B`n0?JFT*%(tFj|#^d}QBXX~XV+S=}s@z`a;kqJ6cS1~-FP+Y+v7DepX7Dr|& z3YDw;>NVY;%8fBJ5d3dCUjGW1(0j_q`G?J*C!gtONa6*9`MdRZ@LLUM#f8H9(SKW` z$4Xr%i}qprghb~_9a^XB|2$zq(0(x;DqG)rMg-G=+$31PSw0k%gl5+sQ<{9-tIHG{ z_rM$#7dQ5~GxTGGrAl@A?$@ahB_)M#>U^XEFCvQrMq}P}+z($nnIfEhhs9~{U(fb; zf2rY*{*p=De$=!wXjdkJ>!>@FKUIE4p;p&Xq+N%vaYKc-qjxi9j@2Pr)6b`f-C25s(&juz4EG?=q<`lXsPU1hl7Qvv~% zCh9!g>1K|Aw1vc+)cgv|cO;oco&cbB_c8UYt+&e81bR;b7YxpFYfE6$6`Te^t!Q(5 z2R7vg;QcQ@w9=fKL8pP9sEm2reA2tWjsj;zShR?i9jxn=iE8)7fpbf$PA|CDUiJEG zHKodUQziaDso*Gap|^ZRCH?6@Nr}vh^k}aMVt`)meoUhBhfa-rHQJP#_aU3w24IZbIr z098Cm@FwFsI{sgcNRVo?mqN!^snQu7oX*O}U+E##Fk&2jwJ_o{!h8GVt9Bok-NBW! zPg@z2ZK}i!6R1n}T|pMO_qDof1P8icIoak=lLY^b%JXnrUPjitsf5DlHb@Hsw;k1! z|E}qn6!}Js75_u{U%}GhM*gq&vk!}fay7pqKE}uP<)~fz4|?{=hLv=|*`|IKxAO@? zA|lwGeHf;ZKQPao<0}EXzDQ8KGy+OpC7A^mZvW9FT%XUk-M*LT-{VkAa)lphz{Q(x zc?nxzKbimi=uD8~S;N)ovLrn#HYcZeX4cIoe+sohlTqPta0$dW-t#7{j=!t=HUTKq zdG=SY44h0lFq{7DBthem%p9f~!2Aim60ajx83_BYrGeYnT=UwUW=m1U_!YFF{n`q! zd7;M$BzJvTWA#=ZJ?uYr2WU;6{UwOc^&$$`lyC<(qP){`edF#e>~$kM&18|<{zBNG-3h%UH?tP&!=K`VdmY4!wu`@ z;Wc-ikfdLwQyrrk70rN1ZtVvEu?^$?61Vw+SV(bv(`&mMkN6XgYf<~VUQ?$rFsnQ+ zgRrA|9j72g=|}thYwySnSxGBP-sa*EjKw!?efaQWRqZmF5Wr3SeJV=ax(W0y^I7ca zn5b)Pj~%=jHgO<%NmhC(h?u;1Gf9T`;&y}7m zZMe6CVn%NS1><5k416BldEmMlvS|;nej=hpKR@4Gt!g(gFTMShmE$gY4=0w-HO`w! zs~q@yEw)_T?=i?X1_`qU%=oy@2a)2Ptg`6MuQOMxj+TFgbiV2p<-+>ci=CYjA0%NV zW}%-u!}$l@%gK(uoKxCAZdz?Y;&~O@{@1uIO0Tx%R}%)@UNzx>2!f)oPv>Jn42Dm*gj)f+HnL zllE#V33d2t1+Y-F8{4l2FP?kgqucP()}t2dkwe`hG*ZCvHBJc8p`Y?tfBo^}*?yA* zEZ94V^sJrq;mlN!(d(O;k>C)OOui!oJMedzNagWor_~*TDVh#6W|nJ@5a9gVnGGO* z@a<8M9QV!Z?xhF2(RuEpCBmIAcG1zbHjX4q?CgZzW1d#qkDsm|+kX%~7gVAzNl1x% zPKhv``P^IXv+m>1S0HVYRZ(%={FV>%Enw2)y+q?~!=a(Uo$|NJs;aEyfVzCCJdyvV zHgD$27T|5-9i7f5(_sT2EyXib9wNri#S7=4wzPfqf0}pDNODeI?;d$Wiv4|hdVYr& zr!5(>2w0C!H0$@y?d_sE|_cb$kpeBghDMy5I#}fh-E743`6B3IjowJ3N@_%Y!DcN(4pb%Gha=Pa)&{`!hR+C!)tVcn=mk-a%&8& zS(4hHTti(xSv$~};tq>ZSMq(2uU}#A5eX*El-Y6 zgyStcx`(lU`OhchCOMA~$`V>UcwG z2XKhS99B|*LZJCkw@$=g{b1ROpPzrW8r~yd^m#2@dH`FGzZXN4nj**3Ctaz%6SXEx z@f@c2`q788T8@|^l^Ha*j%AwHenm$ig%OzE-XtuqHhZ`=T{B8b6!r1JvHT~AUK7j| ztt~Ci%{>cwsH56_YeUYwMiaA{R~8Mb)_?QpN$qT2zO=Ej(qwU$?pRgkP7yZE(N{7q zvuEz7+#*}DV$XHVUv}_t zYNbe5d*WEhwd#9K&7;m7c7tV@d*o#~c+}#_mHeHqD&^^FsxQf{TSc>n;G0#!wJI73Sf|K!rSs?$s#bQey3cSJn zxm1;#lfIaC^EQmY4_RE|Y;R|R77HxhFZO#{5S0P7v_;4e#o;I+CN_^oFX%K?tR&y( zbW$HNI?4|_H&?cr^LFFJe8{_25N z&Phpf@^p95sWT^22@3VKW!QOe6Q3ejY=yL&rJ_LYG+THBkdYJeTfl<8$Dbf{h#H%1in?(#&7VkUtnH?C9y~x%nK>A*mQk*$g?U zZIKJVQP;swRuTou-k2HSwJRL+$M!-0gG*20FTt3PU9z z7>LHZP?r>ATw5R{brs*Umg1L7mDW8#osQ(Y_D00hha2h}WUC!cZRm;~U%cR~6v)ZN zB~a>eZL3)gu9n>DlkB`awf)XC82-m@$e*vjMUBzjHArG=4uhuRU3LY@g6#LZ>bbSt zQ_^aGP9;)1I5=Te<_G1L4Lt%% zyWxEyU>#yem5y(6LAmUQgsa`!Swz|8ek}SC$iK4<3FK#JYwYxEN>{qAXwvlOgD1W% z=gOyN&S7I+)b?`1nqQ3a^YgD^R~^qT`H~7a&)T@To#a)Yb}p&jVpOj&wVLcMPF)MG zlyv&ITJO|ZRzSMscG4tx=Q9i#X^3}Yb#HI=^}TM7)uE*2?0H6jf;sf(M-(sG;DY$O zaCdpAi39C=n#}kL6CigfQ(j#=eD@Bw6Jl|sHZ7cR{OWbat@pdAC<=-StZb|nQ#Gn^ zaNAtwQBQ5O12fE5D=$M&RlRaHIuAN&?3B8vWBgV04VdH)Ke0U;gTn**_3Pe7%fD#b zx?vJ!!2iYGdq*|3wcEp%qaG_F3eq(SsPrNoqSCwc4odH#gkGW|f>h}pq)Q2rUPDBr zH|aI>UZsT+NPY{?Iq!Y%9rxaUzcIdX8J;mXnn3p6YdvMoXU$0|MA#vE10N-Vfcs3D zromc$?81K$bpG(;RL~iw`R5^b-?H@Y%7J{86WoVj zwEI&wjh?-3X&IS1V6l`ag)GV6P3Nsje6kICAaVGK&O2E*-}taJ*D{XLsy9P(KXOzR zCNn%@cOL;a9YR&{gE_~;n$6>!%*-FXz1tXi1V96!_!&7mCFs+qrUS&fZ?;d*OkY2b zbvPlkOF^EX`9^D`O)|BXRu-D(9Y0!tJPCFZZ1IwA^9OK@>7A7$6Iep=5qEARc7}%5?IMWoVh!=0G9_tWy#CQP_6z{$;je7(G?tfxwfBUA#^f0`d(F%Gp6(o2#uELJ{OWi(iKZ%_;Tzh=wj)%ewJDw%hYr+Uw_5%575f} zBkvr#_iAEAhkJsdu^9MdRDh5Bd+Siq{j+r}n|TB@SIn$s#7|4@Aa=~C$6-~8Ik1*9 zRVw31&clyZsr0@OT9{gD3I2#E8-Uk{!{=pp-@NrzBhM?6Ww(l~$$*TFjDP}!RJjAq z@83q-IdxymQ~7F!-&MEkAb}6;isb!H;Bj3k%tHAlM)!9(l%vkI$-~`=NVB7r_1(=1lUF$)7sPABwI(iTiXrBS6x#YeJMss3ylz z#=@dSI5i?RacF@8*!F@u4UWn`X$K(t9M7I+i+3ilSfIwr1WFH5N zbn@uw&ftSFmJ$6@MvFG2g9dfl*-rPQ^9 z$m1zAUAz=>2&-`(Kr^EcVas~gj)9crfpPODwgP%8rdu^G58m`IF^J{ZO05}Q z1zw`pu3z88Mh}Q^2@DPo|Ma-Ovp_{k`RgztQzC@sL6%H4C=|+xxYbfh?hEIp%~Mex z9zDk(wbHdR5YS#0+%<8_^Z^0g6r~BDm;XBnT}FStLZbU>jNzzPS5^r?>yX0b>vzjM z0>$}U3v~N>vB)evHcfYrBIIVvC=K0fAQ&uv_0{GShwDTna9j5Tg3(}C07Iw}x>j{s zSku$TG09Ij&?3$Kc1;6947sj))nyvEm~0f@MSk5Wi#JRz#|z&%gs~e-7h(Qcu@Co{h!bWJ|xDWS)8SjeQ)jll6FRGLSc*(NokBtbrEKkm4 z5zRcN`vfaFxvN2qc_@^ef{f(;z0)36PJJSXXNCGMd{s53WU=Exy7kbl7Rd_B-MIzgkOI@Nra&BNxjTmf^VN}Xory#S0)(2S=w9FV{@0qL|s?9#rusVQNy zX4BKd9St0{fb)h-*j$8>UN&!uoWs!2&zF-&O3h&>-oUGht+0?lI19wz?x&rs0qL>5 zy;M(v1el8uPX7WdC}wrtJI0E5H#q3Y{Z?{mRWr(>c3+Bzj3z{^riC#-NeQr21w5CT zWD}4&jcT0(20C#-a49Q@LXiJO5nJj3Jg*Ke^|lAJx|x)l2ajba5`rcscAZy4Mb0gP z#QT~j_cT1L47$iW)Q>OpC0w*!M^gO`)Iv`j@ttm$oaBde;_Pgrc!ak^xGFAN96BEx z$X}wQPV_ozj0G8Qx%uE2VaTdo`!eKgjKia|9Lo;f?LY<=m=9nDEI)>5b<~c-m%jN# zw<|y#om^oUIccS^zLkL&eG}2$RRXS);-Gn1s-wx!;J$K`)Tw&qvm(;S+8Y1a4`^Rx zW@<&2!g-scu&r%^aC<1}I<{o>$Cj};k9Si6fZAM}!@Vs+!Lf6$p>Y?S%kG=U{Q+Mx zD-Y_Ud5nS3h}giHTERTo#Y$LYJGDaCX29HM3#9K9&hKvjAjZS6F!br z53W2XL9cFfRhuur-}!Xm4FUb{BMo-*UF3%nqKS z^ms|t<662s{V8QT5op!adw?4j)XSe?L?Kzf@jU%kx zShUpLm-Dw^YF=TXnJ7Sr@{U_4p`TA!c_%$DivP^$JIAa)Lc!(qJ(xzI`oW3D%6wel zQMDk%XyM@WzTUbus$_aOsBwk5H!PqRhE)kx$TZLqOf)9hUT$W-y1nyp3A+z1t#Do1 zet`c;1LVm0U=#P!-@LikxJq#a^tAGZ&x~gFLTxtwO+~e?_p_3|Za97Eo$Jnq$bLnk z>=W8mw@}Moos|S2rsDDKOv2I8Q3tb>pWg*qnmEsf>eRTgQA+OApN=o8!J-U3mr-(h zmkX{YZn-GR0F2@%qxGR)?kt_hNQNY%tL@WbX9B1(v118?M@5FrV?L(wBWQzoN-vM zyJ0)%D55AUtmZFmCJQ?p`)>~3>MH@2XS~XJljqd!Kf^RT0T0YL(umrU{y7il9bu z_-@Pn&c5DUnv6>%7x%h|Xokqv_b;Y%Qi-VcRIUx_&=c=D4$`ed&}y=%9QH++$8yt$ z(g}>$vkD3dkSgozC(oEpZ+vMX84Y9vG1MxJwvQ0%EZodv$?DuWFw87&V*J1kOTy(c zj$MGT@s|5y$`O+W*$7;2qul!!X}n~;44rO#>YUgD_Ua^K{ej>LglzAK%Qf3#-tK6@ z!UwS;$(?g-f35fLE^EU*55)WV9`Stt^f6U-abyn9_kd7OOrk zDI_GWxsT-9wWHVTg5D`UcwNxbSBFPQi_bPmDI{VIL2L*EX^u`X%)(p~4Eh~x1L9}n=)tAq7Q9IjfN*EFE#c=f5IB?>okkL98&7~M)+HOt-9Xb6S;CdSGiY8X9tOEa##6(@- zQUJUXfd}btJ-zvb1sbSJl%TLyUsK3x+y-*cwYDGK_uch5_k@DFJl|>%+hQD5XC)N~ zlD5rsJG|QMi?Xys;q57>k`=#G^{8btFR?hSyga7S$QyWGDrYqR`Ns>O1!*+*Wnj{W z`1yKnQQ$@eZ~+Ogu$fTP*Ow4a^)9jk#sUBKBX5k9+z-b5@P9Ci}}g#9W2)YH8PcJh$^0q+@r zS-!+{lC1uzvTv#ukCx6jw%C;Z#m znE(*&1zS?GjcA{T)e+h}=?iE$9pS59iZU`2HRiDk0@(lqcg1!?PjL$+dJHXMLxaVz z+D-Yzi`s>JAn`=f&1SfTC9zf)WHa=fXNbPU)-NXqf+ekP#c?k?i!e(qv0%i>$k+Mv6}D9N5=Ui2L^mEB>1pG2}&8M+awT2WML+ua3B- zcBl^#xo}#(S)}V)XMyHzeN(I1Nj`E=B_}E>sutH4l!_>7>+cBu`20zGYnRXb{9>XY z_v{7`WTjy97_F~&=&EG`l2}pzGv7AEONlrA(D(dVEkqpO0>G)|;g6i;C)tU=(1ZwqncR59Q{#xY^z%|o>5(Rt0gt>X?yTV>j-#-7XA>ouKf7F3-uj+)1HNmOY3a!erM=rBe`;AkdNchG<%c^J>F#U5=FH3nx-OfMT58gQ|?N(J#8hHZG>?A6|JLC&6UbfJ>02`#FyVa zv5t@na54e*^MG*~heCSC#{tbD$j0k3`yrK;g{8*+4i*+!Q|9*(L&ax4Seir54tT0p zW@ZNJ>dxSe4b^3V@t+bm6uxE8vIY-QWw8lwDOaGadlh0usLUMAVeUW5%L5lm^YXZG zvhrbz!x`|2O*?kXwjj6Qcr)?j9+X{a9yNqwINqJz2Xt@mmmcFn{UD-KvkQ&vo5IHb zdiwY;PW>6`6on5HJxmJ7R!9K`F}~N%u2Vx_&-gQMiuP>xkxZTxTb|$k;|x6VAIY}` zj=$;nH&S7-6(pns0|WDmi}(3HOYL62p&T5(w@5!0e*esw_iq57`1{KuUiu*w{nNLn zLd{uMjn}@-KCaxvzNrsg$ACBA7dvz2Pp5w*8lLUD1NR<~&e#q3U4=x@AlP;e?vVWH zHr=QRGE!o~Lx5Zdu)L#tZDt09ilxPY1hr5(@x!&VX8^$e_b>dc``=3Do!9=O-w8zl z+%KTzL=9C{aII(V$$R|sE^OP+pZ33gzHaz%-POUNq_{k$eO|)PkCQXDf=ICTq5D0D z@OgrNne(sFhQEIqxX%Bqg7>eF@%sg*N__wN`j3xz`d6Sf{MWyLZ~nhm&+| z4ITOMGB6i_!vIjP38>bB*@EZU&Z!$MbD&G&^dIhPzbX(+<5}=z+t}csIV8*|ViTCD zZk5%Y5SYwZ8&C6&B;`NX%{qJUD!97O?ZD4v74R>wMNwLHyR!1|L_=yW_yJ(mH4K0x zM=ub()KT%6mF%7Ny_BB8o?CG1FHT?`=DizmKRP;^|DRK@fbM>l15733FPQ--{6pzI z);H(QBhd%P+h0FDTbPXR%b^PqzfQ}WLEdPx0eB0e;n@@}ZE)0`?1V(Ah_-iU}00YPYq9_az4Z|EB4cY1j z03A3!oQ3!Qtdj547e}XO$IOp* z#jGLT#?E&#td?J8VZ$r0B-P z#>&}VKR)U2>wdoFR_3iCYK# zigfM}ti60vUE`kwJvd^IFp=U6aKy zPm<9(g&bTzP`($j_;nMc5}Ay-qdn`@p%eH{QwT4nV|H07QPdcG7|=ewNPDf|>?|w0 zJt;*(FXE`h7Mt(o{-Z@1)Q*7x3*E`lF$sCcOy=gXP}g;abfV7zq5eF+)J3nKnZs%^ zuB1dxeD2NHUj!Ey7xEYlVLB90sRd4<0MFlF$Fc&2>(-*)h~7|o3To;t)u7bV@NZ|Q ztge2Cg1Y22wi{&pN#X%FDXU>MKkz<{3@qrR&FF$pS|fj1_das|39vQd4z5O=$9;yM zSuX!VPzsLZ&)a_J$s&AF5Z=Kk6t(Eq<~KE(b{H-E^s zynpl7rP9-_P`{$Sr6ukV6!U3-hEHLT- zGYPVFKIX)yzIG%eB%}>WO+JAb7_z>0?b?#TfFv$2Ysyn6L(1J5iK zMk%-N`uOMo7te3m29&|ofE#p~SjYFhOMqvOp?_oC*0c^8to*e$qG=B0FI!nVv1M}5B?WMH%Vs|n_I`D&+ zPY`bG?8Ht?+~?!F6if3T4?uD8od3?i3QSH`R`lcmqd9A|x=LMJi;iVAynpNC?a9lR zFPjRMMA2IU)SL!PB;nR~k(dp87ngYM%~nHR0v`7UY-X@yZC{<5Q4v6E!j=Rb>V0v_ zSR;0jjomD^=7@h8U7IX~tc{f3Bu(rjOy&)M*@cfwU`POSQvDQo;;E{ZAd|66oW_gL zD*Lul%2fp6s6bkWk2z0OU8~q3P z^A~BmO|JKC1NFy#pmy6UlqzOJJ+{W`eo=* zuI*tU#fFKgciBqN>d-B)oFzl(@H6eGg!p*G;Q*hLS*L|n1iV!vMIWbUr`VOp4pj=k zoElO%_AkX6xOak+%L6MakHQe{O&UhaUGyYq5DJ5!hr z26$I+)5}z$^XuykUXNHsx0BJU%rUeg z9$H(;|H;&O@83+Fqhg`|&VY*I_Is+UtB;O=1869EduHc!O9BH>p6tZs|IM%Yg)QAv z#xvhecjQ6YPT89`XCM)+CP0eZGZPNf2L1;dXDQS7Ar8u9!ouhP3sCn$Pk!k?e-0#m zoHR5RKq)%~1};u>*ssr@rzTX8K=YEeyXb$jn65imBZJ%;8~yNpx6hvWDg`TO#qXzA zOnP|(N_1dA{rdIm=G{p?hgpkvNW%7|-}Y>WI$P`XTSxV4Xlm|{7c+47#Da51AUn7c zs(c@F0Bj9R3NGg%d499wtzh^F{BwTZ6W5cyMf23-*Z{!snRR)jyAHG|0s}SQizHc| zC~zhS5by()S`aEO8@C*vw(sAKR(9h%7TaUN3C6#tYERj$b3#IRusw3Qa)uAYa0O)*r8?T}t zu?AP|kl$36ao9?L)mfpjz&sDQVO#8c0kw;Uy85kf#Iw&v(=oP~X3$0hgM>ha4#UVu zn9IN`#^f^dp{|4ks(FSSC8C(?q>sI-fvBC{ZBk&QZ8KJ_Juh{;`#3Zx;iqn1^yr1` zS%JY&z4Ef z;sw#kFtAKaC#%H70Yulqa!zwB!C|*8dg;`WDW>#hcv;!?K6TgQgGr!-Qf=Q4Ldva~ zS^sOaNyp>X;`{y9e`RhN3W0_8?=Mh*S~Pbi z$`B_fygxKMBww1uP}Brna4A|s?@OM$_vg)r^@<;(ekkfh4Y0I^GgLebO+Iiw7W^2= zr@1ZsxZJ3@*kmL|qE&;P@$tu7>ch-uZWY{nb`$NpgvQWIzJGS+kGHnN7l;@+dW{C! z4gsgOlz7jDq+HBr;c0$aK`g~8H6DNI5xtd_6{8O_yL$BPHUyIP`gNPXHJI&FwQs|I zIeh$>FDA}lZEmiAaByXDk%Ekj8A3;w(ssRA``X>ovhud~?mBZ#=@&AnuiM#qpbf%sOQ%j5_FD<&Z*&|QP1_bm&HpjjNFWaQGSvvqQ->*{=N^gl5- znO}JdaRkG)MeY9J!;w=M-U%Pb&w0wM?CR=jD$yu~7d@CT^j?Hvh(jfNfq!+hpk#Ob z9J8=s>9b=_+@pup5?0`K%QM@JA;`MA37N%mm(R5U0bo+V%YiIMZjI&DXYmV9+S)1D zTgk&IQV<*d(4(!0tdXI@JIqDz!^6X!4i8JBTc*P;Me^2GRy;E7;~5-ViA0`&~sMS=&mcJVaQD zV-Rf5rIDSuxQOsFr7-@hy#Z~RnWiCv*l%co-byOyGOg~ez`=zvRx1`1%K{J{YzZD+S69a{G6G5O&`{G+NiT4D)h!82t*Vsw zp;pf277@Yr*^aG@*kF0Ta40G&l8EjG1SAQ?64!H5g}s(u0e!e7c`nI}o{RHF=nrd^ zm$~1bK2t^!s9>_)c-7OAj|w_GhICmqo*QMdq}tv!rXk76&T=KOoWFi=41mG$7xqS2 zw*_~T_8B;q5Snyi>+9>uh)?++{y1HxU;?4df4Gt#|2Q@+EiH(mD@a<_O2v5@&QGnpqRo0 zk$tNe6&uS8X^%y{%pbVM`uo{~d5F-}F!s$`q%c@2=6Dqozk79KgRbo)TNw`>WrAGg z=31QeGJMR*Y3agb!f~G+vb~az3J6HW5cdfSFs6-LSW`J*?KjCK)9v>MIc=Tv#i-|B3Z+L74Z=%~-_gHSk;dn_8p~S68-HLU4 zU^nOBpj~G)Pdwfpu}f4Eg9cqLvGN09m@B^i)xq8lMn9@dE2q!h&20j+lMmv*l6WQ% zHQ!I2EK?(Ma&S1@bHeD?n&c|xIJxc*I}p2w`}71E@s$;;Nq2nbV;8{lC`AqM6P8nn zPX!s9T3cI{b+U@zJXcaus-KsO^tsQ@Bvtn(Yr*TkDylcMz4lkKH=)GYNw6vDJ?47A z0+K&Ev$`@?fR~$)XJe~<_id)&17BLjS7rAfdX?> zrjdW*k2qc|8soFonzT1Ay2QCxxCLIevj~p=g19PGK`-R;P#E&UZp-7X0Mm$v`|{={ zv4-AjGnRR1xABg|i@*FXd zdnd%60pxzJ_t)rz~cdx3#5V0^X?-Lp}c1SN4dooLZ zV*mWeI*}Kq|Mebrs9OoC5gQXz3x2#13@zX=txdUe@Jm%mbnDlHur4K;JshrRZFl6* zyxWVEgP&mfZR(&WgGA)`%To>Eaw`L1Xh837v8cI$c-5)zDkUYcdi?_qhf7aqrr#Xf zpT6W7?H}4)1z(;~MQj~_=!koFVOfwOwyEiZu~ca7uK#3J6{R`fUwe^urx}C$nGq&Z zWW#S! zih^bXv*)#|VLJO>yX<@1;N$p(j4)AQS2ki%Ge#83(Ad~mRtKbW)5O?Mrd=Z^40)?Q z(jFX_i~}33I)aInPC|Y@C}1{#xh#0We%lP0s^^v~J2?ODvsdqLJUgSWr$>{nernf~ zd&_)#JNrP#z^hRsy{4AvzJTdY>eMYUvsF_IMm+oW>zBQ^K6bf}$%TM4nKQ2nuiOZu2#@$)@;$xz@4q|sK>6oeFnsr~Z~udUI@QVj z=db_o#s7CV{+*oszq|2&cLT(R|BD;{{|t;@>8TXYo|)3P6``_O2dfFFJ?brC?HoeF zqv8(cJo(mdhw~X6cJ4I^D&pZb6I&K;CLP19oeCkS^`kOV*CHkIT#hk%HwZsZdU^IX zcE~Vkfh}!X`6^fO`FAhif)vrR3=nh2&}9DY$F}rewV{#*H196cvRL30#MMVb=)##UBjV8q1tpOrO^q zo?rOG?;&H3-+KwlYm!W5s+#6fy4G?s4)tZ}?P#=!6IZ)naywrg{sC$3qnh%4EL7#v zH)n>sd%x!zqrQ=@uEdv|l7$5w-6@o1xD*(@tt%OjJ? z8ikNOF)^{@fQGlSfbT~cR0wbi*se*ED8WdSHh;CmeGP~U!_`^hYlm%f;Qot#KG#Yq zJgN73aA(0-#J4V5=yB`F_-xXpGbLGHr@UU*A39K{9&Jntt-@=6vbr2ct=2|eBYj-y zR-TiSNCq*SDBc|{o>ewaF2tZV!DEfczp!qL1l{Qnl!J|v;r`!Fk8?ESO@e@Q&8s+byNtbM%C|Rfj{m! zp)7i(@@ub*&d!TTc~VPU^e^GCuDZp_@47J8<4YfA2P;6?7NF3dQpr-IzH6U5BMBt* zfZU5f6ZD>&fsUE9MP_{`zp!xs;HcNSRsA71)1`|STdumFm5{0Gm{I1TQds%k2yra? zW#p;LoW-l527eb2rO!+gLLNLYG3fZKZ@=+Iwv)1G@HG-u6F5bv#NtdueAx?e9o>Xb z+EJ(_bGwLp+{)QEcH@xK#Q}<_?3-k%Uf@wfwwstS%Nr|Nab?0>g7ux9zzkhWQ`P-w zuHK=K>fq8Jn_hFd4hWOZ>Q3uHTe=$4Az142ngxB01b}69t3+9iTZbd|c1X5&NMsXY z>lTwWN5^y+8I|&#eU4C|jmin7Z4q%Eo-*rb(IF)#T{qiZTCqW)@+onx44WHBw}%6(hFrhurJDU~y&m=!t!kSA4^sENEOXril#;xpn*8?5zGNuTnzl#!4PVIJZDEiviorR0UUAR~N)+`zpQ< z#Q|~LT)1R36VYUsi_G=f-=oMOkkVVXpBPf(F@MFd+<54WtJ@eK>ubpjsiv7b)5=92 zXk9~?lDes7*GDnQ41bkp2p?-rXVZ8AtY7B`d!xv*;sL6VfF!7=)p+*|a-%suiDE36 zbunT&>~+)SYd7Q)Qt4=J74lUI2;?RvQe9z7L3>Si{CfX+AvhsUAPbstWAOc2cd2@U zOZ=&4S7czb6M)!_3laBa>{qMwL927;XD$;fWT z#z7!x5$qk)#v5u;T#=X6qk{sAb}v@g`|7%x$a|RK@S7*V?#W?0F~>nBD5g+DRw~|A z^r^Rlm&!^?g`u6zmqc*fl~+IYP)a#h4P-Wn%-YgQ%VK7Er?$qP5Q9Qs^VHW)Av*OJ3T^wdWUA>#x3b;jC+0o~Us}=95)a-^t1TUlX)fy=R-y4! zpQa3c-6ULFpzqloQ~CbTai>$1_~w-pCI%uy>H0)%#BEt|kv$*3HiCoonGscq#DFHMGCY$qD&9e_?@4pUj zn_8YZ92ZOV`D{Z&MZ?0vKM6-qzm-bGZR7Mm6+PVV57#}pz*N@Pgd}lxb)h*nfjO%m zetU*VHJq9;wRRSyxSH~QnWnL{mPNlxky0>K)Zy!wsTZ2&H98p%+#Uz`ON|WKS}XOG znwlrT3!^Jl-H$_L5iqJy-AbnLqa zf6SHku;OlV$1Tff7G*qBlIAl+jhtz$KIqEXqy=MBU60%S>hq@a7oPeZO0jn)X2e_E8@AbW>gtPT$pWlxU(oAU;5X`Vq z#C(4wI*MOdT3ja)CZ(a`Y^29%z}LfzlqD4$aJbbRPl0(*n@L-uk? zUBS}$(ASd9uzlM@D>swifT&o#PN);mdo%+FJg``9HU}TDFKVNLi;Sz{c{!bA5t6h8 zmLq&=ixr)w?`-2f#l~ygjbvvo@V-q}?XYu6Gkedtf_4v-XV-Pq)qC{?tdu{}b9#AMDjgLLbsc6}-h2Q)VSAF{VvEnQkA?*NtdVbq~LvK z|Heiah+tI7$3=zZR^39VOjbPzx%UG?`DsoBvGFwJtAHTwI2%4tH!+djboA~ryIF#h z=4!`s27m#>#FzLJ}!iH2-Oofh@_`!)*FiuuUG zyOfldZ-i9pxPjB`Zaux~54gBIsLbaFr-Kz8U#R4v4Bt3T^CYarEUcz(PR?d(Fg?tY zY-YUZ_kKu2Gng*@>y#e2@BTTO&t6J<+wLjcdCaDq(Gw^=J--uUYlO=HRoOsn3T;JE zNyZ7NHhs^ZU*%onsP_EuUP2=#Rv!28;2Y4w&@d3AqoJvEV{`qCA#UKyYbdd*?K}Xgt$04yKZFhD=--GTtQvJTYnF3 zXQ#8b=*(6$cDk;}%~&<*aK}q0d{Py$2Fx}v*vU%9Wa)u)YtL$&Y?cb$kBt?ZiY;Mj z-n+VjJY-EAkcgz>msPBugtcX`BSP$8mUTWy2lX@G<=#F|yRn5g(!nWM{ElOt&RLh; z<1PI-L#~4)!Dr_)&xfS1S^vTgYKzO!w9s1Oc)V&`(@I-07KS-)Aw%Rk-Osyn=Id3( zq;u4jZlao${h?1->(ae`;r7UO%tWtW54#m!9FbnM!;xqAg$2V1xyYB16Wfg>1l0Hs zfe;d^3(Vy}-MRVxg3@=}Fe4oUwI|UUt1OTczSP%2t7|s&-sdFZB{68E`>tX1kWF)) zmX%MHR(DHF%eQ9;Tj$0JS1=<-GdeIrSgbIxvbL=o7@I8T%!p)xY{}$8xHvfMc^3~V zq0vsW0@+|47LCaj8c^O{q6qJa53M`4{+J5^H9t_ew;f?zxS3}s%;TLOeA_^j&#t%k z`@0=kP6m1Y_`wZU+rB;Rxkscu4x&D1Ax5L#17k7mG%pQwj<@izeH<SshM4j(M;b-iY?MI2pL_1LRUXmfCNPoP?5N)B!4_^`I*+%ck^ z)9I)Oblrn7*)qgs4`cnvJ&MmY>&o)&?JX_R z)^d>*VTN8RRub9{%VjT^k@|y^#Q9|Se4ghFIa0&C-mxf8EY#0G zBsl;f$)ug7a(L+?b43UvV-2z_D+4{Sv_WaV!>s@s3IjI|ROMFiad6(bk{+0lSNwjChn*gZ)DYTu z*T&Pg_V?qV42ogaY|k3Hf>dU65_9Ou3M`&E$c$PV&iwp|V_u&=qh|4Y!TjT8?Nmf< zPUTWW2*SYDxbuj$b*C?9r?aptG}hqYP|p_MrxaJ?ADcgM4-%<(O~A|&J2*YTTs|3wOL~K|%@7q$U4bpQx@JtaxtSB@!s+P$#!XNJaZT&%92(h)m&Nfd z;&)@ys|TP@`v-=n9**N4F)lJJ{;2olUGKLlJ53A-LqmOAQ!#H_Z(DVBiQEe?1PRIY zu*Y?2Te=@UfRbKW_p9Cy4eO9lbi8N{tEnUtSZAt|hZPAGnaHxnrHy4XIDG{gM7kG_ zc?PgQuZL-*NZ$&teDB^Cks`r&=^9P?BXju2x|PC^9To2EgV4ynxnvb7-PIoJbzQ zlJ>;2B6QE5*3zzjQVVxD&V7?w>`vjbo!r=9qfUCiOmCX){+O-0;Am>SVC`v&foPYY zzMF}Sn@K^ywHr4UQiS1*i*b$JL?kh#?cKa%b49{f(p--xy>EqErFxZ1f?H5#UV3SM zNyC&`u#zn0^}CO_Zx3i_j$37l@&$4|u=aDDR@2pY;N5ovk4H(VqW4*8SC%^$3@0d3~g3br@_`P+Uz+nb|v4%sU`C z%S*LAi_U^um_a7Lr{wwWG5*+_)BR6#utrZz-)U{x*2d2_zQ6Lotiy8n?WT4@t_y08vL8UCvX}lNd1Ble&-l=0B?Zx&xg9h< z{F0jVPR@W8Rz%nqcaUe>6yrL2*9@K_xwwpGH>$v}Gm=cp8|SwF;mMIv+fn*Gz@02m zXY&ywe3m@-n0uu;TVRFydPHs+RZuO;CO2ObfX)(OixDx|lBPH@C79uM?|8qK{ni@D zXoRgsL!V!ZLs3J&qD?F3vfW`H>}vf4>j*XS_>du?96dX_MyoY*x)eg4sOgN2D+zo2 z@f}Nc(&cJ*q+(T(QO>q>6j!mP0sCDlssw(eBfTfG88!YY4Jf^~R5ym%*;Pa$xC&UA zgNca={AjAGe=puW$}cD;NE!y~p1;#2xBBQZjaelAz9Vnmih<9Z)Rk1?>+Abe z-4t?*^bViHnQ1MS#8^-R1T8vAh{Nou`Ch*;G(>IJPnkts4*EXw+F3#1 zLz*uG?;GaL48Rr}{Sdx>b$|hJModpHX9ZWSGA8cV<`&g0d-#t3dFKaHR)Dm-jItqY zlZH*UOozL%L>dH{oCy&g7RQ;UPd}wi2S=ZeiOO3gI&v;P0*HX#j2CIuCxpie!|P5K zipFP%)PSYE56d|}-|L(CMSTCrt<=p7`l&`Cv@S1{>++kbo4HTxHy^AREmr zc8El5H}n3eN?(cD5lbHH&dj6a2#IZ@(Yx6t8T#5kpYr;ocu>h%#EsyPnnw%Qrdf;v z+yC(AmM|~_%(^i_*>220eyNF-tFpN1%Vv8U{cYYYxSu@E*Bacuf68tz0hXn8<&+PV z>tr|E+H*Y6_dOe{WiV4SGgLGycOwO~c-cX;g7i*K^4X1N3!|PulsPXu?j%L*^l`0o zvm0Fku2WAFrat{r5Ta9NoCMW=#k$vcL!WMbd|6s1m)IALe1&d3-i=gpR(%ni5ZN6A z(HuRu_wOY<-0_Nn^gT4pZJ{{c`IK<3+4aiR$b-!OTvsJ&04g4X_#9Y##!|Wdg48A( zi%-;r_z@7@K_Ul%wcM&IczsY$Z*M}>hbxk4-@G&^dfa={#Z>X@$9FHjNAH~n5Y@s; z*g^6*4vZQ&{`~g&=huL{RfivY0>1EjE;X&^?Mh_k9RdE{D0Bv%}^%a^a?0qveDnS_P z9ca$>q+Kp*QMQ*W#%2^}Z{ZeIqPRFWXJXGS+4ykWxqGlVQXLK?yU%4^n zK{Tc+Gb7)D;7%M*fGFA(w)so`@0lC60LlRJ9dNJ*V4_>Oin`lj(suec0I`6cz?#S7 z%Yc;|$Ijja z2wCXN%uGU31RDgn>sGC2Y?;Lbjcfg>?4E&G7tuv~A7}5}EV^8yCG+fM%L$1{)yZ3? zh{YUF4|i)Gs^qT5A@+No^VO$NORgF~&#Q7mI|ji1syfABYLhYmst+K&?Bv$RSHW<> zQz}~%_;L1n*tOmLF>p*pGYi!s;QpmNTP42Kq_5mx>^mgcSujL$c9Fgk-iRfEZqXVN zziD>|oxo+wTZ1f(>3FGFM z&RwqOYX{y!WWPZhF77a{WE@u?fQ)_fEfadaC_T;vogt(c-dKHsQchog&nkqGpT}tR zBrK%xo(Quh3U|zW@ytzfk#fWc^N_-kSImrSH=%h+$k$BJdu9p%cYt?zx>^y8ty`_= zhEE9REKYZe($L)1`c%XN5oCjSL^Q8{@b{;g-|gaOe-d30mRb=unHl*B*oP+ZZ;$*~ zZzfrv#C;>93Nh=6P!EbJP~mlPb)`MtJ;9fIIS+0_ZrtR)8Qzog9(d8kd2WC0SYfRK z#X-4ya|Q?wVAq(i8F1nZ!MYR0?oS|b>nm$t@>s541vOqID_boX*5p|X+HQFpc6{tB zjP_C?XOY$ZYOywQV6mwyMDK1D=&eKx{mSpz8G{lQ+&H(;0LAd?DlLv=>&f%W0(_n- z#Kp~xAM|Wx?Gilzl?T0KWBcGAe|!+$J#du-qeD&fR{6{x)3cjGizM5nf+H*1S=DafL&9XscXW+Lo^8&TgM7&r0a9N6X- zX$Z$+x(kh^oE@N1ti!?z(B{@QUfVy?ZEU`v;XB96W`sI<%&z%peFFFU$oSg(*56Iv zB^ziaME%B2P^29KZ9HQH5JUic(#O-MJo)qIPyZUj$?_cs-$aLzygdLX(TjvIg?f49 zAHDiMkT1|c=ZXIdItliUm;qdye7hFa-P(TjZen9n}4XSGd4afFYf3*z%{PhS>OumUdtuoj??7;9#U4Y2X zP?ZB2n-y0>1Shkn^Go|m$^7*d8+PWZ1lOYg#YL_lcZY(w`em|9PKtC1dk@Ph%Idb- zc6NHXjAtUy=o3(fg48`ky%Jcc_o&v(ZbiNS3VSNa)iAo6YuiceZJ=|kAncCqcC0N` zJ_PL6bXpp~s(Y>d0Lhdc5c^2BkX%hO%8bQ;^e#>IfS{{OX>|1M`DD?&4yFwdGi*cC z2dn_h0n9F#DTGGH3iR1B9F=J8fN!Fl;o2+(N9RC`!f+$igy$^D{b}X2*UF%w_CP1* z8=zDK_V)JvUv<|R)zr4FJr)pA0YNYz;6VkXBS=%wBVC$`QbJSdC4ka9HVlL+z1e8e zrMCnSkls6lCcSq-`kTRW&$;jYz2l9W!3YD`$=xcSAm~al!%CU+BCT-O00EYz%kkVDJo5SZbS9>fd{&5s)CF~ zw{BbCEJU~8qq-viJFx|ZWaw=qIZ2G>guXQ`9w;UG!;G2h@$Tjc9g2YK#oz0IS5-WE zp!BJf3+R=9mkpw{59C`NepQ8$k^QRv@B|p=#?P>@Bpe|PCFBN*mV>30FvTTeOG?sl zsli!)9C*+UCRjpGw}G3c)R#;|6&6g~IGMk0!uXeuPaF z_w5Mo!b%gv5&YLhja@hM=YUn+TF2pAouicx6UxAlYDt)|d9l(FUI~v<&C_QYxS1}#G<+%P z_p`m`A6@A`&arl(+Ru>)!oli^7J0D5w&4{Ze>@3Bz`2U^Wm1Srj2ALCuB zSvT*!#AJb8j#R(&^3&oZ=enDM#O`)y+H~z@TC^}oFZ>|W0I?Zg*=4&E!6z=h?;Hbj zM7<~gMT9#0!DlxmVn59G0&gZX)rEE0VRW3uD{n`4X}hLvI3Ajy%+1s5>Gl4?F16QX zv??R7h>;6(n`~PrN)fTY{)) zG}#OL@OZxU<>U^L>pQB~8S);#D=Ep!!llJ8*T!u}OdqRipg)1w|1|!5Fhm{|6*YWn zoVy-5KQ~g<<34grUQzkOk&a+}X)@RL-d0#L2|GQn0KhW23uTTn1`*7*mV3JI&4H`l1uSaCgiIurMe1^iO`Bn)^CM&w!w$rdVki5M-Ixw_bOir5mIS zIP`WFR6Or5%2x!uun%XR!?*|tokkiOa$D$dTMWi-OuY&btc;$m4>)@UK2SIDF_`iyk_3*$VZq6bmi8=F zK*Y4yPqK6>rHxY~XLqj4b-{Aua-ox>y`!g$>wu?6A?-2by!SasZDg-rN@HXkraGXo zh25ORpNngY6)$;Sx@3$Q-$r~)V(s5qtE)AnQFSp=8+|=y=Vb4A@j}9hfor#tPsK~ZX4y?=gKlA!b!004;liLC6@6f6k^DQ;CYl| ztTvmA>kB`j{!ht&iF6LdS-?UzXnX=&lnh&PZUblo3c+5l?~9c%zg{n4`36F~DIG>e zq!X}7g)hspqGAo&M~NVKcNVOETeG&cg|VWd(N0R@phqAO>n_2!a?Hjyv%N3svOj<8 z<`3HdwPIIA7qG1ZosV|!$m7Qb8ptXtF8cHnXo~q6Zbi|C#_e|R$$UKv#_OPQLx$27 zN3{f#N*I#`WgU8fC&-z(pKQoim zG=8>dK+WPw&RNddjNHDxoDA@?Pjq{pf7>hc&_Lx3gy;qnV`Q24{2RcBFC;5KTLv7^ zaXPLb;p{@CBd8+C7>a-uLnVcTZyP_&5OiHBc&tJHc!q}U&RTe*uPq;Y{Vc75XWRhK zyFWFG{@f3#5Kc!MBLsDOT7&IozL~>)GMCCZrvHVe@y+!7d}jNgolS>D|3JgbJKMK-~7WpVARJ|r#cuIQLH8b$YL~Fthc@WU5C;655t+Nsq2fYz1cfo1^2#} z4Gp=6gRD-&tKVqq2Q>wKQS6K40dfWe9RnhgpL?fVG<|*jn?fOo3k2nyb!7#OM#gxe zeMx=L13R5*p2@qt%-oN5ga?DebHKP!^`=x(P8Cy~QN9(LmXGhazc4kX3s#EnN6hdW z`4~*Hw|YotT^(%1i+iA$lZ}57uKCd59w*Q!YWAxhC^8d%WWi%4f)oc77ECDLj&u*n z3RcEiISk67Z|Yu{I)H=Rz`zKPS@Pe(&Qcma&LBE6w#_4lH;x>R)^&FtXCUA|fUw zZ4}h9upeqkfKWuE$5A6{QxO#tmWx3k2r;7#yli9}s}9?govrPBZiFS(MZj3TS)@TTNW_q@6aEGR6Fx^(!Q`CaZBp zYBd{|8t!4X_ph-}ID<$`WaG{c8oaI3)-xb*?b<9B)jpF%rml!a+RnRfjYT9q zS@EN!k7On2&SOzRv1W*~XJH1SOj6*b*O#`Y!s{erA5%o6L zsLv%ZoZ(Bhmfm>V>+hRK(A9v!rTto4op)AtoTA#NL4BI+u6XCa*gNgzxdkzB!PQh1 zm@u@pZT7?)5i=gzi3Ed9;=^=44D|FUfyTQ0^8UQBwb`$f!mo}c#R@KM>e^M(o7jG7_7gZ*o#?zJ_ z!hC_lYU!AvVKmw~f9qReFPpyZjjb86sq!bY zKsR{gNoQEAHMCniFy*Y1+Va9f+7m!&%cPd>*a9nCUAiY4oM5)v5&MHC4+IPw)1gC$ z46S_qwi2Q!iN`8&v9jwpd$GKQ@ob;8L$h-;hFe&)LTBM^vEyVXJkeQ-d5;=!OiwmP z-{*E3P#_BLT;!*flcMYw#exmct}o&Pup&^k)F+16M)Fv()$=FE8IxcX!nm0LUBt8; zl!O5N62-E{8+dAsJmBe5R}F^s+|vfOO|oF4T#Hu9NAydMeh*M$=Bm6Qo}1^f-!S05 z=<$aahO3HN%h!{TKS)oKq=(ObwG01jdi;DUCjqE{@CbGmkf9Q^_uBAPN(H7-`x4uG zc^lHotem)-Y{E3Hzos_Kc!qD1<>335WzBsoy zH!10Z^)SjBTqD-(RShex^^>g33zyF~ibOAepGalqwJ=5T7CNyt;T7BQZJd#-i|1IawM{CaVg_DJiZ6|-@QYRZZPoL68u|Ea6XQG*zJfCOEM$cL zrxuUq7J(YpL-FS(y552L~UOt6%8v z$~8PO@4$9fLTih(Q}U~XJiqi3v~_mg8)H6!EN=`Y zFnVh-w|8BaQ7?a2oYq^Jc)5fS7E|s$l;4*{e0HmpnKcADGaOrLyz5iB#xI#g)u{^=SK9AmWFA;{S4>EWUc&(>BWS|< zBY|d3Kn2zM6g*<9^}gmZvgON{D?}-8iETXw08M52v?AqkBs=QstKjXaRE)Gwj8EzV z^z{it0&JR`0#IEuB}P+hEIFIY8`$}IMS}G}vJPzglCdeVMrKlGwy)`w~EWs-_h*Q55w=1-^P#&h-3QIhaVkEia2k+F>)^+u9pd*Wm>P9=e)1 zsyG;D^gXKF^C+Ay9r^_N4p>q$Uo)Ae7;p%xG-!bj3@RVsRly=r1lN+oa*39Kp;6Sq z9x?`3+UBZc4;Y=_{fW2w%`BkH8_!!0hNS!A&oCfX`Wd(H6^D^N8hW|6vrf17VR?D& z+wz)1Q~jIo2@oj7c8BIXMC7hjrQUVe`VL<(vDpF@R$PXw4Jq>&cW28#KYa=X-1!e9 zOdm${UTY|dk28FUeSvZt^7L%7Cpz-V=Zzi(0679_JX+~f^1pa*+bSrymDX{B8Z15L zr!{NWSJ$-ijnrYX9rAOvnsuoZe_L#UGtM+Ot=x}a$;bHGUH~ed$8S|uqR0xSPs|7o zaO;-I2(z5j*H5=i5pysyGFrHrzRw|VV>P0mY6$-yG(Xb1BXBJ@q^*h!mLp(3$20oA zs)Vm_r(baQwrNhT?bvbwXfJScK~V?p_!#Uxh<^Cr(JyR;h-h=r^_v}VJYdQK2}p!S z6*gb$>cTtY-lxP)g3k;Z7paAB+o?{N7P$hmK}X9l_H_zn$tNP5=u}ulOJ{;P#sw+* zwMPA}+ideLd+72+oB?>!3K9J|$O%}l100c?y-CZhgp%F>py~_HOY0-ZLHn@{2jg(w z$29{3u1G_|fiov;BvrY<24xuV$^jKZ6Dj<7v@L1*bxdntJ|##G4;T+}@Ud@Qtd z(|RhQucGe|YNVc&vM|d^AnyqLwRg(thXK3<1GbKZjlMb{3ZmyNyiUDO{RZ8(oU5k* z4*P$SdH(t1lPA;Dvql!CTm_DC->3BpzO9S{L-q(+*$S>wRoq{xDjM7Y1qmnfa;_xA zaRcINHZtQK+kp(#SOY8n6lo=Qa3Kq)p)P9npDXy0>>pgemu4)&^gLSIq6r3%xp!~ZPp57I3kmzRjueUh& z;uu`Fyf@-#z^_4Z@4gdt<17#I+(}vSfcl>Ywv|0j9<-+2T7mkb?xd(Y%j^yimsCGmtSmX%bL KNW1yq+5Z3!AL6+H literal 0 HcmV?d00001 diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..995b7a1 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,68 @@ +# Storage, search, and privacy + +## Session discovery + +`cont` reads the agents' central stores, not every folder on your disk. Each session's recorded working directory supplies its location in the picker. Discovery includes sessions from all locations; the initial **view** is scoped to your current directory. + +| Agent | Default store | Resume command | +| --- | --- | --- | +| Pi | `~/.pi/agent/sessions/**/*.jsonl` | `pi --session ` | +| Claude Code | `~/.claude/projects/*/*.jsonl` | `claude --resume ` | +| Codex | `~/.codex/sessions/**/*.jsonl` | `codex resume ` | +| OpenCode | `~/.local/share/opencode/opencode*.db` | `opencode --session ` | +| Oh My Pi | `~/.omp/agent/sessions/**/*.jsonl` | `omp --resume ` | + +### Overrides + +- **Pi:** honors `PI_CODING_AGENT_DIR` and `PI_CODING_AGENT_SESSION_DIR`. +- **Claude Code:** honors `CLAUDE_CONFIG_DIR`. +- **Codex:** honors `CODEX_HOME`; names also come from `session_index.jsonl` beside the session root. +- **OpenCode:** honors `XDG_DATA_HOME` and `OPENCODE_DB`. Relative database names resolve beneath its data directory. Resuming sets `OPENCODE_DB` to the selected store, including custom/channel databases. +- **OMP:** also scans `${XDG_DATA_HOME:-~/.local/share}/omp/sessions` for migrated stores. Its shared `PI_CODING_AGENT_DIR` override is deliberately not auto-used: that would mislabel Pi history as OMP. Use `--omp-dir` for custom/profile stores. The active OMP configuration/profile is inherited when resuming. + +Repeatable `--pi-dir`, `--claude-dir`, `--codex-dir`, and `--omp-dir` add **session/project storage roots**, not working directories. `--opencode-db` adds a SQLite file. + +```sh +cont --all --omp-dir /path/to/omp/sessions +cont --all --opencode-db /path/to/opencode.db +``` + +### Format support + +- Claude subagent logs/sidechain messages and Codex sessions marked as subagents are excluded. Codex archived sessions are not scanned by default. +- OpenCode's current SQLite format is supported; older pre-SQLite JSON stores are not. Reads use a read-only transaction and include committed WAL contents. Child/archived sessions, synthetic/ignored parts, assistant messages, and tool output are excluded. +- OMP supports legacy header titles, current title slots/renames, and human attribution. +- Malformed JSONL records and incomplete live-write tails are skipped. Changed files are retried on refresh. The picker never rewrites session logs or changes OpenCode database records. + +## Search ranking + +1. Exact title phrases, with exact titles and prefixes first. +2. Titles containing every query word, in any order. +3. Fuzzy title matches using Nucleo's Unicode-aware subsequence matcher. +4. Human messages containing the phrase or every query word, in any order, **within one message**. + +Recency breaks ties. Named/renamed titles take precedence over the first human prompt. Search is case-insensitive; fuzzy title matching supports accent normalization. Message search is literal, not semantic or typo-correcting. Assistant replies, tools, images, compaction summaries, and recognized injected notifications are not searched. Human text is not truncated for indexing. + +## Cache and privacy + +The TUI opens immediately, loads cached results, and refreshes changed files in a background worker. JSONL parsing is parallel; unchanged files are detected by nanosecond mtime and size. OpenCode cache invalidation checks both database and WAL metadata, so it catches new messages and renames before checkpointing. Deleted files/sessions are removed during refresh. **Ctrl-R** rescans; there is no watcher or daemon. + +Cache location: + +```text +${XDG_CACHE_HOME:-~/.cache}/cont/sessions-v1.json +``` + +Use `RESUME_CACHE_DIR` to choose a dedicated alternative directory. + +**The cache contains metadata and human messages in plaintext.** Directory/file permissions are `0700`/`0600` on Unix, and writes are atomic. Nothing is sent over the network. Don't share the cache or commit it to a repository. + +- `--no-cache`: don't read or write a cache. +- `--rebuild`: force a fresh parse. +- Delete the cache directory to remove copied text; the next launch rebuilds it. + +## Resume behavior + +Enter restores the terminal, changes to the session's original working directory, and launches the matching agent. On Unix, `exec` gives the agent the existing terminal directly. + +The picker does not fork sessions, change your parent shell's directory, or add permission-bypass flags. Agents must be on `PATH`; their normal configuration and trust checks apply. Missing directories are reported rather than silently resuming elsewhere. `--print` prints the selected command without executing it. diff --git a/scripts/render_demo.py b/scripts/render_demo.py new file mode 100644 index 0000000..62ccc45 --- /dev/null +++ b/scripts/render_demo.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Render the real Ratatui layout with fictional sessions. Never reads user history. + + uv run --with 'pillow==11.3.0' python scripts/render_demo.py + +Use --font /path/to/monospace.ttf to override the locally available font. +The font is rasterized into the PNG, not bundled or redistributed. +""" +import argparse +import json +from pathlib import Path +import subprocess + +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parents[1] +PALETTE = { + "Black": "#11151e", "White": "#eef2fa", "DarkGray": "#7c879d", + "Gray": "#b8c1d4", "Cyan": "#65d9df", "Magenta": "#b89cff", + "Yellow": "#edbd80", "Green": "#9bd39f", "Blue": "#8fb9ff", + "LightMagenta": "#e2a1dc", +} +BG = "#11151e" +FG = "#dbe2ef" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--font", type=Path) + args = parser.parse_args() + candidates = [args.font, Path("/System/Library/Fonts/Menlo.ttc"), + Path("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf")] + font_path = next((p for p in candidates if p and p.is_file()), None) + if not font_path: + parser.error("Provide a monospace font using --font") + font = ImageFont.truetype(str(font_path), 28) + small = ImageFont.truetype(str(font_path), 23) + subprocess.run(["cargo", "test", "--locked", "--release", "ui::demo::render_demo", + "--", "--ignored", "--exact"], cwd=ROOT, check=True) + data = json.loads((ROOT / "target/demo-screen.json").read_text()) + columns, rows = data["width"], data["height"] + cell_w, cell_h = round(font.getlength("M")), 42 + margin, padding, chrome = 48, 32, 78 + window_w = columns * cell_w + padding * 2 + window_h = rows * cell_h + padding * 2 + chrome + image = Image.new("RGB", (window_w + margin * 2, window_h + margin * 2), "#090c13") + draw = ImageDraw.Draw(image) + box = (margin, margin, margin + window_w, margin + window_h) + draw.rounded_rectangle(box, radius=22, fill=BG, outline="#2b3344", width=2) + for i, color in enumerate(["#ef7474", "#e6be68", "#83c995"]): + x, y = margin + 32 + i * 31, margin + 35 + draw.ellipse((x, y - 9, x + 18, y + 9), fill=color) + title = "cont — one picker, five agents" + draw.text((image.width / 2, margin + 35), title, font=small, fill="#929db3", anchor="mm") + draw.line((margin + 1, margin + chrome, margin + window_w - 1, margin + chrome), fill="#252c3c", width=2) + left, top = margin + padding, margin + chrome + padding + for i, cell in enumerate(data["cells"]): + x, y = left + (i % columns) * cell_w, top + (i // columns) * cell_h + # ANSI colors are terminal-theme dependent. Use a readable dark palette; + # selection background is a darker rendition of the terminal's gray. + bg = "#303b50" if cell["bg"] == "DarkGray" else PALETTE.get(cell["bg"], BG) + fg = PALETTE.get(cell["fg"], FG) + if bg != BG: + draw.rectangle((x, y, x + cell_w - 1, y + cell_h - 1), fill=bg) + if cell["text"].strip(): + draw.text((x, y + cell_h / 2), cell["text"], font=font, fill=fg, anchor="lm", + stroke_width=1 if cell["bold"] else 0) + destination = ROOT / "docs/assets/resume.png" + destination.parent.mkdir(parents=True, exist_ok=True) + image.save(destination, optimize=True) + print(f"Wrote {destination.relative_to(ROOT)} ({image.width}×{image.height}); all session data is fictional.") + + +if __name__ == "__main__": + main() diff --git a/src/index.rs b/src/index.rs new file mode 100644 index 0000000..f4f4be4 --- /dev/null +++ b/src/index.rs @@ -0,0 +1,542 @@ +use std::{ + collections::{HashMap, HashSet}, + env, + fs::{self, File}, + io::{BufRead, BufReader, BufWriter, Write}, + path::{Path, PathBuf}, + sync::{ + Mutex, + atomic::{AtomicUsize, Ordering}, + }, + time::UNIX_EPOCH, +}; + +use anyhow::{Context, Result}; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use walkdir::WalkDir; + +use crate::{ + parse::parse_file, + session::{Agent, Session, canonical, compact}, +}; + +const CACHE_VERSION: u32 = 1; + +#[derive(Clone)] +pub struct Sources { + pub roots: Vec<(Agent, PathBuf)>, + pub codex_home: PathBuf, + pub cache: PathBuf, +} + +impl Sources { + pub fn discover(extra: &[(Agent, Vec)]) -> Result { + let home = dirs::home_dir().context("Cannot find home directory")?; + let pi = env_path("PI_CODING_AGENT_SESSION_DIR").unwrap_or_else(|| { + env_path("PI_CODING_AGENT_DIR") + .unwrap_or_else(|| home.join(".pi/agent")) + .join("sessions") + }); + let claude = env_path("CLAUDE_CONFIG_DIR") + .unwrap_or_else(|| home.join(".claude")) + .join("projects"); + let codex_home = env_path("CODEX_HOME").unwrap_or_else(|| home.join(".codex")); + let data_home = env_path("XDG_DATA_HOME").unwrap_or_else(|| home.join(".local/share")); + let opencode_dir = data_home.join("opencode"); + let mut roots = vec![ + (Agent::Pi, pi), + (Agent::Claude, claude), + (Agent::Codex, codex_home.join("sessions")), + // OMP and Pi share PI_CODING_AGENT_DIR; do not index the same override as both. + (Agent::Omp, home.join(".omp/agent/sessions")), + (Agent::Omp, data_home.join("omp/sessions")), + ]; + match env_path("OPENCODE_DB") { + Some(path) if path == Path::new(":memory:") => {} + Some(path) => roots.push(( + Agent::Opencode, + if path.is_absolute() { + path + } else { + opencode_dir.join(path) + }, + )), + None => roots.push((Agent::Opencode, opencode_dir)), + } + for (agent, paths) in extra { + roots.extend(paths.iter().cloned().map(|p| (*agent, p))); + } + let current = env::current_dir()?; + for (_, root) in &mut roots { + if root.is_relative() { + *root = current.join(&*root); + } + *root = canonical(root); + } + let cache_dir = env_path("RESUME_CACHE_DIR").unwrap_or_else(|| { + env_path("XDG_CACHE_HOME") + .unwrap_or_else(|| home.join(".cache")) + .join("cont") + }); + Ok(Self { + roots, + codex_home, + cache: cache_dir.join("sessions-v1.json"), + }) + } +} +fn env_path(name: &str) -> Option { + env::var_os(name) + .filter(|s| !s.is_empty()) + .map(PathBuf::from) +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +struct Stamp { + len: u64, + seconds: u64, + nanos: u32, +} +impl Stamp { + fn read(path: &Path) -> std::io::Result { + let meta = fs::metadata(path)?; + let time = meta + .modified()? + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + Ok(Self { + len: meta.len(), + seconds: time.as_secs(), + nanos: time.subsec_nanos(), + }) + } +} + +#[derive(Deserialize, Serialize)] +struct CachedFile { + agent: Agent, + path: PathBuf, + stamp: Stamp, + session: Option, +} +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +struct DatabaseStamp { + database: Stamp, + wal: Option, +} +impl DatabaseStamp { + fn read(path: &Path) -> std::io::Result { + let mut wal_path = path.as_os_str().to_os_string(); + wal_path.push("-wal"); + let wal = match Stamp::read(Path::new(&wal_path)) { + Ok(stamp) => Some(stamp), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => None, + Err(e) => return Err(e), + }; + Ok(Self { + database: Stamp::read(path)?, + wal, + }) + } +} + +#[derive(Deserialize, Serialize)] +struct CachedDatabase { + path: PathBuf, + stamp: DatabaseStamp, + sessions: Vec, +} + +#[derive(Default, Deserialize, Serialize)] +struct Cache { + version: u32, + files: Vec, + #[serde(default)] + databases: Vec, +} + +pub enum Update { + Snapshot(Vec), + Progress(String), +} + +pub struct Report { + pub sessions: Vec, + pub parsed: usize, + pub reused: usize, + pub warnings: Vec, +} + +pub fn refresh( + sources: &Sources, + rebuild: bool, + no_cache: bool, + notify: impl Fn(Update) + Sync, +) -> Result { + let mut warnings = vec![]; + let old = if no_cache || rebuild { + Cache::default() + } else { + match File::open(&sources.cache) { + Ok(file) => match serde_json::from_reader::<_, Cache>(BufReader::new(file)) { + Ok(cache) if cache.version == CACHE_VERSION => cache, + _ => { + warnings.push("Cache unreadable/outdated; rebuilding".into()); + Cache::default() + } + }, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Cache::default(), + Err(e) => { + warnings.push(format!("Cache: {e}")); + Cache::default() + } + } + }; + let old_files: HashMap<_, _> = old + .files + .into_iter() + .filter(|f| { + sources + .roots + .iter() + .any(|(a, r)| *a == f.agent && f.path.starts_with(r)) + }) + .map(|f| ((f.agent, f.path.clone()), f)) + .collect(); + let old_databases: HashMap<_, _> = old + .databases + .into_iter() + .filter(|db| { + sources + .roots + .iter() + .any(|(a, r)| *a == Agent::Opencode && db.path.starts_with(r)) + }) + .map(|db| (db.path.clone(), db)) + .collect(); + if !old_files.is_empty() || !old_databases.is_empty() { + let mut cached: Vec<_> = old_files + .values() + .filter_map(|f| f.session.clone()) + .chain(old_databases.values().flat_map(|db| db.sessions.clone())) + .collect(); + finish_sessions(&mut cached, sources); + notify(Update::Snapshot(cached)); + } + notify(Update::Progress("Looking for sessions…".into())); + let mut files = vec![]; + let mut databases = HashSet::new(); + let mut seen = HashSet::new(); + for (agent, root) in &sources.roots { + if !root.exists() { + continue; + } + if *agent == Agent::Opencode { + if root.is_file() { + databases.insert(root.clone()); + } else { + match fs::read_dir(root) { + Ok(entries) => { + for entry in entries { + match entry { + Ok(entry) => { + let path = entry.path(); + let name = entry.file_name(); + let name = name.to_string_lossy(); + if name.starts_with("opencode") + && name.ends_with(".db") + && path.is_file() + { + databases.insert(canonical(&path)); + } + } + Err(e) => warnings.push(format!("{}: {e}", root.display())), + } + } + } + Err(e) => warnings.push(format!("{}: {e}", root.display())), + } + } + continue; + } + let walk = WalkDir::new(root).max_depth(if *agent == Agent::Claude { 2 } else { 12 }); + for entry in walk { + let entry = match entry { + Ok(e) => e, + Err(e) => { + warnings.push(e.to_string()); + continue; + } + }; + let path = entry.path(); + if !entry.file_type().is_file() || path.extension().is_none_or(|s| s != "jsonl") { + continue; + } + if *agent == Agent::Claude + && path + .file_name() + .is_some_and(|s| s.to_string_lossy().starts_with("agent-")) + { + continue; + } + if !seen.insert((*agent, path.to_path_buf())) { + continue; + } + match Stamp::read(path) { + Ok(stamp) => files.push((*agent, path.to_path_buf(), stamp)), + Err(e) => warnings.push(format!("{}: {e}", path.display())), + } + } + } + // Most recent sessions get parsed first on a cold start. + files.sort_by_key(|f| std::cmp::Reverse(f.2.seconds)); + let total = files.len(); + let parsed = AtomicUsize::new(0); + let reused = AtomicUsize::new(0); + let done = AtomicUsize::new(0); + let errors = Mutex::new(vec![]); + let new_files: Vec<_> = files + .into_par_iter() + .filter_map(|(agent, path, stamp)| { + let old = old_files.get(&(agent, path.clone())); + let session = if let Some(old) = old.filter(|f| f.stamp == stamp) { + reused.fetch_add(1, Ordering::Relaxed); + old.session.clone() + } else { + parsed.fetch_add(1, Ordering::Relaxed); + match parse_file(agent, &path, stamp.seconds) { + Ok(s) => s, + Err(e) => { + errors + .lock() + .unwrap() + .push(format!("{}: {e}", path.display())); + return None; + } + } + }; + let count = done.fetch_add(1, Ordering::Relaxed) + 1; + if count.is_multiple_of(50) || count == total { + notify(Update::Progress(format!( + "Reading sessions {count}/{total}…" + ))); + } + Some(CachedFile { + agent, + path, + stamp, + session, + }) + }) + .collect(); + warnings.extend(errors.into_inner().unwrap()); + let mut new_databases = vec![]; + for path in databases { + notify(Update::Progress("Reading OpenCode sessions…".into())); + let result = (|| -> Result { + let stamp = DatabaseStamp::read(&path)?; + let sessions = + if let Some(old) = old_databases.get(&path).filter(|db| db.stamp == stamp) { + reused.fetch_add(1, Ordering::Relaxed); + old.sessions.clone() + } else { + parsed.fetch_add(1, Ordering::Relaxed); + crate::opencode::read_database(&path)? + }; + Ok(CachedDatabase { + path: path.clone(), + stamp, + sessions, + }) + })(); + match result { + Ok(db) => new_databases.push(db), + Err(e) => warnings.push(format!("{}: {e:#}", path.display())), + } + } + let cache = Cache { + version: CACHE_VERSION, + files: new_files, + databases: new_databases, + }; + let mut sessions = cache + .files + .iter() + .filter_map(|f| f.session.clone()) + .chain(cache.databases.iter().flat_map(|db| db.sessions.clone())) + .collect(); + finish_sessions(&mut sessions, sources); + let changed = parsed.load(Ordering::Relaxed) > 0 + || cache.files.len() != old_files.len() + || cache.databases.len() != old_databases.len(); + if !no_cache + && changed + && let Err(e) = save_cache(&sources.cache, &cache) + { + warnings.push(format!("Could not save cache: {e:#}")); + } + Ok(Report { + sessions, + parsed: parsed.load(Ordering::Relaxed), + reused: reused.load(Ordering::Relaxed), + warnings, + }) +} + +fn finish_sessions(sessions: &mut Vec, sources: &Sources) { + let mut titles = HashMap::new(); + // The index is tiny and can change independently of rollout mtimes (e.g. /rename). + let mut indexes = HashSet::from([sources.codex_home.join("session_index.jsonl")]); + for (agent, root) in &sources.roots { + if *agent == Agent::Codex + && let Some(parent) = root.parent() + { + indexes.insert(parent.join("session_index.jsonl")); + } + } + for path in indexes { + if let Ok(file) = File::open(path) { + for line in BufReader::new(file).lines().map_while(Result::ok) { + if let Ok(v) = serde_json::from_str::(&line) + && let (Some(id), Some(title)) = (v["id"].as_str(), v["thread_name"].as_str()) + { + titles.insert(id.to_owned(), compact(title, 200)); + } + } + } + } + let mut directories = HashMap::new(); + for s in sessions.iter_mut() { + s.cwd = directories + .entry(s.cwd.clone()) + .or_insert_with(|| canonical(&s.cwd)) + .clone(); + if s.agent == Agent::Codex + && let Some(title) = titles.get(&s.id).filter(|t| !t.is_empty()) + { + s.title.clone_from(title); + } + } + sessions.sort_by(|a, b| { + b.modified + .cmp(&a.modified) + .then_with(|| a.path.cmp(&b.path)) + }); + let mut seen = HashSet::new(); + sessions.retain(|s| seen.insert((s.agent, s.id.clone()))); +} + +fn save_cache(path: &Path, cache: &Cache) -> Result<()> { + let dir = path.parent().context("Cache path has no parent")?; + fs::create_dir_all(dir)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?; + } + let mut tmp = tempfile::NamedTempFile::new_in(dir)?; // 0600 on Unix; atomic replacement. + { + let mut writer = BufWriter::new(tmp.as_file_mut()); + serde_json::to_writer(&mut writer, cache)?; + writer.flush()?; + } + tmp.persist(path)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn cache_refresh_handles_change_deletion_and_corruption() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("sessions"); + fs::create_dir(&root).unwrap(); + let file = root.join("p.jsonl"); + let data = "{\"type\":\"session\",\"id\":\"p\",\"cwd\":\"/work\"}\n"; + fs::write(&file, data).unwrap(); + let sources = Sources { + roots: vec![(Agent::Pi, root)], + codex_home: tmp.path().into(), + cache: tmp.path().join("cache/index.json"), + }; + let run = || refresh(&sources, false, false, |_| {}).unwrap(); + assert_eq!(run().parsed, 1); + assert_eq!(run().reused, 1); + fs::write( + &file, + format!("{data}{{\"type\":\"session_info\",\"name\":\"Changed\"}}\n"), + ) + .unwrap(); + let report = run(); + assert_eq!(report.parsed, 1); + assert_eq!(report.sessions[0].title, "Changed"); + fs::write(&sources.cache, "broken").unwrap(); + assert_eq!(run().parsed, 1); + fs::remove_file(file).unwrap(); + assert!(run().sessions.is_empty()); + } + #[test] + fn opencode_cache_tracks_wal_renames_messages_and_deletions() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("opencode.db"); + let conn = crate::opencode::tests::database(&path); + let sources = Sources { + roots: vec![(Agent::Opencode, path.clone())], + codex_home: tmp.path().into(), + cache: tmp.path().join("cache/index.json"), + }; + let run = || refresh(&sources, false, false, |_| {}).unwrap(); + assert_eq!(run().sessions.len(), 1); + assert_eq!(run().reused, 1); + let main_before = fs::read(&path).unwrap(); + conn.execute( + "UPDATE session SET title = 'Renamed' WHERE id = 'ses_main'", + [], + ) + .unwrap(); + assert_eq!(fs::read(&path).unwrap(), main_before); // Only the WAL changed. + let report = run(); + assert_eq!(report.parsed, 1); + assert_eq!(report.sessions[0].title, "Renamed"); + conn.execute( + "UPDATE part SET data = '{\"type\":\"text\",\"text\":\"edited\"}' WHERE id = 'p1'", + [], + ) + .unwrap(); + assert_eq!(run().sessions[0].messages, ["edited\nworld"]); + conn.execute("DELETE FROM session WHERE id = 'ses_main'", []) + .unwrap(); + assert!(run().sessions.is_empty()); + drop(conn); + fs::remove_file(path).unwrap(); + assert!(run().sessions.is_empty()); + } + #[test] + fn codex_title_sidecar_updates_without_reparsing() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("sessions"); + fs::create_dir(&root).unwrap(); + fs::write( + root.join("x.jsonl"), + r#"{"type":"session_meta","payload":{"id":"x","cwd":"/work"}}"#, + ) + .unwrap(); + let sources = Sources { + roots: vec![(Agent::Codex, root)], + codex_home: tmp.path().into(), + cache: tmp.path().join("cache/index.json"), + }; + refresh(&sources, false, false, |_| {}).unwrap(); + fs::write( + tmp.path().join("session_index.jsonl"), + "{\"id\":\"x\",\"thread_name\":\"Renamed\"}\n", + ) + .unwrap(); + let report = refresh(&sources, false, false, |_| {}).unwrap(); + assert_eq!(report.reused, 1); + assert_eq!(report.sessions[0].title, "Renamed"); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0601dfa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,162 @@ +mod index; +mod opencode; +mod parse; +mod search; +mod session; +mod ui; + +use anyhow::{Context, Result, bail}; +use clap::Parser; +use serde_json::json; +use session::{Agent, canonical, compact}; +use std::{ + io::{self, Write}, + path::PathBuf, +}; + +#[derive(Parser)] +#[command( + version, + about = "Find and resume Pi, Claude Code, Codex, OpenCode, and OMP sessions.", + after_help = "Keys: type to search · Enter resume · Tab here/all · Shift-Tab agent\n Ctrl-L locations · ↑/↓ or Ctrl-P/N move · Ctrl-R refresh · Esc clear/quit\n\nSearch: fuzzy titles first, then case-insensitive words in human messages.\nThe default scope is exactly the current directory (symlinks resolved)." +)] +struct Args { + /// Search across all working directories + #[arg(short, long)] + all: bool, + /// Only show one agent + #[arg(long, value_enum)] + agent: Option, + /// Start in this location rather than the current directory + #[arg(short = 'C', long)] + dir: Option, + /// Initial search text + query: Vec, + /// List matches without opening the TUI + #[arg(long)] + list: bool, + /// Output matches as JSON (implies --list) + #[arg(long)] + json: bool, + /// Maximum matches in --list/--json output + #[arg(long, default_value_t = 30)] + limit: usize, + /// Print the selected session's shell command instead of running it + #[arg(long, conflicts_with_all = ["list", "json"])] + print: bool, + /// Rebuild the local cache from source session files + #[arg(long)] + rebuild: bool, + /// Do not read or write the on-disk cache + #[arg(long)] + no_cache: bool, + /// Additional Pi session root (repeatable) + #[arg(long)] + pi_dir: Vec, + /// Additional Claude projects root (repeatable) + #[arg(long)] + claude_dir: Vec, + /// Additional Codex sessions root (repeatable) + #[arg(long)] + codex_dir: Vec, + /// Additional Oh My Pi session root (repeatable) + #[arg(long)] + omp_dir: Vec, + /// Additional OpenCode SQLite database file (repeatable) + #[arg(long)] + opencode_db: Vec, +} + +fn main() { + if let Err(error) = run() { + if error + .downcast_ref::() + .is_some_and(|e| e.kind() == io::ErrorKind::BrokenPipe) + { + return; + } + eprintln!("cont: {error:#}"); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let args = Args::parse(); + let folder = canonical( + &args + .dir + .unwrap_or(std::env::current_dir().context("Cannot read current directory")?), + ); + if !folder.is_dir() { + bail!("Not a directory: {}", folder.display()); + } + let query = args.query.join(" "); + let sources = index::Sources::discover(&[ + (Agent::Pi, args.pi_dir), + (Agent::Claude, args.claude_dir), + (Agent::Codex, args.codex_dir), + (Agent::Omp, args.omp_dir), + (Agent::Opencode, args.opencode_db), + ])?; + if args.list || args.json { + let report = index::refresh(&sources, args.rebuild, args.no_cache, |_| {})?; + for warning in &report.warnings { + eprintln!("cont: {warning}"); + } + let index = search::SearchIndex::new(report.sessions); + let hits = index.search( + &query, + args.agent, + if args.all { None } else { Some(&folder) }, + ); + let mut out = io::BufWriter::new(io::stdout().lock()); + if args.json { + let rows: Vec<_> = hits.iter().take(args.limit).map(|h| { + let s = &index.sessions[h.index]; + json!({ "agent": s.agent, "id": s.id, "title": s.title, "cwd": s.cwd, "path": s.path, "modified": s.modified, "messages": s.messages.len(), "match": match h.kind { search::MatchKind::Recent => "recent", search::MatchKind::Title => "title", search::MatchKind::Message => "message" }, "excerpt": h.message.map(|i| search::snippet(&s.messages[i], &query, 300)), "command": s.shell_command() }) + }).collect(); + serde_json::to_writer_pretty(&mut out, &rows)?; + writeln!(out)?; + } else { + for hit in hits.iter().take(args.limit) { + let s = &index.sessions[hit.index]; + writeln!( + out, + "{:<8} {:<65} {}{}", + s.agent.name(), + compact(&s.title, 63), + compact(&s.cwd.to_string_lossy(), 300), + if hit.kind == search::MatchKind::Message { + " [text]" + } else { + "" + } + )?; + } + eprintln!( + "{} matches · {} refreshed · {} cached", + hits.len(), + report.parsed, + report.reused + ); + } + out.flush()?; + } else if let Some(session) = ui::run( + sources, + ui::Options { + folder, + all: args.all, + agent: args.agent, + query, + rebuild: args.rebuild, + no_cache: args.no_cache, + }, + )? { + if args.print { + println!("{}", session.shell_command()); + } else { + session.resume()?; + } + } + Ok(()) +} diff --git a/src/opencode.rs b/src/opencode.rs new file mode 100644 index 0000000..f98f833 --- /dev/null +++ b/src/opencode.rs @@ -0,0 +1,210 @@ +//! Read OpenCode's existing SQLite store. No migrations, CLI subprocesses, or writes. +use std::{collections::HashMap, path::Path, time::Duration}; + +use anyhow::{Context, Result}; +use rusqlite::{Connection, OpenFlags}; +use serde::Deserialize; + +use crate::{ + parse::push_human, + session::{Agent, Session, compact}, +}; + +pub fn read_database(path: &Path) -> Result> { + let mut connection = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY) + .with_context(|| format!("OpenCode database {}", path.display()))?; + connection.busy_timeout(Duration::from_millis(500))?; + // A single snapshot prevents mixed metadata/messages during a live agent write. + // Do not use immutable=1: committed messages may still be in the WAL. + let transaction = connection.transaction()?; + let mut sessions = Vec::new(); + let mut positions = HashMap::new(); + { + let mut query = transaction.prepare( + "SELECT id, directory, title, time_updated FROM session + WHERE parent_id IS NULL AND time_archived IS NULL ORDER BY time_updated DESC, id", + )?; + let rows = query.query_map([], |row| { + Ok(Session { + agent: Agent::Opencode, + id: row.get(0)?, + path: path.to_path_buf(), + cwd: row.get::<_, String>(1)?.into(), + title: compact(&row.get::<_, String>(2)?, 200), + modified: row.get::<_, i64>(3)?.max(0) as u64 / 1000, + messages: vec![], + }) + })?; + for row in rows { + let session = row?; + if session.id.is_empty() || !session.cwd.is_absolute() { + continue; + } + positions.insert(session.id.clone(), sessions.len()); + sessions.push(session); + } + } + { + let mut query = transaction.prepare( + "SELECT m.session_id, m.id, p.data FROM message m + JOIN part p ON p.message_id = m.id AND p.session_id = m.session_id + JOIN session s ON s.id = m.session_id + WHERE s.parent_id IS NULL AND s.time_archived IS NULL + AND json_valid(m.data) AND json_extract(m.data, '$.role') = 'user' + AND json_valid(p.data) AND json_extract(p.data, '$.type') = 'text' + ORDER BY m.session_id, m.time_created, m.id, p.time_created, p.id", + )?; + let mut rows = query.query([])?; + let mut previous = None; + let mut text = String::new(); + while let Some(row) = rows.next()? { + let session_id: String = row.get(0)?; + let Some(&index) = positions.get(&session_id) else { + continue; + }; + let message_id: String = row.get(1)?; + let key = (index, message_id); + if previous.as_ref() != Some(&key) { + if let Some((i, _)) = previous.take() { + push_human(&mut sessions[i].messages, &text); + } + text.clear(); + previous = Some(key); + } + let data: String = row.get(2)?; + let Ok(part) = serde_json::from_str::(&data) else { + continue; + }; + if !part.synthetic + && !part.ignored + && let Some(value) = part.text + { + if !text.is_empty() { + text.push('\n'); + } + text.push_str(&value); + } + } + if let Some((i, _)) = previous { + push_human(&mut sessions[i].messages, &text); + } + } + for session in &mut sessions { + if session.title.is_empty() { + session.title = session + .messages + .first() + .map(|s| compact(s, 160)) + .unwrap_or_else(|| "Untitled session".into()); + } + } + transaction.commit()?; + Ok(sessions) +} + +#[derive(Deserialize)] +struct TextPart { + text: Option, + #[serde(default)] + synthetic: bool, + #[serde(default)] + ignored: bool, +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use serde_json::json; + + pub fn database(path: &Path) -> Connection { + let conn = Connection::open(path).unwrap(); + conn.execute_batch("PRAGMA journal_mode=WAL; + CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT, title TEXT, time_updated INTEGER, parent_id TEXT, time_archived INTEGER); + CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT); + CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT); + INSERT INTO session VALUES ('ses_main', '/work', 'Database task', 10000, NULL, NULL); + INSERT INTO session VALUES ('ses_child', '/work', 'Child', 20000, 'ses_main', NULL); + INSERT INTO session VALUES ('ses_archived', '/work', 'Archived', 30000, NULL, 30000); + INSERT INTO message VALUES ('m1', 'ses_main', 1, '{\"role\":\"user\"}'); + INSERT INTO message VALUES ('m2', 'ses_main', 2, '{\"role\":\"assistant\"}'); + INSERT INTO message VALUES ('m3', 'ses_child', 1, '{\"role\":\"user\"}'); + ").unwrap(); + for (id, message, session, data) in [ + ( + "p1", + "m1", + "ses_main", + json!({"type":"text", "text":"hello"}), + ), + ( + "p2", + "m1", + "ses_main", + json!({"type":"text", "text":"world"}), + ), + ( + "p3", + "m1", + "ses_main", + json!({"type":"text", "text":"generated", "synthetic":true}), + ), + ( + "p4", + "m1", + "ses_main", + json!({"type":"text", "text":"ignored", "ignored":true}), + ), + ( + "p5", + "m1", + "ses_main", + json!({"type":"tool", "text":"tool output"}), + ), + ( + "p6", + "m2", + "ses_main", + json!({"type":"text", "text":"assistant secret"}), + ), + ( + "p7", + "m3", + "ses_child", + json!({"type":"text", "text":"subagent"}), + ), + ] { + conn.execute( + "INSERT INTO part VALUES (?1, ?2, ?3, 1, ?4)", + [id, message, session, &data.to_string()], + ) + .unwrap(); + } + conn + } + + #[test] + fn reads_wal_human_parts_only_without_modifying_database() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("opencode.db"); + let conn = database(&path); // Keep connection open: inserts live in WAL. + let before = std::fs::read(&path).unwrap(); + let sessions = read_database(&path).unwrap(); + assert_eq!(sessions.len(), 1); + assert_eq!(sessions[0].modified, 10); + assert_eq!(sessions[0].messages, ["hello\nworld"]); + assert_eq!(std::fs::read(&path).unwrap(), before); + assert_eq!( + conn.query_row("SELECT COUNT(*) FROM session", [], |r| r.get::<_, i64>(0)) + .unwrap(), + 3 + ); + } + + #[test] + fn missing_database_is_not_created() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("missing.db"); + assert!(read_database(&path).is_err()); + assert!(!path.exists()); + } +} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..2578550 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,416 @@ +//! Only human text and session metadata enter the cache. Large tool/image/assistant +//! payloads are skipped by serde rather than materialized as JSON trees. +use std::{ + collections::{HashMap, HashSet}, + fs::File, + io::{self, BufRead, BufReader}, + path::Path, +}; + +use serde::Deserialize; +use serde_json::{Value, value::RawValue}; + +use crate::session::{Agent, Session, compact}; + +#[derive(Default, Deserialize)] +#[serde(default, rename_all = "camelCase")] +struct Entry<'a> { + #[serde(rename = "type")] + kind: &'a str, + id: Option, + uuid: Option, + session_id: Option, + cwd: Option, + name: Option, + title: Option, + attribution: Option, + #[serde(borrow)] + content: Option<&'a RawValue>, + custom_title: Option, + ai_title: Option, + summary: Option, + is_meta: bool, + is_sidechain: bool, + #[serde(borrow)] + message: Option<&'a RawValue>, + #[serde(borrow)] + payload: Option<&'a RawValue>, + #[serde(borrow)] + origin: Option<&'a RawValue>, +} + +#[derive(Default, Deserialize)] +#[serde(default)] +struct Payload<'a> { + #[serde(rename = "type")] + kind: &'a str, + role: &'a str, + attribution: Option, + id: Option, + session_id: Option, + cwd: Option, + thread_source: &'a str, + #[serde(borrow)] + source: Option<&'a RawValue>, + #[serde(borrow)] + content: Option<&'a RawValue>, + message: Option, +} + +pub fn parse_file(agent: Agent, path: &Path, modified: u64) -> io::Result> { + parse( + agent, + path, + modified, + BufReader::with_capacity(128 * 1024, File::open(path)?), + ) +} + +fn parse( + agent: Agent, + path: &Path, + modified: u64, + mut reader: impl BufRead, +) -> io::Result> { + let mut session = Session { + agent, + id: String::new(), + path: path.to_path_buf(), + cwd: Default::default(), + title: String::new(), + modified, + messages: vec![], + }; + let mut seen = HashSet::new(); + let mut events = vec![]; + let mut custom_title = None; + let mut title_slot = None; + let mut ai_title = None; + let mut summary = None; + let mut line = String::new(); + loop { + line.clear(); + match reader.read_line(&mut line) { + Ok(0) => break, + Ok(_) => {} + Err(e) if e.kind() == io::ErrorKind::InvalidData => continue, + Err(e) => return Err(e), + } + // A live writer may leave a partial final record. It is retried next refresh. + let Ok(e) = serde_json::from_str::(&line) else { + continue; + }; + match agent { + Agent::Opencode => return Ok(None), // SQLite adapter; never JSONL. + Agent::Pi | Agent::Omp => match e.kind { + "session" if session.id.is_empty() => { + session.id = e.id.unwrap_or_default(); + session.cwd = e.cwd.unwrap_or_default().into(); + if agent == Agent::Omp { + custom_title = e.title; + } + } + "title" if agent == Agent::Omp => { + title_slot = e.title.filter(|t| !t.trim().is_empty()) + } + "title_change" if agent == Agent::Omp => custom_title = e.title, + "custom_message" + if agent == Agent::Omp && e.attribution.as_deref() == Some("user") => + { + push_text(&mut session.messages, e.content); + } + "session_info" => custom_title = e.name, + "message" => { + if let Some(raw) = e.message + && let Ok(p) = serde_json::from_str::(raw.get()) + && p.role == "user" + && p.attribution.as_deref() != Some("agent") + { + push_text(&mut session.messages, p.content); + } + } + _ => {} + }, + Agent::Claude => { + if e.is_sidechain { + continue; + } + if session.cwd.as_os_str().is_empty() { + session.cwd = e.cwd.unwrap_or_default().into(); + } + if session.id.is_empty() { + session.id = e.session_id.unwrap_or_default(); + } + match e.kind { + "custom-title" => custom_title = e.custom_title, + "ai-title" => ai_title = e.ai_title, + "summary" => summary = e.summary, + "user" if !e.is_meta => { + let nonhuman = e + .origin + .and_then(|r| serde_json::from_str::(r.get()).ok()) + .and_then(|v| v["kind"].as_str().map(|k| k != "human")) + .unwrap_or(false); + if nonhuman || e.uuid.is_some_and(|id| !seen.insert(id)) { + continue; + } + if let Some(raw) = e.message + && let Ok(p) = serde_json::from_str::(raw.get()) + && p.role == "user" + { + push_text(&mut session.messages, p.content); + } + } + _ => {} + } + } + Agent::Codex => { + if !matches!(e.kind, "session_meta" | "response_item" | "event_msg") { + continue; + } + let Some(raw) = e.payload else { continue }; + let Ok(p) = serde_json::from_str::(raw.get()) else { + continue; + }; + match e.kind { + "session_meta" if session.id.is_empty() => { + // Fork logs can contain their parent's header later: first header wins. + let source = p + .source + .and_then(|r| serde_json::from_str::(r.get()).ok()); + if p.thread_source == "subagent" + || source.as_ref().is_some_and(|v| { + v.get("subagent").is_some() || v.as_str() == Some("subagent") + }) + { + return Ok(None); + } + session.id = p.id.or(p.session_id).unwrap_or_default(); + session.cwd = p.cwd.unwrap_or_default().into(); + } + "event_msg" if p.kind == "user_message" => { + if let Some(text) = p.message { + push_human(&mut events, &text); + } + } + "response_item" if p.kind == "message" && p.role == "user" => { + if p.id.is_some_and(|id| !seen.insert(id)) { + continue; + } + push_text(&mut session.messages, p.content); + } + _ => {} + } + } + } + } + // Older Codex records input twice. Keep events, plus response-only prompts + // from newer turns in logs that have been resumed across CLI versions. + if !events.is_empty() { + let mut mirrored = HashMap::::new(); + for text in &events { + *mirrored.entry(text.clone()).or_default() += 1; + } + for text in session.messages { + let remaining = mirrored.entry(text.clone()).or_default(); + if *remaining > 0 { + *remaining -= 1; + } else { + events.push(text); + } + } + session.messages = events; + } + session.title = title_slot + .or(custom_title) + .or(ai_title) + .or(summary) + .filter(|s| !s.trim().is_empty()) + .map(|s| compact(&s, 200)) + .or_else(|| session.messages.first().map(|s| compact(s, 160))) + .unwrap_or_else(|| "Untitled session".into()); + if session.id.is_empty() || !session.cwd.is_absolute() { + return Ok(None); + } + Ok(Some(session)) +} + +#[derive(Deserialize)] +struct TextBlock { + #[serde(rename = "type")] + kind: String, + text: Option, +} + +fn push_text(messages: &mut Vec, raw: Option<&RawValue>) { + let Some(raw) = raw else { return }; + if raw.get().starts_with('"') { + if let Ok(text) = serde_json::from_str::(raw.get()) { + push_human(messages, &text); + } + } else if let Ok(blocks) = serde_json::from_str::>(raw.get()) { + // Ignore tool_result.content and image.data without allocating them. + let text = blocks + .iter() + .filter(|b| matches!(b.kind.as_str(), "text" | "input_text")) + .filter_map(|b| b.text.as_deref()) + .filter(|s| is_human(s)) + .collect::>() + .join("\n"); + push_human(messages, &text); + } +} + +pub(crate) fn push_human(messages: &mut Vec, text: &str) { + if is_human(text) { + // Terminal escape/control sequences from stored content must never be emitted. + messages.push( + text.trim() + .chars() + .filter(|c| !c.is_control() || matches!(c, '\n' | '\t')) + .collect(), + ); + } +} + +fn is_human(text: &str) -> bool { + let text = text.trim(); + !text.is_empty() + && ![ + "# AGENTS.md instructions", + "", + "", + "", + "", + "", + "", + "", + "", + "[Request interrupted by user", + "This session is being continued from a previous conversation", + "", + "", + ] + .iter() + .any(|prefix| text.starts_with(prefix)) +} + +#[cfg(test)] +mod tests { + use super::*; + fn fixture(agent: Agent, input: &str) -> Session { + parse( + agent, + Path::new("/tmp/session.jsonl"), + 42, + io::Cursor::new(input), + ) + .unwrap() + .unwrap() + } + #[test] + fn pi_latest_title_and_only_human_text() { + let s = fixture( + Agent::Pi, + r#"{"type":"session","id":"p","cwd":"/work"} +{"type":"session_info","name":"Old"} +{"type":"message","message":{"role":"user","content":[{"type":"text","text":"fix auth"},{"type":"image","data":"secret"}]}} +{"type":"message","message":{"role":"toolResult","content":"not searchable"}} +{"type":"message","message":{"role":"assistant","content":"not searchable"}} +{"type":"session_info","name":"New"} +not json +{"type":"message","message": +"#, + ); + assert_eq!(s.title, "New"); + assert_eq!(s.messages, ["fix auth"]); + } + #[test] + fn omp_title_slot_header_and_human_attribution() { + let s = fixture( + Agent::Omp, + r#"{"type":"title","title":"Current title"} +{"type":"session","id":"omp-id","cwd":"/work","title":"Legacy title"} +{"type":"message","message":{"role":"user","attribution":"user","content":[{"type":"text","text":"real request"}]}} +{"type":"message","message":{"role":"user","attribution":"agent","content":"injected request"}} +{"type":"custom_message","attribution":"user","content":"user skill request"} +{"type":"title_change","title":"Old title event"} +"#, + ); + assert_eq!(s.agent, Agent::Omp); + assert_eq!(s.title, "Current title"); + assert_eq!(s.messages, ["real request", "user skill request"]); + let s = fixture( + Agent::Omp, + r#"{"type":"session","id":"old","cwd":"/work","title":"Legacy title"}"#, + ); + assert_eq!(s.title, "Legacy title"); + let s = fixture( + Agent::Omp, + r#"{"type":"title","title":""} +{"type":"session","id":"old","cwd":"/work"} +{"type":"title_change","title":"Renamed"} +"#, + ); + assert_eq!(s.title, "Renamed"); + } + #[test] + fn claude_titles_dedup_and_generated_records() { + let s = fixture( + Agent::Claude, + r#"{"type":"user","uuid":"1","sessionId":"c","cwd":"/work","message":{"role":"user","content":"hello"}} +{"type":"user","uuid":"1","message":{"role":"user","content":"hello"}} +{"type":"user","isMeta":true,"message":{"role":"user","content":"injected"}} +{"type":"user","origin":{"kind":"agent"},"message":{"role":"user","content":"injected"}} +{"type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"tool output"}]}} +{"type":"user","message":{"role":"user","content":"background"}} +{"type":"custom-title","customTitle":"Custom title"} +{"type":"ai-title","aiTitle":"Generated title"} +"#, + ); + assert_eq!(s.title, "Custom title"); + assert_eq!(s.messages, ["hello"]); + } + #[test] + fn codex_events_and_responses_are_not_double_counted() { + let s = fixture( + Agent::Codex, + r##"{"type":"session_meta","payload":{"id":"x","cwd":"/work","source":"cli"}} +{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"# AGENTS.md instructions for /work"}]}} +{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"real prompt"}]}} +{"type":"event_msg","payload":{"type":"user_message","message":"real prompt"}} +{"type":"session_meta","payload":{"id":"parent","cwd":"/wrong"}} +"##, + ); + assert_eq!(s.id, "x"); + assert_eq!(s.cwd, Path::new("/work")); + assert_eq!(s.messages, ["real prompt"]); + } + #[test] + fn codex_mixed_versions_keep_new_response_only_prompts() { + let s = fixture( + Agent::Codex, + r#"{"type":"session_meta","payload":{"id":"x","cwd":"/work"}} +{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}} +{"type":"event_msg","payload":{"type":"user_message","message":"old prompt"}} +{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}} +"#, + ); + assert_eq!(s.messages, ["old prompt", "new prompt"]); + } + #[test] + fn modern_codex_response_fallback() { + let s = fixture( + Agent::Codex, + r#"{"type":"session_meta","payload":{"id":"x","cwd":"/work"}} +{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"injected"},{"type":"input_text","text":"日本語 request"}]}} +"#, + ); + assert_eq!(s.messages, ["日本語 request"]); + } + #[test] + fn excludes_codex_subagents() { + let s = parse(Agent::Codex, Path::new("x"), 0, io::Cursor::new(r#"{"type":"session_meta","payload":{"id":"child","cwd":"/work","source":{"subagent":{}}}}"#)).unwrap(); + assert!(s.is_none()); + } +} diff --git a/src/search.rs b/src/search.rs new file mode 100644 index 0000000..e4759db --- /dev/null +++ b/src/search.rs @@ -0,0 +1,201 @@ +use std::{cmp::Reverse, path::Path}; + +use crate::session::{Agent, Session}; +use nucleo_matcher::{ + Config, Matcher, Utf32Str, + pattern::{CaseMatching, Normalization, Pattern}, +}; + +pub struct SearchIndex { + pub sessions: Vec, + text: Vec, +} +struct SearchText { + title: String, + messages: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MatchKind { + Recent, + Title, + Message, +} +#[derive(Clone, Debug)] +pub struct Hit { + pub index: usize, + pub kind: MatchKind, + pub message: Option, + score: u32, + tier: u8, +} + +impl SearchIndex { + pub fn new(sessions: Vec) -> Self { + let text = sessions + .iter() + .map(|s| SearchText { + title: s.title.to_lowercase(), + messages: s.messages.iter().map(|s| s.to_lowercase()).collect(), + }) + .collect(); + Self { sessions, text } + } + + pub fn search(&self, query: &str, agent: Option, cwd: Option<&Path>) -> Vec { + let query = query.trim().to_lowercase(); + let words: Vec<_> = query.split_whitespace().collect(); + let pattern = Pattern::new( + &query, + CaseMatching::Ignore, + Normalization::Smart, + nucleo_matcher::pattern::AtomKind::Fuzzy, + ); + let mut matcher = Matcher::new(Config::DEFAULT); + let mut buffer = vec![]; + let mut hits = vec![]; + for (index, (session, text)) in self.sessions.iter().zip(&self.text).enumerate() { + if agent.is_some_and(|a| a != session.agent) || cwd.is_some_and(|p| p != session.cwd) { + continue; + } + let mut hit = Hit { + index, + kind: MatchKind::Recent, + message: None, + score: 0, + tier: 0, + }; + if !query.is_empty() { + if text.title.contains(&query) { + hit.kind = MatchKind::Title; + hit.tier = 4; + hit.score = if text.title == query { + 2 + } else if text.title.starts_with(&query) { + 1 + } else { + 0 + }; + } else if words.iter().all(|w| text.title.contains(w)) { + hit.kind = MatchKind::Title; + hit.tier = 3; + } else if let Some(score) = + pattern.score(Utf32Str::new(&session.title, &mut buffer), &mut matcher) + { + hit.kind = MatchKind::Title; + hit.tier = 2; + hit.score = score; + } else { + let mut found = None; + for (i, message) in text.messages.iter().enumerate() { + if message.contains(&query) { + found = Some((i, 1)); + break; + } + if found.is_none() && words.iter().all(|w| message.contains(w)) { + found = Some((i, 0)); + } + } + let Some((i, score)) = found else { continue }; + hit.kind = MatchKind::Message; + hit.tier = 1; + hit.score = score; + hit.message = Some(i); + } + } + hits.push(hit); + } + hits.sort_by_key(|h| { + ( + Reverse(h.tier), + Reverse(h.score), + Reverse(self.sessions[h.index].modified), + h.index, + ) + }); + hits + } +} + +/// Context around the matching portion, not just the start of a long prompt. +pub fn snippet(text: &str, query: &str, max_chars: usize) -> String { + let lower = text.to_lowercase(); + let query = query.to_lowercase(); + let pos = query + .split_whitespace() + .filter_map(|w| lower.find(w)) + .min() + .unwrap_or(0); + // Lowercasing can change byte lengths. Convert to a character offset first. + let char_pos = lower[..pos].chars().count(); + let start = char_pos.saturating_sub(70); + let mut output = if start > 0 { + "…".to_owned() + } else { + String::new() + }; + output.extend(text.chars().skip(start).take(max_chars)); + if text.chars().count() > start + max_chars { + output.push('…'); + } + output +} + +#[cfg(test)] +mod tests { + use super::*; + fn session(title: &str, message: &str, modified: u64) -> Session { + Session { + agent: Agent::Pi, + id: title.into(), + path: "/x".into(), + cwd: "/work".into(), + title: title.into(), + modified, + messages: vec![message.into()], + } + } + #[test] + fn title_hits_always_beat_body_hits_and_recency_breaks_ties() { + let index = SearchIndex::new(vec![ + session("Other", "authentication failure", 999), + session("Authentication", "", 1), + session("Auth guide", "", 2), + ]); + let hits = index.search("auth", None, None); + assert_eq!(hits.iter().map(|h| h.index).collect::>(), [2, 1, 0]); + assert_eq!(hits[2].kind, MatchKind::Message); + } + #[test] + fn fuzzy_titles_and_unordered_message_tokens() { + let index = SearchIndex::new(vec![ + session("Authentication refactor", "", 1), + session("Other", "cache invalidation fix", 2), + ]); + assert_eq!(index.search("athref", None, None)[0].index, 0); + let hits = index.search("fix cache", None, None); + assert_eq!(hits[0].index, 1); + assert_eq!(hits[0].message, Some(0)); + assert!(index.search("no match at all", None, None).is_empty()); + } + #[test] + fn scope_is_exact_and_filters_compose() { + let index = SearchIndex::new(vec![session("title", "text", 1)]); + assert!(index.search("", Some(Agent::Claude), None).is_empty()); + assert!(index.search("", None, Some(Path::new("/wor"))).is_empty()); + assert_eq!( + index + .search("", Some(Agent::Pi), Some(Path::new("/work"))) + .len(), + 1 + ); + } + #[test] + fn unicode_and_match_context() { + let index = SearchIndex::new(vec![session("École 日本語", "the café is open", 1)]); + assert_eq!(index.search("日本", None, None).len(), 1); + assert_eq!(index.search("CAFÉ", None, None).len(), 1); + let text = format!("{}needle", "x".repeat(1000)); + assert!(snippet(&text, "needle", 100).contains("needle")); + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..9de274b --- /dev/null +++ b/src/session.rs @@ -0,0 +1,206 @@ +use std::{ + ffi::OsString, + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::{Context, Result, bail}; +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, ValueEnum)] +#[serde(rename_all = "lowercase")] +pub enum Agent { + Pi, + Claude, + Codex, + Opencode, + Omp, +} + +impl Agent { + pub const ALL: [Self; 5] = [ + Self::Pi, + Self::Claude, + Self::Codex, + Self::Opencode, + Self::Omp, + ]; + + pub fn next_filter(current: Option) -> Option { + match current { + None => Some(Self::ALL[0]), + Some(agent) => Self::ALL + .iter() + .position(|a| *a == agent) + .and_then(|i| Self::ALL.get(i + 1)) + .copied(), + } + } + pub fn name(self) -> &'static str { + match self { + Self::Pi => "pi", + Self::Claude => "claude", + Self::Codex => "codex", + Self::Opencode => "opencode", + Self::Omp => "omp", + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Session { + pub agent: Agent, + pub id: String, + pub path: PathBuf, + pub cwd: PathBuf, + pub title: String, + pub modified: u64, + pub messages: Vec, +} + +impl Session { + pub fn args(&self) -> Vec { + match self.agent { + Agent::Pi => vec!["--session".into(), self.path.as_os_str().into()], + Agent::Claude => vec!["--resume".into(), self.id.clone().into()], + Agent::Codex => vec!["resume".into(), self.id.clone().into()], + Agent::Opencode => vec!["--session".into(), self.id.clone().into()], + Agent::Omp => vec!["--resume".into(), self.path.as_os_str().into()], + } + } + + pub fn shell_command(&self) -> String { + let args = self + .args() + .iter() + .map(|a| quote(&a.to_string_lossy())) + .collect::>() + .join(" "); + format!( + "cd -- {} && {}{} {}", + quote(&self.cwd.to_string_lossy()), + if self.agent == Agent::Opencode { + format!("OPENCODE_DB={} ", quote(&self.path.to_string_lossy())) + } else { + String::new() + }, + self.agent.name(), + args + ) + } + + pub fn resume(&self) -> Result<()> { + if !self.cwd.is_dir() { + bail!("Session directory no longer exists: {}", self.cwd.display()); + } + if !self.path.is_file() { + bail!("Session file no longer exists: {}", self.path.display()); + } + let mut command = Command::new(self.agent.name()); + command.args(self.args()).current_dir(&self.cwd); + if self.agent == Agent::Opencode { + // Explicitly select the discovered database, including non-default/channel stores. + command.env("OPENCODE_DB", &self.path); + } + // Do not pass any flags that bypass the agent's permission/trust checks. + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + Err(command.exec()) + .with_context(|| format!("Could not start {}; is it on PATH?", self.agent.name())) + } + #[cfg(not(unix))] + { + let status = command.status().context("Could not start agent")?; + std::process::exit(status.code().unwrap_or(1)); + } + } +} + +fn quote(s: &str) -> String { + format!("'{}'", s.replace('\'', "'\\''")) +} + +pub fn canonical(path: &Path) -> PathBuf { + path.canonicalize().unwrap_or_else(|_| path.to_path_buf()) +} + +pub fn compact(s: &str, limit: usize) -> String { + let text = s.split_whitespace().collect::>().join(" "); + let mut chars = text.chars(); + let mut result: String = chars + .by_ref() + .take(limit) + .filter(|c| !c.is_control()) + .collect(); + if chars.next().is_some() { + result.push('…'); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + pub fn fixture(agent: Agent) -> Session { + Session { + agent, + id: "uuid".into(), + path: "/tmp/a b.jsonl".into(), + cwd: "/tmp/project's dir".into(), + title: "Example".into(), + modified: 1, + messages: vec![], + } + } + #[test] + fn commands_preserve_arguments_and_quote_paths() { + let mut s = fixture(Agent::Pi); + assert_eq!( + s.args(), + vec![ + OsString::from("--session"), + OsString::from("/tmp/a b.jsonl") + ] + ); + assert!(s.shell_command().contains("project'\\''s dir")); + s.agent = Agent::Claude; + assert_eq!( + s.args(), + vec![OsString::from("--resume"), OsString::from("uuid")] + ); + s.agent = Agent::Codex; + assert_eq!( + s.args(), + vec![OsString::from("resume"), OsString::from("uuid")] + ); + } + #[test] + fn new_agent_commands_and_filter_cycle() { + let s = fixture(Agent::Omp); + assert_eq!( + s.args(), + [OsString::from("--resume"), OsString::from("/tmp/a b.jsonl")] + ); + let s = fixture(Agent::Opencode); + assert_eq!( + s.args(), + [OsString::from("--session"), OsString::from("uuid")] + ); + assert!( + s.shell_command() + .contains("OPENCODE_DB='/tmp/a b.jsonl' opencode '--session' 'uuid'") + ); + let mut selected = None; + for agent in Agent::ALL { + selected = Agent::next_filter(selected); + assert_eq!(selected, Some(agent)); + } + assert_eq!(Agent::next_filter(selected), None); + } + #[test] + fn compact_is_unicode_safe() { + assert_eq!(compact(" 日本語\n test", 3), "日本語…"); + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..8f5a1a0 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,738 @@ +use std::{ + collections::BTreeMap, + io::{self, IsTerminal}, + path::PathBuf, + sync::mpsc::{self, Receiver}, + thread, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +use anyhow::{Result, bail}; +use crossterm::{ + cursor::Show, + event::{ + self, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyEvent, KeyEventKind, + KeyModifiers, + }, + execute, + terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + prelude::*, + widgets::{ + Block, Borders, Cell, Clear, List, ListItem, ListState, Paragraph, Row, Table, TableState, + Wrap, + }, +}; + +use crate::{ + index::{self, Report, Sources, Update}, + search::{Hit, MatchKind, SearchIndex, snippet}, + session::{Agent, Session, compact}, +}; + +#[cfg(test)] +mod demo; + +const ACCENT: Color = Color::Cyan; +const DIM: Color = Color::DarkGray; + +struct TerminalGuard; +impl TerminalGuard { + fn enter() -> Result { + terminal::enable_raw_mode()?; + let guard = Self; + execute!(io::stdout(), EnterAlternateScreen, EnableBracketedPaste)?; + Ok(guard) + } +} +impl Drop for TerminalGuard { + fn drop(&mut self) { + restore(); + } +} +fn restore() { + let _ = terminal::disable_raw_mode(); + let _ = execute!( + io::stdout(), + DisableBracketedPaste, + LeaveAlternateScreen, + Show + ); +} + +enum WorkerUpdate { + Index(Update), + Done(Result), +} +fn load(sources: Sources, rebuild: bool, no_cache: bool) -> Receiver { + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + let report = index::refresh(&sources, rebuild, no_cache, |u| { + let _ = tx.send(WorkerUpdate::Index(u)); + }); + let _ = tx.send(WorkerUpdate::Done(report)); + }); + rx +} + +struct Locations { + query: String, + selected: usize, + state: ListState, +} + +struct App { + index: SearchIndex, + query: String, + agent: Option, + folder: PathBuf, + all: bool, + hits: Vec, + selected: usize, + table: TableState, + locations: Option, + status: String, + loading: bool, + elapsed: Duration, +} + +impl App { + fn search(&mut self, preserve: bool) { + let previous = if preserve { + self.hits.get(self.selected).map(|h| { + ( + self.index.sessions[h.index].agent, + self.index.sessions[h.index].id.clone(), + ) + }) + } else { + None + }; + let start = Instant::now(); + self.hits = self.index.search( + &self.query, + self.agent, + if self.all { None } else { Some(&self.folder) }, + ); + self.elapsed = start.elapsed(); + self.selected = previous + .and_then(|(a, id)| { + self.hits.iter().position(|h| { + let s = &self.index.sessions[h.index]; + s.agent == a && s.id == id + }) + }) + .unwrap_or(0); + self.table.select(if self.hits.is_empty() { + None + } else { + Some(self.selected) + }); + } + + fn replace(&mut self, sessions: Vec) { + let previous = self.hits.get(self.selected).map(|h| { + ( + self.index.sessions[h.index].agent, + self.index.sessions[h.index].id.clone(), + ) + }); + self.index = SearchIndex::new(sessions); + self.search(false); + if let Some((a, id)) = previous + && let Some(i) = self.hits.iter().position(|h| { + let s = &self.index.sessions[h.index]; + s.agent == a && s.id == id + }) + { + self.selected = i; + self.table.select(Some(i)); + } + } + + fn move_by(&mut self, amount: isize) { + self.selected = self + .selected + .saturating_add_signed(amount) + .min(self.hits.len().saturating_sub(1)); + self.table.select(if self.hits.is_empty() { + None + } else { + Some(self.selected) + }); + } + + fn location_rows(&self) -> Vec<(PathBuf, usize)> { + let mut counts = BTreeMap::::new(); + for session in &self.index.sessions { + if self.agent.is_none_or(|a| a == session.agent) { + *counts.entry(session.cwd.clone()).or_default() += 1; + } + } + let query = self + .locations + .as_ref() + .map(|l| l.query.to_lowercase()) + .unwrap_or_default(); + let mut rows: Vec<_> = counts + .into_iter() + .filter(|(p, _)| { + query + .split_whitespace() + .all(|w| p.to_string_lossy().to_lowercase().contains(w)) + }) + .collect(); + rows.sort_by(|a, b| { + (a.0 != self.folder) + .cmp(&(b.0 != self.folder)) + .then_with(|| b.1.cmp(&a.1)) + .then_with(|| a.0.cmp(&b.0)) + }); + rows + } + + fn draw(&mut self, frame: &mut Frame) { + let area = frame.area(); + if area.width < 35 || area.height < 10 { + frame.render_widget( + Paragraph::new("Enlarge terminal (35×10 minimum)\nEsc / Ctrl-C to quit"), + area, + ); + return; + } + let preview_height = if area.height >= 22 { 7 } else { 0 }; + let chunks = Layout::vertical([ + Constraint::Length(2), + Constraint::Length(3), + Constraint::Min(3), + Constraint::Length(preview_height), + Constraint::Length(2), + ]) + .split(area); + let mut header = vec![Span::styled(" cont ", Style::default().fg(ACCENT).bold())]; + let filters: Vec<_> = if area.width < 80 { + vec![self.agent] + } else { + std::iter::once(None) + .chain(Agent::ALL.into_iter().map(Some)) + .collect() + }; + for agent in filters { + let count = self + .index + .sessions + .iter() + .filter(|s| { + agent.is_none_or(|a| a == s.agent) && (self.all || s.cwd == self.folder) + }) + .count(); + let label = format!(" {} {} ", agent.map_or("all", Agent::name), count); + header.push(Span::styled( + label, + if agent == self.agent { + Style::default().fg(Color::Black).bg(ACCENT).bold() + } else { + Style::default().fg(DIM) + }, + )); + header.push(Span::raw(" ")); + } + frame.render_widget( + Paragraph::new(Line::from(header)).wrap(Wrap { trim: true }), + chunks[0], + ); + let scope = if self.all { + "All locations".to_owned() + } else { + path_tail(&self.folder, chunks[1].width.saturating_sub(35) as usize) + }; + let label = format!( + " {} · {} results · {:.1} ms ", + scope, + self.hits.len(), + self.elapsed.as_secs_f64() * 1000.0 + ); + // Show the tail of long queries so the insertion point remains visible. + let max = chunks[1].width.saturating_sub(6) as usize / 2; + let visible: String = self + .query + .chars() + .skip(self.query.chars().count().saturating_sub(max)) + .collect(); + frame.render_widget( + Paragraph::new(Line::from(vec![ + Span::styled("› ", Style::default().fg(ACCENT)), + Span::raw(visible), + Span::styled("▏", Style::default().fg(ACCENT)), + ])) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(DIM)) + .title(label), + ), + chunks[1], + ); + let wide = area.width >= 90; + let location_width = (area.width.saturating_sub(25) as usize * 2 / 5).saturating_sub(2); + let rows: Vec<_> = self + .hits + .iter() + .map(|hit| { + let s = &self.index.sessions[hit.index]; + let mut cells = vec![ + Cell::from(s.agent.name()).style(Style::default().fg(agent_color(s.agent))), + Cell::from(s.title.as_str()), + ]; + if wide { + cells.push( + Cell::from(path_tail(&s.cwd, location_width)) + .style(Style::default().fg(DIM)), + ); + } + cells.push(Cell::from(age(s.modified)).style(Style::default().fg(DIM))); + cells.push( + Cell::from(if hit.kind == MatchKind::Message { + "text" + } else { + "" + }) + .style(Style::default().fg(ACCENT)), + ); + Row::new(cells) + }) + .collect(); + let mut widths = vec![Constraint::Length(8), Constraint::Fill(3)]; + let mut labels = vec!["Agent", "Session"]; + if wide { + widths.push(Constraint::Fill(2)); + labels.push("Location"); + } + widths.extend([Constraint::Length(5), Constraint::Length(4)]); + labels.extend(["Age", "Hit"]); + let table = Table::new(rows, widths) + .header( + Row::new(labels) + .style(Style::default().fg(DIM)) + .bottom_margin(1), + ) + .row_highlight_style(Style::default().bg(Color::DarkGray).fg(Color::White).bold()) + .highlight_symbol("› ") + .column_spacing(2); + frame.render_stateful_widget(table, chunks[2], &mut self.table); + if self.hits.is_empty() { + let text = if self.loading && self.index.sessions.is_empty() { + "Reading local session history…" + } else if !self.all { + "No matches here. Tab searches all locations." + } else { + "No matching sessions. Clear search or change agent with Shift-Tab." + }; + frame.render_widget( + Paragraph::new(text).style(Style::default().fg(DIM)), + Rect { + y: chunks[2].y + 2, + height: chunks[2].height.saturating_sub(2), + ..chunks[2] + }, + ); + } + if let Some(hit) = self.hits.get(self.selected) { + let s = &self.index.sessions[hit.index]; + let message = hit + .message + .or_else(|| s.messages.len().checked_sub(1)) + .and_then(|i| s.messages.get(i)); + let preview = message + .map(|m| snippet(m, &self.query, 1800)) + .unwrap_or_else(|| "No human messages recorded.".into()); + let kind = if hit.kind == MatchKind::Message { + "Matching human message" + } else { + "Latest human message" + }; + let lines = vec![ + Line::styled( + format!( + "{} · {} · {} {}", + display_path(&s.cwd), + s.id, + s.messages.len(), + if s.messages.len() == 1 { + "message" + } else { + "messages" + } + ), + Style::default().fg(DIM), + ), + Line::raw(preview), + ]; + frame.render_widget( + Paragraph::new(lines).wrap(Wrap { trim: false }).block( + Block::default() + .borders(Borders::TOP) + .border_style(Style::default().fg(DIM)) + .title(format!(" {kind} ")), + ), + chunks[3], + ); + } + frame.render_widget(Paragraph::new(vec![Line::styled(" Enter resume ↑↓ move Tab here/all Shift-Tab agent Ctrl-L locations Esc clear/quit", Style::default().fg(DIM)), Line::styled(format!(" {}{}", if self.loading { "⟳ " } else { "" }, self.status), Style::default().fg(DIM))]), chunks[4]); + if self.locations.is_some() { + self.draw_locations(frame); + } + } + + fn draw_locations(&mut self, frame: &mut Frame) { + let rows = self.location_rows(); + let locations = self.locations.as_mut().unwrap(); + locations.selected = locations.selected.min(rows.len().saturating_sub(1)); + locations.state.select(if rows.is_empty() { + None + } else { + Some(locations.selected) + }); + let outer = frame.area(); + let area = Rect { + x: outer.x + 2, + y: outer.y + 2, + width: outer.width.saturating_sub(4), + height: outer.height.saturating_sub(4), + }; + frame.render_widget(Clear, area); + let block = Block::default() + .borders(Borders::ALL) + .title(" Locations · Enter select · Tab all · Esc close ") + .border_style(Style::default().fg(ACCENT)); + let inner = block.inner(area); + frame.render_widget(block, area); + let chunks = Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).split(inner); + frame.render_widget(Paragraph::new(format!("› {}▏", locations.query)), chunks[0]); + let items = rows + .iter() + .map(|(path, count)| ListItem::new(format!("{:>4} {}", count, display_path(path)))); + frame.render_stateful_widget( + List::new(items) + .highlight_symbol("› ") + .highlight_style(Style::default().bg(DIM).fg(Color::White)), + chunks[1], + &mut locations.state, + ); + } + + fn location_key(&mut self, key: KeyEvent) { + let rows = self.location_rows(); + let locations = self.locations.as_mut().unwrap(); + match key.code { + KeyCode::Esc => self.locations = None, + KeyCode::Tab => { + self.all = true; + self.locations = None; + self.search(false); + } + KeyCode::Enter => { + if let Some((path, _)) = rows.get(locations.selected) { + self.folder = path.clone(); + self.all = false; + self.locations = None; + self.search(false); + } + } + KeyCode::Up => locations.selected = locations.selected.saturating_sub(1), + KeyCode::Down => { + locations.selected = (locations.selected + 1).min(rows.len().saturating_sub(1)) + } + _ => { + if edit_query(&mut locations.query, key) { + locations.selected = 0; + } + } + } + } +} + +pub struct Options { + pub folder: PathBuf, + pub all: bool, + pub agent: Option, + pub query: String, + pub rebuild: bool, + pub no_cache: bool, +} + +pub fn run(sources: Sources, options: Options) -> Result> { + if !io::stdin().is_terminal() || !io::stdout().is_terminal() { + bail!("The picker needs a terminal. Use --list or --json for non-interactive output."); + } + let hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + restore(); + hook(info); + })); + let _guard = TerminalGuard::enter()?; + let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; + terminal.clear()?; + let mut receiver = load(sources.clone(), options.rebuild, options.no_cache); + let mut app = App { + index: SearchIndex::new(vec![]), + query: options.query, + agent: options.agent, + folder: options.folder, + all: options.all, + hits: vec![], + selected: 0, + table: TableState::default(), + locations: None, + status: "Loading…".into(), + loading: true, + elapsed: Duration::ZERO, + }; + let mut dirty = true; + loop { + while let Ok(update) = receiver.try_recv() { + dirty = true; + match update { + WorkerUpdate::Index(Update::Snapshot(sessions)) => app.replace(sessions), + WorkerUpdate::Index(Update::Progress(status)) => app.status = status, + WorkerUpdate::Done(result) => { + app.loading = false; + match result { + Ok(report) => { + app.status = if report.warnings.is_empty() { + format!( + "{} sessions · {} refreshed · Ctrl-R refresh", + report.sessions.len(), + report.parsed + ) + } else { + format!("Warning: {}", report.warnings.join("; ")) + }; + app.replace(report.sessions); + } + Err(e) => app.status = format!("Error: {e:#} · Ctrl-R retry"), + } + } + } + } + if dirty { + terminal.draw(|f| app.draw(f))?; + dirty = false; + } + if !event::poll(Duration::from_millis(30))? { + continue; + } + match event::read()? { + Event::Key(key) if key.kind != KeyEventKind::Release => { + dirty = true; + if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { + return Ok(None); + } + if app.locations.is_some() { + app.location_key(key); + continue; + } + match key.code { + KeyCode::Esc if app.query.is_empty() => return Ok(None), + KeyCode::Esc => { + app.query.clear(); + app.search(false); + } + KeyCode::Enter => { + if let Some(hit) = app.hits.get(app.selected) { + let session = &app.index.sessions[hit.index]; + if !session.cwd.is_dir() { + app.status = format!( + "Directory no longer exists: {}", + session.cwd.display() + ); + } else if !session.path.is_file() { + app.status = "Session file disappeared; Ctrl-R to refresh".into(); + } else { + return Ok(Some(session.clone())); + } + } + } + KeyCode::Tab => { + app.all = !app.all; + app.search(false); + } + KeyCode::BackTab => { + app.agent = Agent::next_filter(app.agent); + app.search(false); + } + KeyCode::Up => app.move_by(-1), + KeyCode::Down => app.move_by(1), + KeyCode::PageUp => app.move_by(-10), + KeyCode::PageDown => app.move_by(10), + KeyCode::Char(c) if key.modifiers.contains(KeyModifiers::CONTROL) => match c { + 'n' => app.move_by(1), + 'p' => app.move_by(-1), + 'l' => { + app.locations = Some(Locations { + query: String::new(), + selected: 0, + state: ListState::default(), + }) + } + 'r' if !app.loading => { + receiver = load(sources.clone(), false, options.no_cache); + app.loading = true; + } + _ => { + if edit_query(&mut app.query, key) { + app.search(false); + } + } + }, + _ => { + if edit_query(&mut app.query, key) { + app.search(false); + } + } + } + } + Event::Paste(text) => { + let text = compact(&text, 500); + if let Some(l) = &mut app.locations { + l.query.push_str(&text); + l.selected = 0; + } else { + app.query.push_str(&text); + app.search(false); + } + dirty = true; + } + Event::Resize(..) => dirty = true, + _ => {} + } + } +} + +fn edit_query(query: &mut String, key: KeyEvent) -> bool { + match (key.code, key.modifiers.contains(KeyModifiers::CONTROL)) { + (KeyCode::Char('u'), true) => query.clear(), + (KeyCode::Char('w'), true) => { + while query.ends_with(char::is_whitespace) { + query.pop(); + } + while !query.is_empty() && !query.ends_with(char::is_whitespace) { + query.pop(); + } + } + (KeyCode::Backspace, _) => { + query.pop(); + } + (KeyCode::Char(c), false) + if !c.is_control() && !key.modifiers.contains(KeyModifiers::ALT) => + { + query.push(c) + } + _ => return false, + } + true +} + +fn display_path(path: &std::path::Path) -> String { + if let Some(home) = dirs::home_dir() + && let Ok(rest) = path.strip_prefix(home) + { + return format!("~/{}", rest.display()); + } + compact(&path.to_string_lossy(), 300) +} +fn path_tail(path: &std::path::Path, width: usize) -> String { + let text = display_path(path); + let len = text.chars().count(); + if len <= width { + return text; + } + format!( + "…{}", + text.chars() + .skip(len.saturating_sub(width.saturating_sub(1))) + .collect::() + ) +} + +fn agent_color(agent: Agent) -> Color { + match agent { + Agent::Pi => Color::Magenta, + Agent::Claude => Color::Yellow, + Agent::Codex => Color::Green, + Agent::Opencode => Color::Blue, + Agent::Omp => Color::LightMagenta, + } +} +fn age(modified: u64) -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let seconds = now.saturating_sub(modified); + if seconds < 60 { + "now".into() + } else if seconds < 3600 { + format!("{}m", seconds / 60) + } else if seconds < 86400 { + format!("{}h", seconds / 3600) + } else { + format!("{}d", seconds / 86400) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn query_edits_unicode_without_panicking() { + let mut query = "日本語 café".into(); + edit_query( + &mut query, + KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), + ); + assert_eq!(query, "日本語 caf"); + edit_query( + &mut query, + KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL), + ); + assert_eq!(query, "日本語 "); + edit_query( + &mut query, + KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL), + ); + assert_eq!(query, ""); + } + #[test] + fn renders_small_and_normal_terminals() { + let mut app = App { + index: SearchIndex::new(vec![]), + query: "query".into(), + agent: None, + folder: "/work".into(), + all: false, + hits: vec![], + selected: 0, + table: TableState::default(), + locations: None, + status: "ready".into(), + loading: false, + elapsed: Duration::ZERO, + }; + for (width, height) in [(20, 5), (35, 10), (80, 24), (140, 40)] { + let backend = ratatui::backend::TestBackend::new(width, height); + let mut terminal = Terminal::new(backend).unwrap(); + terminal.draw(|f| app.draw(f)).unwrap(); + if width >= 35 { + app.locations = Some(Locations { + query: "".into(), + selected: 0, + state: ListState::default(), + }); + terminal.draw(|f| app.draw(f)).unwrap(); + app.locations = None; + } + } + } +} diff --git a/src/ui/demo.rs b/src/ui/demo.rs new file mode 100644 index 0000000..12694b7 --- /dev/null +++ b/src/ui/demo.rs @@ -0,0 +1,178 @@ +//! Documentation rendering from fictional in-memory sessions only. +//! This module is compiled for tests, never into the installed binary. +use super::*; +use serde_json::json; + +#[test] +#[ignore = "writes target/demo-screen.json for the documentation renderer"] +fn render_demo() { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let examples = [ + ( + Agent::Pi, + "Auth token refresh", + "atlas-api", + 120, + "Refresh the auth token before retrying a failed request.", + ), + ( + Agent::Claude, + "Auth middleware cleanup", + "tinyshop", + 1080, + "Keep the auth middleware small and easy to test.", + ), + ( + Agent::Codex, + "Auth callback tests", + "atlas-api", + 3600, + "Add tests for the auth callback and expired tokens.", + ), + ( + Agent::Omp, + "Auth cookies across subdomains", + "paperplane", + 10800, + "Check the auth cookie settings on the staging subdomain.", + ), + ( + Agent::Opencode, + "Auth flow for the demo app", + "compass", + 86400, + "Build a minimal auth flow for the fictional demo app.", + ), + ( + Agent::Pi, + "Fix login redirect loop", + "atlas-api", + 720, + "The auth callback loses the return URL after a token refresh. Please add a regression test, then fix the redirect without changing the public API.", + ), + ( + Agent::Opencode, + "Session expiry edge cases", + "tinyshop", + 5400, + "Check the auth expiry boundary when the client clock is slightly ahead.", + ), + ( + Agent::Omp, + "Add sign-in regression tests", + "paperplane", + 14400, + "Exercise the auth callback with a missing state parameter.", + ), + ( + Agent::Claude, + "Replace the legacy login screen", + "compass", + 172800, + "Keep the existing auth contract while simplifying the login screen.", + ), + ( + Agent::Pi, + "Improve image caching", + "paperplane", + 900, + "Use long-lived cache headers for immutable images.", + ), + ( + Agent::Claude, + "Tidy the release script", + "atlas-api", + 7200, + "Make release scripts easier to read.", + ), + ( + Agent::Codex, + "Fix pagination boundaries", + "tinyshop", + 1800, + "Cover empty and single-page lists.", + ), + ( + Agent::Codex, + "Document the plugin API", + "compass", + 21600, + "Add a small plugin example to the documentation.", + ), + ( + Agent::Opencode, + "Improve keyboard navigation", + "paperplane", + 28800, + "Make every control usable with the keyboard.", + ), + ( + Agent::Omp, + "Reduce build time", + "compass", + 43200, + "Remove unused dependencies from the build.", + ), + ]; + let sessions = examples + .into_iter() + .enumerate() + .map(|(i, (agent, title, project, age, message))| Session { + agent, + id: format!("demo-session-{:02}", i + 1), + path: format!("/fictional/sessions/{i}.jsonl").into(), + cwd: format!("/work/{project}").into(), + title: title.into(), + modified: now - age, + messages: vec![message.into()], + }) + .collect(); + let mut app = App { + index: SearchIndex::new(sessions), + query: "auth".into(), + agent: None, + folder: "/work/atlas-api".into(), + all: true, + hits: vec![], + selected: 0, + table: TableState::default(), + locations: None, + status: "15 sessions · 0 refreshed · Ctrl-R refresh".into(), + loading: false, + elapsed: Duration::ZERO, + }; + app.search(false); + let selected = app + .hits + .iter() + .position(|hit| app.index.sessions[hit.index].title == "Fix login redirect loop") + .unwrap(); + assert_eq!(app.hits[selected].kind, MatchKind::Message); + app.move_by(selected as isize); + let (width, height) = (112, 26); + let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap(); + terminal.draw(|frame| app.draw(frame)).unwrap(); + let buffer = terminal.backend().buffer(); + let cells: Vec<_> = buffer + .content + .iter() + .map(|cell| { + json!({ + "text": cell.symbol(), + "fg": format!("{:?}", cell.fg), + "bg": format!("{:?}", cell.bg), + "bold": cell.modifier.contains(Modifier::BOLD), + }) + }) + .collect(); + let output = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/demo-screen.json"); + std::fs::create_dir_all(output.parent().unwrap()).unwrap(); + std::fs::write( + output, + serde_json::to_vec(&json!({"width": width, "height": height, "cells": cells})).unwrap(), + ) + .unwrap(); +} diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..787d2cf --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,202 @@ +use serde_json::{Value, json}; +use std::{ + fs, + path::Path, + process::{Command, Output}, +}; + +struct Fixture { + dir: tempfile::TempDir, +} +impl Fixture { + fn new() -> Self { + let dir = tempfile::tempdir().unwrap(); + for path in [ + "pi", + "claude/projects/project", + "codex/sessions", + "work", + "other", + ] { + fs::create_dir_all(dir.path().join(path)).unwrap(); + } + let f = Self { dir }; + let root = f.dir.path(); + f.jsonl("pi/pi.jsonl", &[ + json!({"type":"session", "id":"pi-id", "cwd":root.join("work")}), + json!({"type":"session_info", "name":"Database migration"}), + json!({"type":"message", "message":{"role":"user","content":"add postgres indexes"}}), + json!({"type":"message", "message":{"role":"assistant","content":"assistant-only-secret"}}), + ]); + f.jsonl("claude/projects/project/claude.jsonl", &[ + json!({"type":"user", "sessionId":"claude-id", "cwd":root.join("other"), "message":{"role":"user","content":"consider migration rollback"}}), + json!({"type":"ai-title", "aiTitle":"Different topic"}), + ]); + f.jsonl("codex/sessions/codex.jsonl", &[ + json!({"type":"session_meta", "payload":{"id":"codex-id", "cwd":root.join("work"), "source":"cli"}}), + json!({"type":"event_msg", "payload":{"type":"user_message", "message":"refactor rust"}}), + ]); + f + } + fn jsonl(&self, path: &str, rows: &[Value]) { + fs::write( + self.dir.path().join(path), + rows.iter().map(|v| format!("{v}\n")).collect::(), + ) + .unwrap(); + } + fn run(&self, args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_cont")) + .env("HOME", self.dir.path()) + .env("XDG_DATA_HOME", self.dir.path().join("data")) + .env_remove("OPENCODE_DB") + .env("PI_CODING_AGENT_SESSION_DIR", self.dir.path().join("pi")) + .env("CLAUDE_CONFIG_DIR", self.dir.path().join("claude")) + .env("CODEX_HOME", self.dir.path().join("codex")) + .env("RESUME_CACHE_DIR", self.dir.path().join("cache")) + .current_dir(self.dir.path().join("work")) + .args(args) + .output() + .unwrap() + } + fn rows(&self, args: &[&str]) -> Vec { + let out = self.run(args); + assert!( + out.status.success(), + "{}", + String::from_utf8_lossy(&out.stderr) + ); + serde_json::from_slice(&out.stdout).unwrap() + } +} + +#[test] +fn cli_scope_agent_and_title_first_ranking() { + let f = Fixture::new(); + assert_eq!(f.rows(&["--json"]).len(), 2); + assert_eq!(f.rows(&["--json", "--all"]).len(), 3); + let rows = f.rows(&["--json", "--all", "migration"]); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0]["agent"], "pi"); + assert_eq!(rows[0]["match"], "title"); + assert_eq!(rows[1]["agent"], "claude"); + assert_eq!(rows[1]["match"], "message"); + assert_eq!( + f.rows(&["--json", "--all", "--agent", "codex"])[0]["id"], + "codex-id" + ); + assert!( + f.rows(&["--json", "--all", "assistant-only-secret"]) + .is_empty() + ); +} + +#[test] +fn omp_discovery_and_custom_opencode_database() { + let f = Fixture::new(); + let root = f.dir.path(); + fs::create_dir_all(root.join(".omp/agent/sessions/project")).unwrap(); + f.jsonl( + ".omp/agent/sessions/project/session.jsonl", + &[ + json!({"type":"title","title":"OMP task"}), + json!({"type":"session","id":"omp-id","cwd":root.join("work")}), + json!({"type":"message","message":{"role":"user","content":"unicorn request"}}), + ], + ); + let rows = f.rows(&["--json", "--agent", "omp", "unicorn"]); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0]["title"], "OMP task"); + assert_eq!(rows[0]["match"], "message"); + assert!( + rows[0]["command"] + .as_str() + .unwrap() + .contains("omp '--resume'") + ); + let database = root.join("custom.db"); + let conn = rusqlite::Connection::open(&database).unwrap(); + conn.execute_batch("CREATE TABLE session (id TEXT, directory TEXT, title TEXT, time_updated INTEGER, parent_id TEXT, time_archived INTEGER); + CREATE TABLE message (id TEXT, session_id TEXT, time_created INTEGER, data TEXT); + CREATE TABLE part (id TEXT, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT);").unwrap(); + conn.execute( + "INSERT INTO session VALUES ('ses_test', ?1, 'OpenCode task', 1000, NULL, NULL)", + [root.join("other").to_str().unwrap()], + ) + .unwrap(); + let rows = f.rows(&[ + "--json", + "--all", + "--agent", + "opencode", + "--opencode-db", + database.to_str().unwrap(), + ]); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0]["id"], "ses_test"); + assert!( + rows[0]["command"] + .as_str() + .unwrap() + .contains("OPENCODE_DB=") + ); + assert!( + f.rows(&["--json", "--all", "--agent", "opencode"]) + .is_empty() + ); +} + +#[test] +fn additional_root_and_invalid_cache_are_safe() { + let f = Fixture::new(); + let extra = f.dir.path().join("extra"); + fs::create_dir(&extra).unwrap(); + fs::write( + extra.join("extra.jsonl"), + format!( + "{}\n", + json!({"type":"session","id":"extra","cwd":f.dir.path().join("work")}) + ), + ) + .unwrap(); + assert_eq!( + f.rows(&["--json", "--all", "--pi-dir", extra.to_str().unwrap()]) + .len(), + 4 + ); + // Changing the configured roots must not leak old cached results into the final list. + assert_eq!(f.rows(&["--json", "--all"]).len(), 3); + fs::write(f.dir.path().join("cache/sessions-v1.json"), "bad cache").unwrap(); + assert_eq!(f.rows(&["--json", "--all"]).len(), 3); +} + +#[test] +fn non_tty_help_and_no_cache() { + let f = Fixture::new(); + assert!(f.run(&["--help"]).status.success()); + let out = f.run(&[]); + assert!(!out.status.success()); + assert!(String::from_utf8_lossy(&out.stderr).contains("needs a terminal")); + assert_eq!(f.rows(&["--json", "--no-cache"]).len(), 2); + assert!(!f.dir.path().join("cache").exists()); + assert!(f.rows(&["--json", "--limit", "0"]).is_empty()); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let cache = f.dir.path().join("cache/sessions-v1.json"); + assert_eq!( + fs::metadata(cache).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } +} + +#[test] +#[cfg(unix)] +fn symlinked_working_directory_matches() { + let f = Fixture::new(); + let link = f.dir.path().join("alias"); + std::os::unix::fs::symlink(f.dir.path().join("work"), &link).unwrap(); + assert_eq!(f.rows(&["--json", "-C", link.to_str().unwrap()]).len(), 2); + assert!(Path::new(f.rows(&["--json"])[0]["cwd"].as_str().unwrap()).is_absolute()); +} diff --git a/tests/pty_smoke.py b/tests/pty_smoke.py new file mode 100644 index 0000000..b755682 --- /dev/null +++ b/tests/pty_smoke.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Unix PTY integration checks. No real agents or user session files are touched.""" +import fcntl +import json +import os +from pathlib import Path +import pty +import re +import unicodedata +import select +import signal +import struct +import sqlite3 +import subprocess +import sys +import tempfile +import termios +import time + +BINARY = str(Path(sys.argv[1] if len(sys.argv) > 1 else "target/release/cont").resolve()) + + +class Picker: + def __init__(self, args, env, cwd): + self.master, self.slave = pty.openpty() + self.resize(120, 30) + self.before = termios.tcgetattr(self.slave) + self.output = b"" + self.history = b"" + + def setup(): + os.setsid() + fcntl.ioctl(0, termios.TIOCSCTTY, 0) + + self.proc = subprocess.Popen([BINARY, *args], stdin=self.slave, stdout=self.slave, + stderr=self.slave, env=env, cwd=cwd, preexec_fn=setup) + self.wait_for(b"Ctrl-R refresh") + + def resize(self, width, height): + self.output = b"" + self.width, self.height = width, height + fcntl.ioctl(self.slave, termios.TIOCSWINSZ, struct.pack("HHHH", height, width, 0, 0)) + if hasattr(self, "proc"): + os.kill(self.proc.pid, signal.SIGWINCH) + + def read(self, timeout=0.05): + if select.select([self.master], [], [], timeout)[0]: + try: + data = os.read(self.master, 65536) + self.output += data + self.history += data + except OSError: + pass + + def screen(self): + # Minimal emulator for Ratatui's absolute-cursor/SGR output. Reconstruct + # screen cells because later frames emit diffs, not complete lines. + cells = [[" "] * self.width for _ in range(self.height)] + x = y = 0 + for match in re.finditer(r"\x1b\[([0-?]*)([ -/]*)([@-~])|([^\x1b])", self.history.decode(errors="replace"), re.S): + params, _, command, char = match.groups() + if command in ("H", "f"): + parts = params.split(";") + y = int(parts[0] or 1) - 1 + x = int(parts[1] or 1) - 1 if len(parts) > 1 else 0 + elif command == "J" and params == "2": + cells = [[" "] * self.width for _ in range(self.height)] + elif char == "\r": + x = 0 + elif char == "\n": + y += 1 + elif char and ord(char) >= 32: + wide = unicodedata.east_asian_width(char) in ("W", "F") + if 0 <= x < self.width and 0 <= y < self.height: + cells[y][x] = char + if wide and x + 1 < self.width: + cells[y][x + 1] = "" + x += 2 if wide else 1 + return "\n".join("".join(row) for row in cells) + + def wait_for(self, marker): + end = time.monotonic() + 8 + self.read() + while marker not in self.output and marker.decode() not in self.screen() and time.monotonic() < end: + self.read() + assert marker in self.output or marker.decode() in self.screen(), (marker, self.screen()) + + def send(self, keys): + self.output = b"" + os.write(self.master, keys) + + def finish(self, code): + end = time.monotonic() + 8 + while self.proc.poll() is None and time.monotonic() < end: + self.read() + if self.proc.poll() is None: + self.proc.kill() + raise AssertionError(("Picker did not exit", self.screen(), self.output[-2000:])) + self.read() + assert self.proc.returncode == code, (self.proc.returncode, self.output[-4000:]) + after = termios.tcgetattr(self.master) + assert after == self.before, "Terminal settings were not restored" + assert b"\x1b[?1049l" in self.output, "Alternate screen was not left" + os.close(self.master) + os.close(self.slave) + + +def main(): + with tempfile.TemporaryDirectory(prefix="cont-test-") as tmp: + root = Path(tmp) + work = root / "work with 'quotes'" + other = root / "other" + for p in [work, other, root / "pi", root / "claude/projects/p", root / "codex/sessions", root / ".omp/agent/sessions", root / "data/opencode", root / "bin"]: + p.mkdir(parents=True) + env = os.environ | { + "HOME": str(root), + "XDG_DATA_HOME": str(root / "data"), + "OPENCODE_DB": str(root / "data/opencode/opencode.db"), + "PI_CODING_AGENT_SESSION_DIR": str(root / "pi"), + "CLAUDE_CONFIG_DIR": str(root / "claude"), + "CODEX_HOME": str(root / "codex"), + "RESUME_CACHE_DIR": str(root / "cache"), + "PATH": str(root / "bin") + os.pathsep + os.environ["PATH"], + "RESUME_TEST_LOG": str(root / "handoff.json"), + "TERM": "xterm-256color", + } + sessions = { + ".omp/agent/sessions/omp.jsonl": [ + {"type": "title", "title": "OMP needle"}, + {"type": "session", "id": "omp-id", "cwd": str(other)}, + {"type": "message", "message": {"role": "user", "content": "omp prompt"}}, + ], + "pi/a.jsonl": [ + {"type": "session", "id": "pi-id", "cwd": str(work)}, + {"type": "session_info", "name": "Pi needle"}, + {"type": "message", "message": {"role": "user", "content": "日本語 café"}}, + ], + "claude/projects/p/c.jsonl": [ + {"type": "user", "sessionId": "claude-id", "cwd": str(other), "message": {"role": "user", "content": "Claude needle"}}, + ], + "codex/sessions/x.jsonl": [ + {"type": "session_meta", "payload": {"id": "codex-id", "cwd": str(work)}}, + {"type": "event_msg", "payload": {"type": "user_message", "message": "Codex needle"}}, + ], + } + for path, records in sessions.items(): + (root / path).write_text("".join(json.dumps(r) + "\n" for r in records)) + database = root / "data/opencode/opencode.db" + conn = sqlite3.connect(database) + conn.executescript(""" + CREATE TABLE session (id TEXT, directory TEXT, title TEXT, time_updated INTEGER, parent_id TEXT, time_archived INTEGER); + CREATE TABLE message (id TEXT, session_id TEXT, time_created INTEGER, data TEXT); + CREATE TABLE part (id TEXT, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT); + """) + conn.execute("INSERT INTO session VALUES ('ses_oc', ?, 'OpenCode needle', 1000, NULL, NULL)", [str(other)]) + conn.commit() + conn.close() + stub = f"#!{sys.executable}\n" + '''import json, os, sys, termios +flags = termios.tcgetattr(0)[3] +with open(os.environ["RESUME_TEST_LOG"], "w") as f: + json.dump({"agent": os.path.basename(sys.argv[0]), "args": sys.argv[1:], "cwd": os.getcwd(), + "canonical": bool(flags & termios.ICANON), "echo": bool(flags & termios.ECHO), + "database": os.environ.get("OPENCODE_DB")}, f) +sys.exit(23) +''' + for agent in ["pi", "claude", "codex", "opencode", "omp"]: + path = root / "bin" / agent + path.write_text(stub) + path.chmod(0o755) + + # Actual exec handoff, argv boundaries, original cwd, exit status, terminal flags. + for agent, args, cwd in [ + ("pi", ["--session", str((root / "pi/a.jsonl").resolve())], work), + ("claude", ["--resume", "claude-id"], other), + ("codex", ["resume", "codex-id"], work), + ("opencode", ["--session", "ses_oc"], other), + ("omp", ["--resume", str((root / ".omp/agent/sessions/omp.jsonl").resolve())], other), + ]: + p = Picker(["--all", "--agent", agent], env, work) + p.send(b"\r") + p.finish(23) + log = json.loads((root / "handoff.json").read_text()) + selected_database = log.pop("database") + if agent == "opencode": + assert selected_database == str(database.resolve()) + assert log == {"agent": agent, "args": args, "cwd": str(cwd.resolve()), "canonical": True, "echo": True}, log + + # Scope, agent cycling, Unicode paste, clearing, location selection, resize, quit. + p = Picker([], env, work) + assert "2 results" in p.screen(), p.screen() + p.send(b"\t") + p.wait_for(b"All locations") + p.send(b"\x1b[Z") + p.wait_for(b"1 results") + p.send(b"\x1b[200~" + "日本語".encode() + b"\x1b[201~") + p.wait_for("日本語".encode()) + p.send(b"\x15") # Ctrl-U + p.wait_for(b"All locations") + p.send(b"\x1b[Z") # Claude + p.wait_for(b"Claude needle") + p.send(b"\x0c") # Ctrl-L + p.wait_for(b"Locations") + p.send(b"other") + p.wait_for(b"other") + p.send(b"\r") + p.wait_for(b"1 results") + p.resize(30, 8) + p.wait_for(b"Enlarge terminal") + p.resize(120, 30) + p.wait_for(b"Enter resume") + p.send(b"\x03") + p.finish(0) + + # Print-only selection and failed executable must also restore the terminal. + p = Picker(["--all", "--agent", "pi", "--print"], env, work) + p.send(b"\r") + p.finish(0) + assert b"cd -- '" in p.output + assert b"'--session'" in p.output + (root / "bin/pi").unlink() + env["PATH"] = str(root / "bin") # Deliberately no fallback to the real Pi. + p = Picker(["--all", "--agent", "pi"], env, work) + p.send(b"\r") + p.finish(1) + assert b"Could not start pi" in p.output + print("PTY smoke tests passed: all agents, scope/filter/paste/resize, print, errors, terminal restoration") + + +if __name__ == "__main__": + main()