diff --git a/Cargo.lock b/Cargo.lock index 9a61100..218f38c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -76,7 +76,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -91,6 +91,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.11.0" @@ -265,6 +271,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" @@ -284,7 +301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -317,6 +334,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "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.2" @@ -386,12 +423,115 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -412,7 +552,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -475,6 +615,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "log" version = "0.4.29" @@ -494,6 +640,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "ureq", ] [[package]] @@ -579,6 +726,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pkg-config" version = "0.3.32" @@ -613,6 +766,15 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -696,6 +858,20 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -720,7 +896,42 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -808,12 +1019,24 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -825,6 +1048,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -832,10 +1066,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -858,6 +1092,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -921,6 +1165,47 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -949,6 +1234,12 @@ dependencies = [ "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 = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -1056,13 +1347,31 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1071,6 +1380,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1080,6 +1398,70 @@ 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 = "winnow" version = "0.7.15" @@ -1177,6 +1559,35 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -1197,6 +1608,66 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/README.md b/README.md index 4e1e4c6..4b4eee9 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,38 @@ lowfat ls -la { "shellCommandPrefix": "eval \"$(lowfat shell-init zsh)\"; " } ``` +**Codex, Grok Build, Antigravity** — direct prefixing works in all: + +```sh +lowfat git status +lowfat docker ps +``` + +Shell integration auto-activates for Codex when `CODEX_ENV` is set (or force with `LOWFAT_ENABLE=1`). + +Non-interactive runs often need bypass/approval flags: + +- Codex: `codex exec --dangerously-bypass-approvals-and-sandbox ...` +- Grok Build: `grok --single --always-approve --permission-mode bypassPermissions ...` + +Antigravity (`agy`) supports direct prefixing inside sessions; non-interactive terminal execution is inconsistent. + +**Discoverability skills** are installed for autonomous agent use: + +- Claude Code: `~/.claude/skills/lowfat/SKILL.md` +- Codex: `~/.codex/skills/lowfat/SKILL.md` +- .agents / Devin-style: `~/.agents/skills/lowfat/SKILL.md` +- Antigravity (Google): `~/.gemini/config/plugins/google-antigravity-sdk/skills/lowfat/SKILL.md` + +**Tested agents (one-shot verification)** + +| Assistant | Result | Notes | +|---------------|----------|-------| +| Claude Code | PASS | Pre-existing hooks; clean `--print` execution | +| Codex | PASS | Used bypass flags for non-interactive exec | +| Grok Build | PASS | `--single` + bypass/always-approve | +| Antigravity | PARTIAL | Direct prefix works in-session; `--print` terminal invocation is unreliable | + ### Usage highlights ```sh @@ -206,9 +238,10 @@ core you extend yourself. | Custom filters | `.lf` DSL + shell + Python (PEP 723/uv) | TOML DSL | | Levels | `lite` / `full` / `ultra` | `-l aggressive`, `--ultra-compact` | | File-content filtering | `post-read` hook (code, markdown, HTML, data, lock files) | `rtk read` / `smart` (signatures, summaries) | -| Agent integrations | Claude Code, OpenCode, shell, Pi | 14 tools (Claude Code, Copilot, Gemini, Codex, …) | +| Agent integrations | Claude Code, Codex, Grok Build, Antigravity, OpenCode, shell, Pi | 14 tools (Claude Code, Copilot, Gemini, Codex, …) | | Telemetry | None — local-only | Opt-in, off by default (anonymous aggregate) | | Savings analytics | `lowfat stats` / `history` (local) | `rtk gain` / `discover` (local) | +| Optional remote log | Opt-in to your Supabase observatory (eng_evaluations) | — | ### Token savings, head-to-head @@ -222,13 +255,31 @@ counted with `tiktoken` (`cl100k_base`); savings are vs the raw command output: | `git log` | 3350 | -93% | -97% | -56% | | `ls -la` | 153 | -77% | -89% | -86% | | `find` | 535 | -0% | -58% | -66% | - -Honest read: lowfat compresses git harder; rtk edges out `find`; `ls` is close. +| Honest read: lowfat compresses git harder; rtk edges out `find`; `ls` is close. `find` only engages at lowfat's `ultra` level. rtk's `--ultra-compact` gave near-identical numbers to its default here, so the default is shown. This is a single small run on one repo — directional, not a benchmark; measure on your own workload before trusting any of it. +### Optional Supabase observatory integration + +lowfat can optionally log its own token savings to a personal Supabase +"observatory" (tables `eng_technologies` + `eng_evaluations`). This is +**completely optional**, best-effort, and non-blocking. + +- Only activates when `LOWFAT_SUPABASE_URL` + `LOWFAT_SUPABASE_KEY` (or the + generic `SUPABASE_URL` / `SUPABASE_SERVICE_ROLE_KEY`) are present. +- Logs aggregates only: command, subcommand, raw/filtered/saved tokens, + savings %, had_plugin, reduced, exit code, elapsed ms. **No raw output or + full arguments** are sent. +- Records under technology slug `lowfat` (category `tool`). +- `lowfat stats` / `history` / `info` remain fully local and work offline. +- Enabled status appears in `lowfat info` when active. +- Debug failures with `LOWFAT_OBSERVATORY_DEBUG=1`. + +This respects the local-first design: the remote path is fire-and-forget and +never affects stdout, exit code, or local tracking. + ## License Apache-2.0 diff --git a/crates/lowfat/Cargo.toml b/crates/lowfat/Cargo.toml index 77bbd04..f7e929c 100644 --- a/crates/lowfat/Cargo.toml +++ b/crates/lowfat/Cargo.toml @@ -20,3 +20,4 @@ clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = "3" +ureq = { version = "2", default-features = false, features = ["json", "tls"] } diff --git a/crates/lowfat/src/commands/hook.rs b/crates/lowfat/src/commands/hook.rs index c227cde..78ec890 100644 --- a/crates/lowfat/src/commands/hook.rs +++ b/crates/lowfat/src/commands/hook.rs @@ -28,14 +28,16 @@ pub fn run() -> Result<()> { None => return Ok(()), }; + let mut updated_input = json!({ "command": rewritten }); + if let Some(description) = payload["tool_input"]["description"].as_str() { + updated_input["description"] = json!(description); + } + let output = json!({ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", - "updatedInput": { - "command": rewritten, - "description": payload["tool_input"]["description"] - } + "updatedInput": updated_input } }); diff --git a/crates/lowfat/src/commands/run.rs b/crates/lowfat/src/commands/run.rs index adaae65..9856956 100644 --- a/crates/lowfat/src/commands/run.rs +++ b/crates/lowfat/src/commands/run.rs @@ -1,4 +1,5 @@ use crate::filters; +use crate::observatory::{log_savings_if_enabled, SavingsData}; use lowfat_core::config::RunfConfig; use lowfat_core::db::{Db, InvocationRecord, TrackRecord}; use lowfat_core::pipeline::{Pipeline, StageType}; @@ -51,12 +52,13 @@ pub fn run(args: &[String]) -> i32 { // Skip filtering for tiny outputs — overhead exceeds any possible savings. const MIN_FILTER_TOKENS: usize = 128; if lowfat_core::tokens::estimate_tokens(&raw) < MIN_FILTER_TOKENS { + let tokens = lowfat_core::tokens::estimate_tokens(&raw) as u64; if let Ok(db) = Db::open(&config.data_dir) { let _ = db.record_invocation(&InvocationRecord { command: cmd.clone(), subcommand: subcommand.clone(), - raw_tokens: lowfat_core::tokens::estimate_tokens(&raw) as u64, - filtered_tokens: lowfat_core::tokens::estimate_tokens(&raw) as u64, + raw_tokens: tokens, + filtered_tokens: tokens, had_plugin: false, in_scope: false, reduced: false, @@ -64,6 +66,19 @@ pub fn run(args: &[String]) -> i32 { exit_code, }); } + log_savings_if_enabled( + &config.data_dir, + SavingsData { + command: cmd.clone(), + subcommand: subcommand.clone(), + raw_tokens: tokens, + filtered_tokens: tokens, + had_plugin: false, + reduced: false, + exit_code, + exec_time_ms: None, + }, + ); print!("{raw}"); return exit_code; } @@ -129,6 +144,17 @@ pub fn run(args: &[String]) -> i32 { let elapsed = start.elapsed().as_millis() as u64; + // Compute usage history values outside the DB block so they are also + // available for the optional remote observatory log. + let known = known_subcommands(cmd, &plugin_map, &external_plugins, &external_map); + let raw_tokens = lowfat_core::tokens::estimate_tokens(&raw) as u64; + let filtered_tokens = lowfat_core::tokens::estimate_tokens(&filtered) as u64; + let in_scope = + filter_name.is_some() && (known.is_empty() || known.iter().any(|s| s == &subcommand)); + let had_plugin = filter_name.is_some(); + let reduced = filtered_tokens < raw_tokens; + let hist_sub = history_subcommand(&subcommand, &known); + // Track if let Ok(db) = Db::open(&config.data_dir) { let args_str = cmd_args.join(" "); @@ -143,27 +169,34 @@ pub fn run(args: &[String]) -> i32 { .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(), }); - // Usage history — command+subcommand only, no args. Powers `lowfat history`. - let known = known_subcommands(cmd, &plugin_map, &external_plugins, &external_map); - // in_scope = plugin is declared to handle *this* subcommand. Empty - // `known` means "no subcommand restriction" — treat as universal. - let in_scope = - filter_name.is_some() && (known.is_empty() || known.iter().any(|s| s == &subcommand)); - let raw_tokens = lowfat_core::tokens::estimate_tokens(&raw) as u64; - let filtered_tokens = lowfat_core::tokens::estimate_tokens(&filtered) as u64; let _ = db.record_invocation(&InvocationRecord { command: cmd.clone(), - subcommand: history_subcommand(&subcommand, &known), + subcommand: hist_sub.clone(), raw_tokens, filtered_tokens, - had_plugin: filter_name.is_some(), + had_plugin, in_scope, - reduced: filtered_tokens < raw_tokens, + reduced, is_external_plugin, exit_code, }); } + // Remote observatory (best-effort, non-blocking) + log_savings_if_enabled( + &config.data_dir, + SavingsData { + command: cmd.clone(), + subcommand: hist_sub, + raw_tokens, + filtered_tokens, + had_plugin, + reduced, + exit_code, + exec_time_ms: Some(elapsed), + }, + ); + // Tee on failure let tee_dir = config.data_dir.join("tee"); tee::save_on_failure(&tee_dir, &format!("{cmd}_{subcommand}"), &raw, exit_code); @@ -362,6 +395,8 @@ fn passthrough(cmd: &str, args: &[String], config: &RunfConfig) -> i32 { }; let elapsed = start.elapsed().as_millis() as u64; + let subcommand = args.first().cloned().unwrap_or_default(); + let tokens = lowfat_core::tokens::estimate_tokens(&raw) as u64; if let Ok(db) = Db::open(&config.data_dir) { let args_str = args.join(" "); @@ -375,8 +410,6 @@ fn passthrough(cmd: &str, args: &[String], config: &RunfConfig) -> i32 { .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(), }); - let subcommand = args.first().cloned().unwrap_or_default(); - let tokens = lowfat_core::tokens::estimate_tokens(&raw) as u64; let _ = db.record_invocation(&InvocationRecord { command: cmd.to_string(), subcommand: history_subcommand(&subcommand, &[]), @@ -390,6 +423,21 @@ fn passthrough(cmd: &str, args: &[String], config: &RunfConfig) -> i32 { }); } + // Remote observatory (best-effort, non-blocking) + log_savings_if_enabled( + &config.data_dir, + SavingsData { + command: cmd.to_string(), + subcommand: history_subcommand(&subcommand, &[]), + raw_tokens: tokens, + filtered_tokens: tokens, + had_plugin: false, + reduced: false, + exit_code, + exec_time_ms: Some(elapsed), + }, + ); + print!("{raw}"); exit_code } diff --git a/crates/lowfat/src/commands/status.rs b/crates/lowfat/src/commands/status.rs index fccccd0..db98377 100644 --- a/crates/lowfat/src/commands/status.rs +++ b/crates/lowfat/src/commands/status.rs @@ -37,6 +37,10 @@ pub fn run() -> Result<()> { println!(); println!(" {Y}💰{R} {D}saved:{R} {B}{input}{R} {D}→{R} {G}{B}{output}{R} {D}tokens{R}"); println!(" {Y}⚡{R} {D}speed:{R} {B}{time_without:.1}s{R} {D}→{R} {G}{B}{time_s:.1}s{R}"); + + if crate::observatory::enabled() { + println!(" {D}🌐 observatory:{R} {G}on{R}"); + } println!(); Ok(()) diff --git a/crates/lowfat/src/main.rs b/crates/lowfat/src/main.rs index 08edecf..6d8502e 100644 --- a/crates/lowfat/src/main.rs +++ b/crates/lowfat/src/main.rs @@ -1,5 +1,6 @@ mod commands; mod filters; +mod observatory; use clap::{Parser, Subcommand}; diff --git a/crates/lowfat/src/observatory.rs b/crates/lowfat/src/observatory.rs new file mode 100644 index 0000000..e39400d --- /dev/null +++ b/crates/lowfat/src/observatory.rs @@ -0,0 +1,252 @@ +//! Optional, best-effort logging of token savings to a personal Supabase +//! "observatory" (eng_technologies + eng_evaluations). Designed to be +//! completely non-blocking for the main filter path. +//! +//! Enabled only when LOWFAT_SUPABASE_URL + KEY (or the generic SUPABASE_* +//! fallbacks) are present in the environment. All errors are swallowed unless +//! LOWFAT_OBSERVATORY_DEBUG=1. + +use std::path::Path; +use std::thread; +use std::time::Duration; + +use serde_json::json; + +static ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// Returns true if observatory credentials are configured (checked once). +pub fn enabled() -> bool { + *ENABLED.get_or_init(|| resolve_url_key().is_some()) +} + +fn resolve_url_key() -> Option<(String, String)> { + let url = std::env::var("LOWFAT_SUPABASE_URL") + .or_else(|_| std::env::var("SUPABASE_URL")) + .ok()?; + let key = std::env::var("LOWFAT_SUPABASE_KEY") + .or_else(|_| std::env::var("SUPABASE_SERVICE_ROLE_KEY")) + .ok()?; + if url.trim().is_empty() || key.trim().is_empty() { + return None; + } + Some((url, key)) +} + +/// Lightweight record of one filter invocation's savings. +#[derive(Clone, Debug)] +pub struct SavingsData { + pub command: String, + pub subcommand: String, + pub raw_tokens: u64, + pub filtered_tokens: u64, + pub had_plugin: bool, + pub reduced: bool, + pub exit_code: i32, + pub exec_time_ms: Option, +} + +/// Fire-and-forget log. Spawns a background thread if enabled. +/// Never blocks the caller and never propagates errors. +/// +/// When LOWFAT_OBSERVATORY_DEBUG is set, run synchronously so diagnostics are +/// visible before this short-lived CLI process exits. +pub fn log_savings_if_enabled(data_dir: &Path, data: SavingsData) { + if !enabled() { + return; + } + let dir = data_dir.to_path_buf(); + if std::env::var("LOWFAT_OBSERVATORY_DEBUG").is_ok() { + if let Err(e) = log_savings_inner(&dir, &data) { + eprintln!("[lowfat] observatory log failed: {e}"); + } + return; + } + thread::spawn(move || { + if let Err(e) = log_savings_inner(&dir, &data) { + if std::env::var("LOWFAT_OBSERVATORY_DEBUG").is_ok() { + eprintln!("[lowfat] observatory log failed: {e}"); + } + } + }); +} + +fn log_savings_inner(data_dir: &Path, data: &SavingsData) -> Result<(), Box> { + let (url, key) = resolve_url_key().ok_or("missing observatory credentials")?; + if std::env::var("LOWFAT_OBSERVATORY_DEBUG").is_ok() { + eprintln!("[lowfat] observatory: data_dir={}", data_dir.display()); + eprintln!("[lowfat] observatory: posting to {}/rest/v1/eng_evaluations (tech ensure first)", url.trim_end_matches('/')); + } + let tech_id = ensure_lowfat_technology(&url, &key, data_dir)?; + + let saved = data.raw_tokens as i64 - data.filtered_tokens as i64; + let pct = if data.raw_tokens > 0 { + (saved as f64 / data.raw_tokens as f64) * 100.0 + } else { + 0.0 + }; + let verdict = if saved > 0 { "adopt" } else { "trial" }; + + let summary = if data.subcommand.is_empty() { + format!("lowfat {}: {}→{} tokens ({:.1}%)", data.command, data.raw_tokens, data.filtered_tokens, pct) + } else { + format!("lowfat {} {}: {}→{} tokens ({:.1}%)", data.command, data.subcommand, data.raw_tokens, data.filtered_tokens, pct) + }; + + let scores = json!({ + "command": data.command, + "subcommand": data.subcommand, + "raw_tokens": data.raw_tokens, + "filtered_tokens": data.filtered_tokens, + "saved_tokens": saved, + "savings_pct": pct, + "had_plugin": data.had_plugin, + "reduced": data.reduced, + "exit_code": data.exit_code, + "exec_time_ms": data.exec_time_ms, + }); + + let body = json!({ + "technology_id": tech_id, + "phase": "retrospective", + "verdict": verdict, + "scores": scores, + "summary": summary, + "status": "open", + }); + + let base = url.trim_end_matches('/'); + let endpoint = format!("{base}/rest/v1/eng_evaluations"); + + let agent = ureq::AgentBuilder::new() + .timeout(Duration::from_secs(4)) + .build(); + + let resp = agent + .post(&endpoint) + .set("apikey", &key) + .set("Authorization", &format!("Bearer {}", key)) + .set("Content-Type", "application/json") + .set("Prefer", "return=minimal") + .send_json(body)?; + + // Best effort: drain any small response body + let _ = resp.into_string(); + if std::env::var("LOWFAT_OBSERVATORY_DEBUG").is_ok() { + eprintln!("[lowfat] observatory: POST eng_evaluations accepted (status OK)"); + } + Ok(()) +} + +/// Ensure the "lowfat" technology row exists and return its UUID. +/// Caches the id in data_dir/lowfat_tech_id for fast subsequent calls. +fn ensure_lowfat_technology( + url: &str, + key: &str, + data_dir: &Path, +) -> Result> { + let cache_path = data_dir.join("lowfat_tech_id"); + + // Fast path: cached id + if let Ok(s) = std::fs::read_to_string(&cache_path) { + let trimmed = s.trim(); + if !trimmed.is_empty() { + return Ok(trimmed.to_string()); + } + } + + let base = url.trim_end_matches('/'); + let agent = ureq::AgentBuilder::new() + .timeout(Duration::from_secs(3)) + .build(); + + // Try to find existing by slug + let get_url = format!("{base}/rest/v1/eng_technologies?select=id&slug=eq.lowfat"); + if let Ok(res) = agent + .get(&get_url) + .set("apikey", key) + .set("Authorization", &format!("Bearer {}", key)) + .call() + { + if let Ok(rows) = res.into_json::>() { + if let Some(first) = rows.first() { + if let Some(id) = first.get("id").and_then(|v| v.as_str()) { + let _ = std::fs::write(&cache_path, id); + return Ok(id.to_string()); + } + } + } + } + + // Not found — insert + let ins = json!({ + "slug": "lowfat", + "name": "lowfat", + "category": "tool", + "status": "adopted", + "description": "Lightweight token-aware output and file content filter for LLM agents" + }); + + let post_url = format!("{base}/rest/v1/eng_technologies"); + let created_res = agent + .post(&post_url) + .set("apikey", key) + .set("Authorization", &format!("Bearer {}", key)) + .set("Content-Type", "application/json") + .set("Prefer", "return=representation") + .send_json(ins); + + match created_res { + Ok(resp) => { + if let Ok(rows) = resp.into_json::>() { + if let Some(first) = rows.first() { + if let Some(id) = first.get("id").and_then(|v| v.as_str()) { + let _ = std::fs::write(&cache_path, id); + return Ok(id.to_string()); + } + } + } + } + Err(_) => { + // Possible conflict (unique slug) from a race. Fall back to GET. + } + } + + // Re-fetch after possible conflict + let get_url2 = format!("{base}/rest/v1/eng_technologies?select=id&slug=eq.lowfat"); + let res2 = agent + .get(&get_url2) + .set("apikey", key) + .set("Authorization", &format!("Bearer {}", key)) + .call()?; + + let rows: Vec = res2.into_json()?; + let id = rows + .first() + .and_then(|v| v.get("id")) + .and_then(|v| v.as_str()) + .ok_or("failed to resolve lowfat technology id")? + .to_string(); + + let _ = std::fs::write(&cache_path, &id); + Ok(id) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn disabled_when_no_env() { + // Ensure we don't accidentally treat empty as enabled in tests. + // We cannot easily clear env for the OnceLock in a unit test without + // forking, so just assert the resolver returns None for obviously bad values. + // Instead, directly test the private resolver by temporarily setting and then + // we just trust the logic; here we test that malformed does not enable. + std::env::remove_var("LOWFAT_SUPABASE_URL"); + std::env::remove_var("SUPABASE_URL"); + std::env::remove_var("LOWFAT_SUPABASE_KEY"); + std::env::remove_var("SUPABASE_SERVICE_ROLE_KEY"); + // Re-init path is cached; this test mainly documents intent. + // A true off test would require a fresh process. + } +}