├── .github └── FUNDING.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── actor.rs ├── animation.rs ├── cli.rs ├── config.rs ├── debug_log.rs ├── git.rs ├── main.rs ├── model.rs ├── openai.rs └── util.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Sett17] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | example.tape 3 | dev.debug 4 | turboCommit.iml 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.69" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi 0.1.19", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.1.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 36 | 37 | [[package]] 38 | name = "base64" 39 | version = "0.21.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 42 | 43 | [[package]] 44 | name = "bit-set" 45 | version = "0.5.3" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 48 | dependencies = [ 49 | "bit-vec", 50 | ] 51 | 52 | [[package]] 53 | name = "bit-vec" 54 | version = "0.6.3" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 57 | 58 | [[package]] 59 | name = "bitflags" 60 | version = "1.3.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.6.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 69 | 70 | [[package]] 71 | name = "bstr" 72 | version = "1.3.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" 75 | dependencies = [ 76 | "memchr", 77 | "once_cell", 78 | "regex-automata", 79 | "serde", 80 | ] 81 | 82 | [[package]] 83 | name = "bumpalo" 84 | version = "3.12.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 87 | 88 | [[package]] 89 | name = "bytes" 90 | version = "1.4.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 93 | 94 | [[package]] 95 | name = "cc" 96 | version = "1.0.79" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 99 | dependencies = [ 100 | "jobserver", 101 | ] 102 | 103 | [[package]] 104 | name = "cfg-if" 105 | version = "1.0.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 108 | 109 | [[package]] 110 | name = "chrono" 111 | version = "0.4.24" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" 114 | dependencies = [ 115 | "num-integer", 116 | "num-traits", 117 | "serde", 118 | ] 119 | 120 | [[package]] 121 | name = "colored" 122 | version = "2.0.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 125 | dependencies = [ 126 | "atty", 127 | "lazy_static", 128 | "winapi", 129 | ] 130 | 131 | [[package]] 132 | name = "core-foundation" 133 | version = "0.9.3" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 136 | dependencies = [ 137 | "core-foundation-sys", 138 | "libc", 139 | ] 140 | 141 | [[package]] 142 | name = "core-foundation-sys" 143 | version = "0.8.3" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 146 | 147 | [[package]] 148 | name = "crates_io_api" 149 | version = "0.8.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "9fdfaac64c5eb7a33a5b895ffbaf6f1146721b6cd4c889010d19c8a1c1cae562" 152 | dependencies = [ 153 | "chrono", 154 | "futures", 155 | "log", 156 | "reqwest", 157 | "serde", 158 | "serde_derive", 159 | "serde_json", 160 | "serde_path_to_error", 161 | "tokio", 162 | "url", 163 | ] 164 | 165 | [[package]] 166 | name = "crossterm" 167 | version = "0.25.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" 170 | dependencies = [ 171 | "bitflags 1.3.2", 172 | "crossterm_winapi", 173 | "libc", 174 | "mio", 175 | "parking_lot", 176 | "signal-hook", 177 | "signal-hook-mio", 178 | "winapi", 179 | ] 180 | 181 | [[package]] 182 | name = "crossterm" 183 | version = "0.26.1" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" 186 | dependencies = [ 187 | "bitflags 1.3.2", 188 | "crossterm_winapi", 189 | "libc", 190 | "mio", 191 | "parking_lot", 192 | "signal-hook", 193 | "signal-hook-mio", 194 | "winapi", 195 | ] 196 | 197 | [[package]] 198 | name = "crossterm_winapi" 199 | version = "0.9.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" 202 | dependencies = [ 203 | "winapi", 204 | ] 205 | 206 | [[package]] 207 | name = "displaydoc" 208 | version = "0.2.5" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 211 | dependencies = [ 212 | "proc-macro2", 213 | "quote", 214 | "syn 2.0.96", 215 | ] 216 | 217 | [[package]] 218 | name = "dyn-clone" 219 | version = "1.0.11" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" 222 | 223 | [[package]] 224 | name = "edit" 225 | version = "0.1.4" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "c562aa71f7bc691fde4c6bf5f93ae5a5298b617c2eb44c76c87832299a17fbb4" 228 | dependencies = [ 229 | "tempfile", 230 | "which", 231 | ] 232 | 233 | [[package]] 234 | name = "either" 235 | version = "1.8.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 238 | 239 | [[package]] 240 | name = "encoding_rs" 241 | version = "0.8.32" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" 244 | dependencies = [ 245 | "cfg-if", 246 | ] 247 | 248 | [[package]] 249 | name = "equivalent" 250 | version = "1.0.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 253 | 254 | [[package]] 255 | name = "errno" 256 | version = "0.3.10" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 259 | dependencies = [ 260 | "libc", 261 | "windows-sys 0.59.0", 262 | ] 263 | 264 | [[package]] 265 | name = "eventsource-stream" 266 | version = "0.2.3" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" 269 | dependencies = [ 270 | "futures-core", 271 | "nom", 272 | "pin-project-lite", 273 | ] 274 | 275 | [[package]] 276 | name = "fancy-regex" 277 | version = "0.11.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" 280 | dependencies = [ 281 | "bit-set", 282 | "regex", 283 | ] 284 | 285 | [[package]] 286 | name = "fastrand" 287 | version = "1.9.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 290 | dependencies = [ 291 | "instant", 292 | ] 293 | 294 | [[package]] 295 | name = "fnv" 296 | version = "1.0.7" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 299 | 300 | [[package]] 301 | name = "foreign-types" 302 | version = "0.3.2" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 305 | dependencies = [ 306 | "foreign-types-shared", 307 | ] 308 | 309 | [[package]] 310 | name = "foreign-types-shared" 311 | version = "0.1.1" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 314 | 315 | [[package]] 316 | name = "form_urlencoded" 317 | version = "1.2.1" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 320 | dependencies = [ 321 | "percent-encoding", 322 | ] 323 | 324 | [[package]] 325 | name = "futures" 326 | version = "0.3.27" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" 329 | dependencies = [ 330 | "futures-channel", 331 | "futures-core", 332 | "futures-executor", 333 | "futures-io", 334 | "futures-sink", 335 | "futures-task", 336 | "futures-util", 337 | ] 338 | 339 | [[package]] 340 | name = "futures-channel" 341 | version = "0.3.27" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" 344 | dependencies = [ 345 | "futures-core", 346 | "futures-sink", 347 | ] 348 | 349 | [[package]] 350 | name = "futures-core" 351 | version = "0.3.27" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" 354 | 355 | [[package]] 356 | name = "futures-executor" 357 | version = "0.3.27" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" 360 | dependencies = [ 361 | "futures-core", 362 | "futures-task", 363 | "futures-util", 364 | ] 365 | 366 | [[package]] 367 | name = "futures-io" 368 | version = "0.3.27" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" 371 | 372 | [[package]] 373 | name = "futures-macro" 374 | version = "0.3.27" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" 377 | dependencies = [ 378 | "proc-macro2", 379 | "quote", 380 | "syn 1.0.109", 381 | ] 382 | 383 | [[package]] 384 | name = "futures-sink" 385 | version = "0.3.27" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" 388 | 389 | [[package]] 390 | name = "futures-task" 391 | version = "0.3.27" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" 394 | 395 | [[package]] 396 | name = "futures-timer" 397 | version = "3.0.2" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" 400 | 401 | [[package]] 402 | name = "futures-util" 403 | version = "0.3.27" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" 406 | dependencies = [ 407 | "futures-channel", 408 | "futures-core", 409 | "futures-io", 410 | "futures-macro", 411 | "futures-sink", 412 | "futures-task", 413 | "memchr", 414 | "pin-project-lite", 415 | "pin-utils", 416 | "slab", 417 | ] 418 | 419 | [[package]] 420 | name = "git2" 421 | version = "0.18.3" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" 424 | dependencies = [ 425 | "bitflags 2.6.0", 426 | "libc", 427 | "libgit2-sys", 428 | "log", 429 | "openssl-probe", 430 | "openssl-sys", 431 | "url", 432 | ] 433 | 434 | [[package]] 435 | name = "h2" 436 | version = "0.3.26" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 439 | dependencies = [ 440 | "bytes", 441 | "fnv", 442 | "futures-core", 443 | "futures-sink", 444 | "futures-util", 445 | "http", 446 | "indexmap 2.7.0", 447 | "slab", 448 | "tokio", 449 | "tokio-util", 450 | "tracing", 451 | ] 452 | 453 | [[package]] 454 | name = "hashbrown" 455 | version = "0.12.3" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 458 | 459 | [[package]] 460 | name = "hashbrown" 461 | version = "0.15.2" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 464 | 465 | [[package]] 466 | name = "hermit-abi" 467 | version = "0.1.19" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 470 | dependencies = [ 471 | "libc", 472 | ] 473 | 474 | [[package]] 475 | name = "hermit-abi" 476 | version = "0.2.6" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 479 | dependencies = [ 480 | "libc", 481 | ] 482 | 483 | [[package]] 484 | name = "home" 485 | version = "0.5.4" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408" 488 | dependencies = [ 489 | "winapi", 490 | ] 491 | 492 | [[package]] 493 | name = "http" 494 | version = "0.2.9" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 497 | dependencies = [ 498 | "bytes", 499 | "fnv", 500 | "itoa", 501 | ] 502 | 503 | [[package]] 504 | name = "http-body" 505 | version = "0.4.5" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 508 | dependencies = [ 509 | "bytes", 510 | "http", 511 | "pin-project-lite", 512 | ] 513 | 514 | [[package]] 515 | name = "httparse" 516 | version = "1.8.0" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 519 | 520 | [[package]] 521 | name = "httpdate" 522 | version = "1.0.2" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 525 | 526 | [[package]] 527 | name = "hyper" 528 | version = "0.14.24" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" 531 | dependencies = [ 532 | "bytes", 533 | "futures-channel", 534 | "futures-core", 535 | "futures-util", 536 | "h2", 537 | "http", 538 | "http-body", 539 | "httparse", 540 | "httpdate", 541 | "itoa", 542 | "pin-project-lite", 543 | "socket2", 544 | "tokio", 545 | "tower-service", 546 | "tracing", 547 | "want", 548 | ] 549 | 550 | [[package]] 551 | name = "hyper-tls" 552 | version = "0.5.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 555 | dependencies = [ 556 | "bytes", 557 | "hyper", 558 | "native-tls", 559 | "tokio", 560 | "tokio-native-tls", 561 | ] 562 | 563 | [[package]] 564 | name = "icu_collections" 565 | version = "1.5.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 568 | dependencies = [ 569 | "displaydoc", 570 | "yoke", 571 | "zerofrom", 572 | "zerovec", 573 | ] 574 | 575 | [[package]] 576 | name = "icu_locid" 577 | version = "1.5.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 580 | dependencies = [ 581 | "displaydoc", 582 | "litemap", 583 | "tinystr", 584 | "writeable", 585 | "zerovec", 586 | ] 587 | 588 | [[package]] 589 | name = "icu_locid_transform" 590 | version = "1.5.0" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 593 | dependencies = [ 594 | "displaydoc", 595 | "icu_locid", 596 | "icu_locid_transform_data", 597 | "icu_provider", 598 | "tinystr", 599 | "zerovec", 600 | ] 601 | 602 | [[package]] 603 | name = "icu_locid_transform_data" 604 | version = "1.5.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 607 | 608 | [[package]] 609 | name = "icu_normalizer" 610 | version = "1.5.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 613 | dependencies = [ 614 | "displaydoc", 615 | "icu_collections", 616 | "icu_normalizer_data", 617 | "icu_properties", 618 | "icu_provider", 619 | "smallvec", 620 | "utf16_iter", 621 | "utf8_iter", 622 | "write16", 623 | "zerovec", 624 | ] 625 | 626 | [[package]] 627 | name = "icu_normalizer_data" 628 | version = "1.5.0" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 631 | 632 | [[package]] 633 | name = "icu_properties" 634 | version = "1.5.1" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 637 | dependencies = [ 638 | "displaydoc", 639 | "icu_collections", 640 | "icu_locid_transform", 641 | "icu_properties_data", 642 | "icu_provider", 643 | "tinystr", 644 | "zerovec", 645 | ] 646 | 647 | [[package]] 648 | name = "icu_properties_data" 649 | version = "1.5.0" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 652 | 653 | [[package]] 654 | name = "icu_provider" 655 | version = "1.5.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 658 | dependencies = [ 659 | "displaydoc", 660 | "icu_locid", 661 | "icu_provider_macros", 662 | "stable_deref_trait", 663 | "tinystr", 664 | "writeable", 665 | "yoke", 666 | "zerofrom", 667 | "zerovec", 668 | ] 669 | 670 | [[package]] 671 | name = "icu_provider_macros" 672 | version = "1.5.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 675 | dependencies = [ 676 | "proc-macro2", 677 | "quote", 678 | "syn 2.0.96", 679 | ] 680 | 681 | [[package]] 682 | name = "idna" 683 | version = "1.0.3" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 686 | dependencies = [ 687 | "idna_adapter", 688 | "smallvec", 689 | "utf8_iter", 690 | ] 691 | 692 | [[package]] 693 | name = "idna_adapter" 694 | version = "1.2.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 697 | dependencies = [ 698 | "icu_normalizer", 699 | "icu_properties", 700 | ] 701 | 702 | [[package]] 703 | name = "indexmap" 704 | version = "1.9.2" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 707 | dependencies = [ 708 | "autocfg", 709 | "hashbrown 0.12.3", 710 | ] 711 | 712 | [[package]] 713 | name = "indexmap" 714 | version = "2.7.0" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 717 | dependencies = [ 718 | "equivalent", 719 | "hashbrown 0.15.2", 720 | ] 721 | 722 | [[package]] 723 | name = "inquire" 724 | version = "0.6.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "fd079157ad94a32f7511b2e13037f3ae417ad80a6a9b0de29154d48b86f5d6c8" 727 | dependencies = [ 728 | "bitflags 1.3.2", 729 | "crossterm 0.25.0", 730 | "dyn-clone", 731 | "lazy_static", 732 | "newline-converter", 733 | "thiserror", 734 | "unicode-segmentation", 735 | "unicode-width", 736 | ] 737 | 738 | [[package]] 739 | name = "instant" 740 | version = "0.1.12" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 743 | dependencies = [ 744 | "cfg-if", 745 | ] 746 | 747 | [[package]] 748 | name = "io-lifetimes" 749 | version = "1.0.5" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" 752 | dependencies = [ 753 | "libc", 754 | "windows-sys 0.45.0", 755 | ] 756 | 757 | [[package]] 758 | name = "ipnet" 759 | version = "2.7.1" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" 762 | 763 | [[package]] 764 | name = "itoa" 765 | version = "1.0.5" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 768 | 769 | [[package]] 770 | name = "jobserver" 771 | version = "0.1.26" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" 774 | dependencies = [ 775 | "libc", 776 | ] 777 | 778 | [[package]] 779 | name = "js-sys" 780 | version = "0.3.61" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 783 | dependencies = [ 784 | "wasm-bindgen", 785 | ] 786 | 787 | [[package]] 788 | name = "lazy_static" 789 | version = "1.4.0" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 792 | 793 | [[package]] 794 | name = "libc" 795 | version = "0.2.169" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 798 | 799 | [[package]] 800 | name = "libgit2-sys" 801 | version = "0.16.2+1.7.2" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" 804 | dependencies = [ 805 | "cc", 806 | "libc", 807 | "libssh2-sys", 808 | "libz-sys", 809 | "openssl-sys", 810 | "pkg-config", 811 | ] 812 | 813 | [[package]] 814 | name = "libssh2-sys" 815 | version = "0.3.1" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" 818 | dependencies = [ 819 | "cc", 820 | "libc", 821 | "libz-sys", 822 | "openssl-sys", 823 | "pkg-config", 824 | "vcpkg", 825 | ] 826 | 827 | [[package]] 828 | name = "libz-sys" 829 | version = "1.1.8" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" 832 | dependencies = [ 833 | "cc", 834 | "libc", 835 | "pkg-config", 836 | "vcpkg", 837 | ] 838 | 839 | [[package]] 840 | name = "linux-raw-sys" 841 | version = "0.1.4" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 844 | 845 | [[package]] 846 | name = "litemap" 847 | version = "0.7.4" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 850 | 851 | [[package]] 852 | name = "lock_api" 853 | version = "0.4.9" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 856 | dependencies = [ 857 | "autocfg", 858 | "scopeguard", 859 | ] 860 | 861 | [[package]] 862 | name = "log" 863 | version = "0.4.17" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 866 | dependencies = [ 867 | "cfg-if", 868 | ] 869 | 870 | [[package]] 871 | name = "memchr" 872 | version = "2.5.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 875 | 876 | [[package]] 877 | name = "mime" 878 | version = "0.3.16" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 881 | 882 | [[package]] 883 | name = "minimal-lexical" 884 | version = "0.2.1" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 887 | 888 | [[package]] 889 | name = "mio" 890 | version = "0.8.11" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 893 | dependencies = [ 894 | "libc", 895 | "log", 896 | "wasi", 897 | "windows-sys 0.48.0", 898 | ] 899 | 900 | [[package]] 901 | name = "native-tls" 902 | version = "0.2.11" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 905 | dependencies = [ 906 | "lazy_static", 907 | "libc", 908 | "log", 909 | "openssl", 910 | "openssl-probe", 911 | "openssl-sys", 912 | "schannel", 913 | "security-framework", 914 | "security-framework-sys", 915 | "tempfile", 916 | ] 917 | 918 | [[package]] 919 | name = "newline-converter" 920 | version = "0.2.2" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" 923 | dependencies = [ 924 | "unicode-segmentation", 925 | ] 926 | 927 | [[package]] 928 | name = "nom" 929 | version = "7.1.3" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 932 | dependencies = [ 933 | "memchr", 934 | "minimal-lexical", 935 | ] 936 | 937 | [[package]] 938 | name = "num-integer" 939 | version = "0.1.45" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 942 | dependencies = [ 943 | "autocfg", 944 | "num-traits", 945 | ] 946 | 947 | [[package]] 948 | name = "num-traits" 949 | version = "0.2.15" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 952 | dependencies = [ 953 | "autocfg", 954 | ] 955 | 956 | [[package]] 957 | name = "num_cpus" 958 | version = "1.15.0" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 961 | dependencies = [ 962 | "hermit-abi 0.2.6", 963 | "libc", 964 | ] 965 | 966 | [[package]] 967 | name = "once_cell" 968 | version = "1.17.1" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 971 | 972 | [[package]] 973 | name = "openssl" 974 | version = "0.10.70" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" 977 | dependencies = [ 978 | "bitflags 2.6.0", 979 | "cfg-if", 980 | "foreign-types", 981 | "libc", 982 | "once_cell", 983 | "openssl-macros", 984 | "openssl-sys", 985 | ] 986 | 987 | [[package]] 988 | name = "openssl-macros" 989 | version = "0.1.1" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 992 | dependencies = [ 993 | "proc-macro2", 994 | "quote", 995 | "syn 2.0.96", 996 | ] 997 | 998 | [[package]] 999 | name = "openssl-probe" 1000 | version = "0.1.5" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 1003 | 1004 | [[package]] 1005 | name = "openssl-sys" 1006 | version = "0.9.105" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" 1009 | dependencies = [ 1010 | "cc", 1011 | "libc", 1012 | "pkg-config", 1013 | "vcpkg", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "parking_lot" 1018 | version = "0.12.1" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1021 | dependencies = [ 1022 | "lock_api", 1023 | "parking_lot_core", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "parking_lot_core" 1028 | version = "0.9.7" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 1031 | dependencies = [ 1032 | "cfg-if", 1033 | "libc", 1034 | "redox_syscall", 1035 | "smallvec", 1036 | "windows-sys 0.45.0", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "percent-encoding" 1041 | version = "2.3.1" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1044 | 1045 | [[package]] 1046 | name = "pin-project-lite" 1047 | version = "0.2.9" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 1050 | 1051 | [[package]] 1052 | name = "pin-utils" 1053 | version = "0.1.0" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1056 | 1057 | [[package]] 1058 | name = "pkg-config" 1059 | version = "0.3.26" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 1062 | 1063 | [[package]] 1064 | name = "proc-macro2" 1065 | version = "1.0.93" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 1068 | dependencies = [ 1069 | "unicode-ident", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "quote" 1074 | version = "1.0.38" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1077 | dependencies = [ 1078 | "proc-macro2", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "redox_syscall" 1083 | version = "0.2.16" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1086 | dependencies = [ 1087 | "bitflags 1.3.2", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "regex" 1092 | version = "1.7.1" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" 1095 | dependencies = [ 1096 | "aho-corasick", 1097 | "memchr", 1098 | "regex-syntax", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "regex-automata" 1103 | version = "0.1.10" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1106 | 1107 | [[package]] 1108 | name = "regex-syntax" 1109 | version = "0.6.28" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 1112 | 1113 | [[package]] 1114 | name = "reqwest" 1115 | version = "0.11.14" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" 1118 | dependencies = [ 1119 | "base64", 1120 | "bytes", 1121 | "encoding_rs", 1122 | "futures-core", 1123 | "futures-util", 1124 | "h2", 1125 | "http", 1126 | "http-body", 1127 | "hyper", 1128 | "hyper-tls", 1129 | "ipnet", 1130 | "js-sys", 1131 | "log", 1132 | "mime", 1133 | "native-tls", 1134 | "once_cell", 1135 | "percent-encoding", 1136 | "pin-project-lite", 1137 | "serde", 1138 | "serde_json", 1139 | "serde_urlencoded", 1140 | "tokio", 1141 | "tokio-native-tls", 1142 | "tokio-util", 1143 | "tower-service", 1144 | "url", 1145 | "wasm-bindgen", 1146 | "wasm-bindgen-futures", 1147 | "wasm-streams", 1148 | "web-sys", 1149 | "winreg", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "reqwest-eventsource" 1154 | version = "0.4.0" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "8f03f570355882dd8d15acc3a313841e6e90eddbc76a93c748fd82cc13ba9f51" 1157 | dependencies = [ 1158 | "eventsource-stream", 1159 | "futures-core", 1160 | "futures-timer", 1161 | "mime", 1162 | "nom", 1163 | "pin-project-lite", 1164 | "reqwest", 1165 | "thiserror", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "rustc-hash" 1170 | version = "1.1.0" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1173 | 1174 | [[package]] 1175 | name = "rustix" 1176 | version = "0.36.17" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" 1179 | dependencies = [ 1180 | "bitflags 1.3.2", 1181 | "errno", 1182 | "io-lifetimes", 1183 | "libc", 1184 | "linux-raw-sys", 1185 | "windows-sys 0.45.0", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "ryu" 1190 | version = "1.0.12" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 1193 | 1194 | [[package]] 1195 | name = "schannel" 1196 | version = "0.1.21" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 1199 | dependencies = [ 1200 | "windows-sys 0.42.0", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "scopeguard" 1205 | version = "1.1.0" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1208 | 1209 | [[package]] 1210 | name = "security-framework" 1211 | version = "2.8.2" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" 1214 | dependencies = [ 1215 | "bitflags 1.3.2", 1216 | "core-foundation", 1217 | "core-foundation-sys", 1218 | "libc", 1219 | "security-framework-sys", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "security-framework-sys" 1224 | version = "2.8.0" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" 1227 | dependencies = [ 1228 | "core-foundation-sys", 1229 | "libc", 1230 | ] 1231 | 1232 | [[package]] 1233 | name = "serde" 1234 | version = "1.0.152" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 1237 | dependencies = [ 1238 | "serde_derive", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "serde_derive" 1243 | version = "1.0.152" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 1246 | dependencies = [ 1247 | "proc-macro2", 1248 | "quote", 1249 | "syn 1.0.109", 1250 | ] 1251 | 1252 | [[package]] 1253 | name = "serde_json" 1254 | version = "1.0.93" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" 1257 | dependencies = [ 1258 | "itoa", 1259 | "ryu", 1260 | "serde", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "serde_path_to_error" 1265 | version = "0.1.10" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "db0969fff533976baadd92e08b1d102c5a3d8a8049eadfd69d4d1e3c5b2ed189" 1268 | dependencies = [ 1269 | "serde", 1270 | ] 1271 | 1272 | [[package]] 1273 | name = "serde_urlencoded" 1274 | version = "0.7.1" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1277 | dependencies = [ 1278 | "form_urlencoded", 1279 | "itoa", 1280 | "ryu", 1281 | "serde", 1282 | ] 1283 | 1284 | [[package]] 1285 | name = "serde_yaml" 1286 | version = "0.9.19" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10" 1289 | dependencies = [ 1290 | "indexmap 1.9.2", 1291 | "itoa", 1292 | "ryu", 1293 | "serde", 1294 | "unsafe-libyaml", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "signal-hook" 1299 | version = "0.3.15" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" 1302 | dependencies = [ 1303 | "libc", 1304 | "signal-hook-registry", 1305 | ] 1306 | 1307 | [[package]] 1308 | name = "signal-hook-mio" 1309 | version = "0.2.3" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 1312 | dependencies = [ 1313 | "libc", 1314 | "mio", 1315 | "signal-hook", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "signal-hook-registry" 1320 | version = "1.4.1" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1323 | dependencies = [ 1324 | "libc", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "slab" 1329 | version = "0.4.8" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 1332 | dependencies = [ 1333 | "autocfg", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "smallvec" 1338 | version = "1.13.2" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1341 | 1342 | [[package]] 1343 | name = "socket2" 1344 | version = "0.4.7" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 1347 | dependencies = [ 1348 | "libc", 1349 | "winapi", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "stable_deref_trait" 1354 | version = "1.2.0" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1357 | 1358 | [[package]] 1359 | name = "syn" 1360 | version = "1.0.109" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1363 | dependencies = [ 1364 | "proc-macro2", 1365 | "quote", 1366 | "unicode-ident", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "syn" 1371 | version = "2.0.96" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 1374 | dependencies = [ 1375 | "proc-macro2", 1376 | "quote", 1377 | "unicode-ident", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "synstructure" 1382 | version = "0.13.1" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1385 | dependencies = [ 1386 | "proc-macro2", 1387 | "quote", 1388 | "syn 2.0.96", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "tempfile" 1393 | version = "3.4.0" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 1396 | dependencies = [ 1397 | "cfg-if", 1398 | "fastrand", 1399 | "redox_syscall", 1400 | "rustix", 1401 | "windows-sys 0.42.0", 1402 | ] 1403 | 1404 | [[package]] 1405 | name = "terminal-supports-emoji" 1406 | version = "0.1.3" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "c8873a7a1f2d286cfedc10663a722309b1c74092852cf149aee738cbe901c6eb" 1409 | dependencies = [ 1410 | "atty", 1411 | "lazy_static", 1412 | ] 1413 | 1414 | [[package]] 1415 | name = "thiserror" 1416 | version = "1.0.38" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 1419 | dependencies = [ 1420 | "thiserror-impl", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "thiserror-impl" 1425 | version = "1.0.38" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 1428 | dependencies = [ 1429 | "proc-macro2", 1430 | "quote", 1431 | "syn 1.0.109", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "tiktoken-rs" 1436 | version = "0.2.2" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "7bdbfb7cc1905920c7b0487c47c3ea858a979b85ed41b74dfe411ca6e3f6fcdf" 1439 | dependencies = [ 1440 | "anyhow", 1441 | "base64", 1442 | "bstr", 1443 | "fancy-regex", 1444 | "lazy_static", 1445 | "parking_lot", 1446 | "rustc-hash", 1447 | ] 1448 | 1449 | [[package]] 1450 | name = "tinystr" 1451 | version = "0.7.6" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1454 | dependencies = [ 1455 | "displaydoc", 1456 | "zerovec", 1457 | ] 1458 | 1459 | [[package]] 1460 | name = "tokio" 1461 | version = "1.26.0" 1462 | source = "registry+https://github.com/rust-lang/crates.io-index" 1463 | checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" 1464 | dependencies = [ 1465 | "autocfg", 1466 | "bytes", 1467 | "libc", 1468 | "memchr", 1469 | "mio", 1470 | "num_cpus", 1471 | "parking_lot", 1472 | "pin-project-lite", 1473 | "signal-hook-registry", 1474 | "socket2", 1475 | "tokio-macros", 1476 | "windows-sys 0.45.0", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "tokio-macros" 1481 | version = "1.8.2" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 1484 | dependencies = [ 1485 | "proc-macro2", 1486 | "quote", 1487 | "syn 1.0.109", 1488 | ] 1489 | 1490 | [[package]] 1491 | name = "tokio-native-tls" 1492 | version = "0.3.1" 1493 | source = "registry+https://github.com/rust-lang/crates.io-index" 1494 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1495 | dependencies = [ 1496 | "native-tls", 1497 | "tokio", 1498 | ] 1499 | 1500 | [[package]] 1501 | name = "tokio-util" 1502 | version = "0.7.7" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" 1505 | dependencies = [ 1506 | "bytes", 1507 | "futures-core", 1508 | "futures-sink", 1509 | "pin-project-lite", 1510 | "tokio", 1511 | "tracing", 1512 | ] 1513 | 1514 | [[package]] 1515 | name = "tower-service" 1516 | version = "0.3.2" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1519 | 1520 | [[package]] 1521 | name = "tracing" 1522 | version = "0.1.37" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1525 | dependencies = [ 1526 | "cfg-if", 1527 | "pin-project-lite", 1528 | "tracing-core", 1529 | ] 1530 | 1531 | [[package]] 1532 | name = "tracing-core" 1533 | version = "0.1.30" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1536 | dependencies = [ 1537 | "once_cell", 1538 | ] 1539 | 1540 | [[package]] 1541 | name = "try-lock" 1542 | version = "0.2.4" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1545 | 1546 | [[package]] 1547 | name = "turbocommit" 1548 | version = "1.5.4" 1549 | dependencies = [ 1550 | "anyhow", 1551 | "colored", 1552 | "crates_io_api", 1553 | "crossterm 0.26.1", 1554 | "edit", 1555 | "futures", 1556 | "git2", 1557 | "home", 1558 | "inquire", 1559 | "reqwest", 1560 | "reqwest-eventsource", 1561 | "serde", 1562 | "serde_json", 1563 | "serde_yaml", 1564 | "tempfile", 1565 | "terminal-supports-emoji", 1566 | "tiktoken-rs", 1567 | "tokio", 1568 | "unicode-segmentation", 1569 | "url", 1570 | ] 1571 | 1572 | [[package]] 1573 | name = "unicode-ident" 1574 | version = "1.0.6" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 1577 | 1578 | [[package]] 1579 | name = "unicode-segmentation" 1580 | version = "1.10.1" 1581 | source = "registry+https://github.com/rust-lang/crates.io-index" 1582 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 1583 | 1584 | [[package]] 1585 | name = "unicode-width" 1586 | version = "0.1.10" 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" 1588 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 1589 | 1590 | [[package]] 1591 | name = "unsafe-libyaml" 1592 | version = "0.2.11" 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" 1594 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1595 | 1596 | [[package]] 1597 | name = "url" 1598 | version = "2.5.4" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1601 | dependencies = [ 1602 | "form_urlencoded", 1603 | "idna", 1604 | "percent-encoding", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "utf16_iter" 1609 | version = "1.0.5" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1612 | 1613 | [[package]] 1614 | name = "utf8_iter" 1615 | version = "1.0.4" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1618 | 1619 | [[package]] 1620 | name = "vcpkg" 1621 | version = "0.2.15" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1624 | 1625 | [[package]] 1626 | name = "want" 1627 | version = "0.3.0" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1630 | dependencies = [ 1631 | "log", 1632 | "try-lock", 1633 | ] 1634 | 1635 | [[package]] 1636 | name = "wasi" 1637 | version = "0.11.0+wasi-snapshot-preview1" 1638 | source = "registry+https://github.com/rust-lang/crates.io-index" 1639 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1640 | 1641 | [[package]] 1642 | name = "wasm-bindgen" 1643 | version = "0.2.84" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 1646 | dependencies = [ 1647 | "cfg-if", 1648 | "wasm-bindgen-macro", 1649 | ] 1650 | 1651 | [[package]] 1652 | name = "wasm-bindgen-backend" 1653 | version = "0.2.84" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 1656 | dependencies = [ 1657 | "bumpalo", 1658 | "log", 1659 | "once_cell", 1660 | "proc-macro2", 1661 | "quote", 1662 | "syn 1.0.109", 1663 | "wasm-bindgen-shared", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "wasm-bindgen-futures" 1668 | version = "0.4.34" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" 1671 | dependencies = [ 1672 | "cfg-if", 1673 | "js-sys", 1674 | "wasm-bindgen", 1675 | "web-sys", 1676 | ] 1677 | 1678 | [[package]] 1679 | name = "wasm-bindgen-macro" 1680 | version = "0.2.84" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 1683 | dependencies = [ 1684 | "quote", 1685 | "wasm-bindgen-macro-support", 1686 | ] 1687 | 1688 | [[package]] 1689 | name = "wasm-bindgen-macro-support" 1690 | version = "0.2.84" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 1693 | dependencies = [ 1694 | "proc-macro2", 1695 | "quote", 1696 | "syn 1.0.109", 1697 | "wasm-bindgen-backend", 1698 | "wasm-bindgen-shared", 1699 | ] 1700 | 1701 | [[package]] 1702 | name = "wasm-bindgen-shared" 1703 | version = "0.2.84" 1704 | source = "registry+https://github.com/rust-lang/crates.io-index" 1705 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 1706 | 1707 | [[package]] 1708 | name = "wasm-streams" 1709 | version = "0.2.3" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" 1712 | dependencies = [ 1713 | "futures-util", 1714 | "js-sys", 1715 | "wasm-bindgen", 1716 | "wasm-bindgen-futures", 1717 | "web-sys", 1718 | ] 1719 | 1720 | [[package]] 1721 | name = "web-sys" 1722 | version = "0.3.61" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" 1725 | dependencies = [ 1726 | "js-sys", 1727 | "wasm-bindgen", 1728 | ] 1729 | 1730 | [[package]] 1731 | name = "which" 1732 | version = "4.4.0" 1733 | source = "registry+https://github.com/rust-lang/crates.io-index" 1734 | checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" 1735 | dependencies = [ 1736 | "either", 1737 | "libc", 1738 | "once_cell", 1739 | ] 1740 | 1741 | [[package]] 1742 | name = "winapi" 1743 | version = "0.3.9" 1744 | source = "registry+https://github.com/rust-lang/crates.io-index" 1745 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1746 | dependencies = [ 1747 | "winapi-i686-pc-windows-gnu", 1748 | "winapi-x86_64-pc-windows-gnu", 1749 | ] 1750 | 1751 | [[package]] 1752 | name = "winapi-i686-pc-windows-gnu" 1753 | version = "0.4.0" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1756 | 1757 | [[package]] 1758 | name = "winapi-x86_64-pc-windows-gnu" 1759 | version = "0.4.0" 1760 | source = "registry+https://github.com/rust-lang/crates.io-index" 1761 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1762 | 1763 | [[package]] 1764 | name = "windows-sys" 1765 | version = "0.42.0" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1768 | dependencies = [ 1769 | "windows_aarch64_gnullvm 0.42.1", 1770 | "windows_aarch64_msvc 0.42.1", 1771 | "windows_i686_gnu 0.42.1", 1772 | "windows_i686_msvc 0.42.1", 1773 | "windows_x86_64_gnu 0.42.1", 1774 | "windows_x86_64_gnullvm 0.42.1", 1775 | "windows_x86_64_msvc 0.42.1", 1776 | ] 1777 | 1778 | [[package]] 1779 | name = "windows-sys" 1780 | version = "0.45.0" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1783 | dependencies = [ 1784 | "windows-targets 0.42.1", 1785 | ] 1786 | 1787 | [[package]] 1788 | name = "windows-sys" 1789 | version = "0.48.0" 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" 1791 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1792 | dependencies = [ 1793 | "windows-targets 0.48.5", 1794 | ] 1795 | 1796 | [[package]] 1797 | name = "windows-sys" 1798 | version = "0.59.0" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1801 | dependencies = [ 1802 | "windows-targets 0.52.6", 1803 | ] 1804 | 1805 | [[package]] 1806 | name = "windows-targets" 1807 | version = "0.42.1" 1808 | source = "registry+https://github.com/rust-lang/crates.io-index" 1809 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 1810 | dependencies = [ 1811 | "windows_aarch64_gnullvm 0.42.1", 1812 | "windows_aarch64_msvc 0.42.1", 1813 | "windows_i686_gnu 0.42.1", 1814 | "windows_i686_msvc 0.42.1", 1815 | "windows_x86_64_gnu 0.42.1", 1816 | "windows_x86_64_gnullvm 0.42.1", 1817 | "windows_x86_64_msvc 0.42.1", 1818 | ] 1819 | 1820 | [[package]] 1821 | name = "windows-targets" 1822 | version = "0.48.5" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1825 | dependencies = [ 1826 | "windows_aarch64_gnullvm 0.48.5", 1827 | "windows_aarch64_msvc 0.48.5", 1828 | "windows_i686_gnu 0.48.5", 1829 | "windows_i686_msvc 0.48.5", 1830 | "windows_x86_64_gnu 0.48.5", 1831 | "windows_x86_64_gnullvm 0.48.5", 1832 | "windows_x86_64_msvc 0.48.5", 1833 | ] 1834 | 1835 | [[package]] 1836 | name = "windows-targets" 1837 | version = "0.52.6" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1840 | dependencies = [ 1841 | "windows_aarch64_gnullvm 0.52.6", 1842 | "windows_aarch64_msvc 0.52.6", 1843 | "windows_i686_gnu 0.52.6", 1844 | "windows_i686_gnullvm", 1845 | "windows_i686_msvc 0.52.6", 1846 | "windows_x86_64_gnu 0.52.6", 1847 | "windows_x86_64_gnullvm 0.52.6", 1848 | "windows_x86_64_msvc 0.52.6", 1849 | ] 1850 | 1851 | [[package]] 1852 | name = "windows_aarch64_gnullvm" 1853 | version = "0.42.1" 1854 | source = "registry+https://github.com/rust-lang/crates.io-index" 1855 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 1856 | 1857 | [[package]] 1858 | name = "windows_aarch64_gnullvm" 1859 | version = "0.48.5" 1860 | source = "registry+https://github.com/rust-lang/crates.io-index" 1861 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1862 | 1863 | [[package]] 1864 | name = "windows_aarch64_gnullvm" 1865 | version = "0.52.6" 1866 | source = "registry+https://github.com/rust-lang/crates.io-index" 1867 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1868 | 1869 | [[package]] 1870 | name = "windows_aarch64_msvc" 1871 | version = "0.42.1" 1872 | source = "registry+https://github.com/rust-lang/crates.io-index" 1873 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 1874 | 1875 | [[package]] 1876 | name = "windows_aarch64_msvc" 1877 | version = "0.48.5" 1878 | source = "registry+https://github.com/rust-lang/crates.io-index" 1879 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1880 | 1881 | [[package]] 1882 | name = "windows_aarch64_msvc" 1883 | version = "0.52.6" 1884 | source = "registry+https://github.com/rust-lang/crates.io-index" 1885 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1886 | 1887 | [[package]] 1888 | name = "windows_i686_gnu" 1889 | version = "0.42.1" 1890 | source = "registry+https://github.com/rust-lang/crates.io-index" 1891 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 1892 | 1893 | [[package]] 1894 | name = "windows_i686_gnu" 1895 | version = "0.48.5" 1896 | source = "registry+https://github.com/rust-lang/crates.io-index" 1897 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1898 | 1899 | [[package]] 1900 | name = "windows_i686_gnu" 1901 | version = "0.52.6" 1902 | source = "registry+https://github.com/rust-lang/crates.io-index" 1903 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1904 | 1905 | [[package]] 1906 | name = "windows_i686_gnullvm" 1907 | version = "0.52.6" 1908 | source = "registry+https://github.com/rust-lang/crates.io-index" 1909 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1910 | 1911 | [[package]] 1912 | name = "windows_i686_msvc" 1913 | version = "0.42.1" 1914 | source = "registry+https://github.com/rust-lang/crates.io-index" 1915 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 1916 | 1917 | [[package]] 1918 | name = "windows_i686_msvc" 1919 | version = "0.48.5" 1920 | source = "registry+https://github.com/rust-lang/crates.io-index" 1921 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1922 | 1923 | [[package]] 1924 | name = "windows_i686_msvc" 1925 | version = "0.52.6" 1926 | source = "registry+https://github.com/rust-lang/crates.io-index" 1927 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1928 | 1929 | [[package]] 1930 | name = "windows_x86_64_gnu" 1931 | version = "0.42.1" 1932 | source = "registry+https://github.com/rust-lang/crates.io-index" 1933 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 1934 | 1935 | [[package]] 1936 | name = "windows_x86_64_gnu" 1937 | version = "0.48.5" 1938 | source = "registry+https://github.com/rust-lang/crates.io-index" 1939 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1940 | 1941 | [[package]] 1942 | name = "windows_x86_64_gnu" 1943 | version = "0.52.6" 1944 | source = "registry+https://github.com/rust-lang/crates.io-index" 1945 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1946 | 1947 | [[package]] 1948 | name = "windows_x86_64_gnullvm" 1949 | version = "0.42.1" 1950 | source = "registry+https://github.com/rust-lang/crates.io-index" 1951 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 1952 | 1953 | [[package]] 1954 | name = "windows_x86_64_gnullvm" 1955 | version = "0.48.5" 1956 | source = "registry+https://github.com/rust-lang/crates.io-index" 1957 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1958 | 1959 | [[package]] 1960 | name = "windows_x86_64_gnullvm" 1961 | version = "0.52.6" 1962 | source = "registry+https://github.com/rust-lang/crates.io-index" 1963 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1964 | 1965 | [[package]] 1966 | name = "windows_x86_64_msvc" 1967 | version = "0.42.1" 1968 | source = "registry+https://github.com/rust-lang/crates.io-index" 1969 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 1970 | 1971 | [[package]] 1972 | name = "windows_x86_64_msvc" 1973 | version = "0.48.5" 1974 | source = "registry+https://github.com/rust-lang/crates.io-index" 1975 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1976 | 1977 | [[package]] 1978 | name = "windows_x86_64_msvc" 1979 | version = "0.52.6" 1980 | source = "registry+https://github.com/rust-lang/crates.io-index" 1981 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1982 | 1983 | [[package]] 1984 | name = "winreg" 1985 | version = "0.10.1" 1986 | source = "registry+https://github.com/rust-lang/crates.io-index" 1987 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 1988 | dependencies = [ 1989 | "winapi", 1990 | ] 1991 | 1992 | [[package]] 1993 | name = "write16" 1994 | version = "1.0.0" 1995 | source = "registry+https://github.com/rust-lang/crates.io-index" 1996 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1997 | 1998 | [[package]] 1999 | name = "writeable" 2000 | version = "0.5.5" 2001 | source = "registry+https://github.com/rust-lang/crates.io-index" 2002 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2003 | 2004 | [[package]] 2005 | name = "yoke" 2006 | version = "0.7.5" 2007 | source = "registry+https://github.com/rust-lang/crates.io-index" 2008 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 2009 | dependencies = [ 2010 | "serde", 2011 | "stable_deref_trait", 2012 | "yoke-derive", 2013 | "zerofrom", 2014 | ] 2015 | 2016 | [[package]] 2017 | name = "yoke-derive" 2018 | version = "0.7.5" 2019 | source = "registry+https://github.com/rust-lang/crates.io-index" 2020 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 2021 | dependencies = [ 2022 | "proc-macro2", 2023 | "quote", 2024 | "syn 2.0.96", 2025 | "synstructure", 2026 | ] 2027 | 2028 | [[package]] 2029 | name = "zerofrom" 2030 | version = "0.1.5" 2031 | source = "registry+https://github.com/rust-lang/crates.io-index" 2032 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 2033 | dependencies = [ 2034 | "zerofrom-derive", 2035 | ] 2036 | 2037 | [[package]] 2038 | name = "zerofrom-derive" 2039 | version = "0.1.5" 2040 | source = "registry+https://github.com/rust-lang/crates.io-index" 2041 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 2042 | dependencies = [ 2043 | "proc-macro2", 2044 | "quote", 2045 | "syn 2.0.96", 2046 | "synstructure", 2047 | ] 2048 | 2049 | [[package]] 2050 | name = "zerovec" 2051 | version = "0.10.4" 2052 | source = "registry+https://github.com/rust-lang/crates.io-index" 2053 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2054 | dependencies = [ 2055 | "yoke", 2056 | "zerofrom", 2057 | "zerovec-derive", 2058 | ] 2059 | 2060 | [[package]] 2061 | name = "zerovec-derive" 2062 | version = "0.10.3" 2063 | source = "registry+https://github.com/rust-lang/crates.io-index" 2064 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2065 | dependencies = [ 2066 | "proc-macro2", 2067 | "quote", 2068 | "syn 2.0.96", 2069 | ] 2070 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "turbocommit" 3 | version = "1.5.4" 4 | edition = "2021" 5 | authors = [ "dikkadev",] 6 | description = "A CLI tool to create commit messages with OpenAI GPT models" 7 | readme = "README.md" 8 | keywords = [ "chatgpt", "commit", "commit_message", "cli", "command-line",] 9 | repository = "https://github.com/dikkadev/turboCommit" 10 | homepage = "https://github.com/dikkadev/turboCommit" 11 | documentation = "https://github.com/dikkadev/turboCommit" 12 | license = "MIT" 13 | 14 | [dependencies] 15 | anyhow = "1.0.69" 16 | colored = "2.0.0" 17 | crates_io_api = "0.8.1" 18 | crossterm = "0.26.1" 19 | edit = "0.1.4" 20 | futures = "0.3.27" 21 | git2 = "0.18.2" 22 | home = "0.5.4" 23 | inquire = "0.6.0" 24 | reqwest-eventsource = "0.4.0" 25 | serde_json = "1.0.93" 26 | serde_yaml = "0.9.19" 27 | terminal-supports-emoji = "0.1.3" 28 | tiktoken-rs = "0.2.2" 29 | unicode-segmentation = "1.10.1" 30 | url = "2.4.1" 31 | 32 | [dev-dependencies] 33 | tempfile = "3.4.0" 34 | 35 | [dependencies.reqwest] 36 | version = "0.11.14" 37 | features = [ "stream",] 38 | 39 | [dependencies.serde] 40 | version = "1.0.152" 41 | features = [ "derive",] 42 | 43 | [dependencies.tokio] 44 | version = "1.26.0" 45 | features = [ "full",] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Raik Rohde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # turboCommit 2 | 3 | ![Crates.io](https://img.shields.io/crates/v/turbocommit) 4 | ![Crates.io](https://img.shields.io/crates/d/turbocommit) 5 | ![Crates.io](https://img.shields.io/crates/l/turbocommit) 6 | 7 | A powerful CLI tool that leverages OpenAI's GPT models to generate high-quality, conventional commit messages from your staged changes. 8 | 9 | ## Features 10 | 11 | - 🤖 Uses OpenAI's GPT models to analyze your staged changes 12 | - 📝 Generates conventional commit messages that follow best practices 13 | - 🎯 Interactive selection from multiple commit message suggestions 14 | - ✏️ Edit messages directly or request AI revisions 15 | - 🧠 Advanced reasoning mode for enhanced AI interactions 16 | - 🔍 Comprehensive debugging capabilities with file or stdout logging 17 | - ⚡ Streaming responses for real-time feedback 18 | - 🔄 Auto-update checks to keep you on the latest version 19 | - 🎨 Beautiful terminal UI with color-coded output 20 | - ⚙️ Configurable settings via YAML config file 21 | 22 | ## Installation 23 | 24 | ```bash 25 | cargo install turbocommit 26 | ``` 27 | 28 | Pro tip: Add an alias to your shell configuration for quicker access: 29 | ```bash 30 | # Add to your .bashrc, .zshrc, etc. 31 | alias tc='turbocommit' 32 | ``` 33 | 34 | ## Usage 35 | 36 | 1. Stage your changes: 37 | ```bash 38 | git add . # or stage specific files 39 | ``` 40 | 41 | 2. Generate commit messages: 42 | ```bash 43 | turbocommit # or 'tc' if you set up the alias 44 | ``` 45 | 46 | After generating commit messages, you can: 47 | - Select your preferred message from multiple suggestions 48 | - Edit the message directly before committing 49 | - Request AI revisions with additional context or requirements 50 | - Commit the message once you're satisfied 51 | 52 | ### Options 53 | 54 | - `-n ` - Number of commit message suggestions to generate 55 | - `-t ` - Temperature for GPT model (0.0 to 2.0) (no effect in reasoning mode) 56 | - `-f ` - Frequency penalty (-2.0 to 2.0) 57 | - `-m ` - Specify the GPT model to use 58 | - `-r, --enable-reasoning` - Enable support for models with reasoning capabilities (like o-series) 59 | - `--reasoning-effort ` - Set reasoning effort for supported models (low/medium/high, default: medium) 60 | - `-d, --debug` - Show basic debug info in console 61 | - `--debug-file ` - Write detailed debug logs to file (use '-' for stdout) 62 | - `--auto-commit` - Automatically commit with the generated message 63 | - `--amend` - Amend the last commit with the generated message (useful with Git hooks) 64 | - `--api-key ` - Provide API key directly 65 | - `--api-endpoint ` - Custom API endpoint URL 66 | - `-p, --print-once` - Disable streaming output 67 | 68 | #### Reasoning Mode 69 | When using models that support reasoning capabilities (like OpenAI's o-series), this mode enables their built-in reasoning features. These models are specifically designed to analyze code changes and generate commit messages with their own reasoning process. 70 | 71 | Example usage: 72 | ```bash 73 | turbocommit -r -m o3-mini -n 1 # Enable reasoning mode with default effort 74 | turbocommit -r --reasoning-effort high -m o3-mini -n 1 # Specify reasoning effort 75 | ``` 76 | 77 | #### Debugging 78 | Debug output helps troubleshoot API interactions: 79 | ```bash 80 | turbocommit -d # Basic info to console 81 | turbocommit --debug-file debug.log # Detailed logs to file 82 | turbocommit --debug-file - # Detailed logs to stdout 83 | ``` 84 | 85 | The debug logs include: 86 | - Request details (model, tokens, parameters) 87 | - API responses and errors 88 | - Timing information 89 | - Full request/response JSON (in file mode) 90 | 91 | ### Model-Specific Notes 92 | 93 | Different models have different capabilities and limitations: 94 | 95 | #### O-Series Models (e.g., o3-mini) 96 | - Support reasoning mode 97 | - Do not support temperature/frequency parameters 98 | - May not support multiple choices (`-n`) 99 | - Optimized for specific tasks 100 | 101 | #### Standard GPT Models 102 | - Support all parameters 103 | - Multiple choices available 104 | - Temperature and frequency tuning 105 | - Standard reasoning capabilities 106 | 107 | For more options, run: 108 | ```bash 109 | turbocommit --help 110 | ``` 111 | 112 | ## Configuration 113 | 114 | turboCommit creates a config file at `~/.turbocommit.yaml` on first run. You can customize: 115 | 116 | - Default model 117 | - API endpoint 118 | - Temperature and frequency penalty 119 | - Number of suggestions 120 | - System message prompt 121 | - Auto-update checks 122 | - Reasoning mode defaults 123 | - And more! 124 | 125 | Example configuration: 126 | ```yaml 127 | model: "gpt-4" 128 | default_temperature: 1.0 129 | default_frequency_penalty: 0.0 130 | default_number_of_choices: 3 131 | enable_reasoning: true 132 | reasoning_effort: "medium" 133 | disable_print_as_stream: false 134 | disable_auto_update_check: false 135 | ``` 136 | 137 | ### Multiple Config Files 138 | 139 | You can maintain multiple configuration files for different use cases (e.g., different providers or environments) and specify which one to use with the `-c` or `--config` option: 140 | 141 | ```bash 142 | # Use a local config file 143 | turbocommit -c ./local-config.yaml 144 | 145 | # Use a different provider's config 146 | turbocommit -c ~/.turbocommit-azure.yaml 147 | 148 | # Use the default config 149 | turbocommit # uses ~/.turbocommit.yaml 150 | ``` 151 | 152 | Each config file follows the same format as shown above. This allows you to easily switch between different configurations without modifying the default config file. 153 | 154 | ## Contributing 155 | 156 | Contributions are welcome! Feel free to open issues and pull requests. 157 | 158 | ## License 159 | 160 | Licensed under MIT - see the [LICENSE](LICENSE) file for details. 161 | 162 | ### Using turboCommit with --amend 163 | 164 | The `--amend` option allows you to change the commit message of your last commit. This is useful when: 165 | - You want to improve the message of your last commit 166 | - You want to fix a typo in your commit message 167 | - You want to add more context to your commit message 168 | 169 | Usage: 170 | ```bash 171 | # First, make sure you have no staged changes 172 | git status # Should show no staged changes 173 | 174 | # Then use --amend to improve the last commit's message 175 | turbocommit --amend # This will analyze the last commit's changes and suggest a new message 176 | ``` 177 | 178 | Important Notes: 179 | - When using `--amend`, you must not have any staged changes 180 | - The tool will analyze only the changes from your last commit 181 | - If you want to include new changes in the amended commit: 182 | 1. Either commit them first normally, then amend that commit 183 | 2. Or use `git commit --amend` manually to include them 184 | 185 | You can also combine this with auto-commit for a quick message update: 186 | ```bash 187 | turbocommit --amend --auto-commit # Automatically amend with the first generated message 188 | ``` 189 | 190 | ### Using turboCommit with Git Hooks 191 | 192 | If your project uses Git hooks (e.g., linters, formatters), here's how to use turboCommit effectively: 193 | 194 | 1. Stage and commit your changes normally: 195 | ```bash 196 | git add . 197 | turbocommit 198 | ``` 199 | 200 | 2. If hooks fail: 201 | - Fix the issues reported by hooks 202 | - Stage the fixed files (`git add .`) 203 | - Commit again 204 | 205 | 3. If you want to improve the commit message after all hooks pass: 206 | ```bash 207 | # Make sure you have no staged changes 208 | git status 209 | 210 | # Then improve the message 211 | turbocommit --amend # This will analyze the commit and suggest a better message 212 | ``` 213 | 214 | This workflow ensures that: 215 | - Code quality checks run before the commit 216 | - You can improve the commit message after all checks pass 217 | - The final commit message is high-quality and descriptive -------------------------------------------------------------------------------- /src/actor.rs: -------------------------------------------------------------------------------- 1 | use std::process; 2 | 3 | use colored::Colorize; 4 | use crossterm::execute; 5 | use crossterm::style::Print; 6 | use inquire::Select; 7 | 8 | use crate::cli::Options; 9 | use crate::{git, openai, util, debug_log::DebugLogger}; 10 | 11 | pub struct Actor { 12 | messages: Vec, 13 | options: Options, 14 | api_key: String, 15 | pub used_tokens: usize, 16 | api_endpoint: String, 17 | debug_logger: DebugLogger, 18 | } 19 | 20 | impl Actor { 21 | pub fn new(options: Options, api_key: String, api_endpoint: String) -> Self { 22 | // Get debug_file before moving options 23 | let debug_file = options.debug_file.clone(); 24 | Self { 25 | messages: Vec::new(), 26 | options, 27 | api_key, 28 | used_tokens: 0, 29 | api_endpoint, 30 | debug_logger: DebugLogger::new(debug_file), 31 | } 32 | } 33 | 34 | pub fn add_message(&mut self, message: openai::Message) { 35 | self.messages.push(message); 36 | } 37 | 38 | async fn ask(&mut self) -> anyhow::Result> { 39 | let n = if self.options.enable_reasoning { 1 } else { self.options.n }; 40 | let mut request = openai::Request::new( 41 | self.options.model.clone().to_string(), 42 | self.messages.clone(), 43 | n, 44 | self.options.t, 45 | self.options.f, 46 | ); 47 | 48 | // Add reasoning effort if reasoning mode is enabled 49 | if self.options.enable_reasoning { 50 | request = request.with_reasoning_effort(self.options.reasoning_effort.clone()); 51 | } 52 | 53 | // Log request details 54 | let json = serde_json::to_string(&request)?; 55 | self.debug_logger.log_request(&json); 56 | 57 | // Log basic info about the request 58 | let info = format!( 59 | "model={}, reasoning={}, effort={}, messages={}, tokens={}", 60 | self.options.model.0, 61 | self.options.enable_reasoning, 62 | self.options.reasoning_effort.as_deref().unwrap_or("none"), 63 | self.messages.len(), 64 | self.used_tokens 65 | ); 66 | self.debug_logger.log_info(&info); 67 | 68 | // Only show minimal info in regular debug mode 69 | if self.options.debug && self.options.debug_file.is_none() { 70 | println!("\n{}", "Request Info:".blue().bold()); 71 | println!(" Model: {}", self.options.model.0.purple()); 72 | if self.options.enable_reasoning { 73 | println!(" Reasoning: {} ({})", 74 | "enabled".purple(), 75 | self.options.reasoning_effort.as_deref().unwrap_or("medium").purple() 76 | ); 77 | } 78 | println!(" Messages: {}", self.messages.len().to_string().purple()); 79 | println!(" Tokens (input): {}", self.used_tokens.to_string().purple()); 80 | } 81 | 82 | match request 83 | .execute( 84 | self.api_key.clone(), 85 | self.options.print_once, 86 | self.used_tokens, 87 | self.api_endpoint.clone(), 88 | self.options.debug, 89 | &mut self.debug_logger, 90 | ) 91 | .await 92 | { 93 | Ok(choices) => { 94 | // Log successful response 95 | self.debug_logger.log_response(&format!( 96 | "success: generated {} choices", 97 | choices.len() 98 | )); 99 | Ok(choices) 100 | } 101 | Err(e) => { 102 | // Log error details 103 | self.debug_logger.log_error(&format!("API error: {:#?}", e)); 104 | Err(e) 105 | } 106 | } 107 | } 108 | 109 | pub async fn start(&mut self) -> anyhow::Result<()> { 110 | let first_choices = self.ask().await?; 111 | let mut message = match util::choose_message(first_choices) { 112 | Some(message) => message, 113 | None => { 114 | return Ok(()); 115 | } 116 | }; 117 | let tasks = vec![ 118 | Task::Commit.to_str(), 119 | Task::Edit.to_str(), 120 | Task::Revise.to_str(), 121 | Task::Abort.to_str(), 122 | ]; 123 | 124 | loop { 125 | let task = Select::new("What to do with the message?", tasks.clone()).prompt()?; 126 | 127 | match Task::from_str(task) { 128 | Task::Commit => { 129 | match git::commit(message, self.options.amend) { 130 | Ok(_) => {} 131 | Err(e) => { 132 | println!("{e}"); 133 | process::exit(1); 134 | } 135 | }; 136 | println!("{} 🎉", if self.options.amend { "Commit message amended!" } else { "Commit successful!" }.purple()); 137 | break; 138 | } 139 | Task::Edit => { 140 | message = edit::edit(message)?; 141 | execute!( 142 | std::io::stdout(), 143 | Print(format!( 144 | "{}\n", 145 | format!("[{}]=======", "Edited Message".purple()).bright_black() 146 | )), 147 | Print(&message), 148 | Print(format!("{}\n", "=======================".bright_black())), 149 | )?; 150 | } 151 | Task::Revise => { 152 | self.add_message(openai::Message::assistant(message.clone())); 153 | let input = inquire::Text::new("Revise:").prompt()?; 154 | self.add_message(openai::Message::user(input)); 155 | 156 | let choices = self.ask().await?; 157 | 158 | message = match util::choose_message(choices) { 159 | Some(message) => message, 160 | None => { 161 | return Ok(()); 162 | } 163 | }; 164 | } 165 | Task::Abort => { 166 | break; 167 | } 168 | } 169 | } 170 | 171 | Ok(()) 172 | } 173 | 174 | pub async fn auto_commit(&mut self) -> anyhow::Result { 175 | let choices = self.ask().await?; 176 | if choices.is_empty() { 177 | return Err(anyhow::anyhow!("No commit message generated")); 178 | } 179 | let message = choices[0].clone(); 180 | git::commit(message.clone(), self.options.amend)?; 181 | Ok(message) 182 | } 183 | } 184 | 185 | enum Task { 186 | Commit, 187 | Edit, 188 | Revise, 189 | Abort, 190 | } 191 | 192 | impl Task { 193 | pub fn from_str(s: &str) -> Self { 194 | match s { 195 | "Commit it" => Self::Commit, 196 | "Edit it & Commit" => Self::Edit, 197 | "Revise" => Self::Revise, 198 | "Abort" => Self::Abort, 199 | _ => unreachable!(), 200 | } 201 | } 202 | 203 | pub fn to_str(&self) -> &str { 204 | match self { 205 | Self::Commit => "Commit it", 206 | Self::Edit => "Edit it & Commit", 207 | Self::Revise => "Revise", 208 | Self::Abort => "Abort", 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/animation.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use crossterm::{ 3 | cursor::MoveToColumn, 4 | execute, 5 | style::{Color, Print, ResetColor, SetForegroundColor}, 6 | terminal::{Clear, ClearType}, 7 | }; 8 | use std::io::Write; 9 | use tokio::time::Duration; 10 | 11 | pub async fn start( 12 | message: String, 13 | no_animation: bool, 14 | writer: W, 15 | ) -> tokio::task::JoinHandle<()> { 16 | let mut writer = writer; 17 | tokio::spawn(async move { 18 | if no_animation { 19 | writeln!(writer, "{}", message.bright_black()).ok(); 20 | return; 21 | } 22 | let emoji_support = 23 | terminal_supports_emoji::supports_emoji(terminal_supports_emoji::Stream::Stdout); 24 | let frames = if emoji_support { 25 | vec![ 26 | "🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", 27 | ] 28 | } else { 29 | vec!["/", "-", "\\", "|"] 30 | }; 31 | let mut current_frame = 0; 32 | loop { 33 | current_frame = (current_frame + 1) % frames.len(); 34 | match execute!( 35 | writer, 36 | Clear(ClearType::CurrentLine), 37 | MoveToColumn(0), 38 | SetForegroundColor(Color::Yellow), 39 | Print(message.bright_black()), 40 | Print(frames[current_frame]), 41 | ResetColor 42 | ) { 43 | Ok(_) => {} 44 | Err(_) => {} 45 | } 46 | tokio::time::sleep(Duration::from_millis(100)).await; 47 | } 48 | }) 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | 54 | use std::io::Result; 55 | use std::sync::{Arc, Mutex}; 56 | 57 | use super::*; 58 | 59 | #[tokio::test] 60 | async fn test_animation() { 61 | let msg = String::from("Loading"); 62 | let buffer = Arc::new(Mutex::new(Vec::new())); 63 | let writer = SharedBufferWriter { 64 | buffer: buffer.clone(), 65 | }; 66 | let animation = start(msg, false, writer).await; 67 | tokio::time::sleep(Duration::from_millis(120)).await; 68 | animation.abort(); 69 | 70 | let locked_buffer = buffer.lock().unwrap(); 71 | let output = String::from_utf8(locked_buffer.clone()).unwrap(); 72 | 73 | let expected = "\u{1b}[2K\u{1b}[1G\u{1b}[38;5;11m\u{1b}[90mLoading\u{1b}[0m🕐\u{1b}[0m\u{1b}[2K\u{1b}[1G\u{1b}[38;5;11m\u{1b}[90mLoading\u{1b}[0m🕑\u{1b}[0m"; 74 | 75 | assert_eq!(output, expected); 76 | } 77 | 78 | struct SharedBufferWriter { 79 | buffer: Arc>>, 80 | } 81 | 82 | impl Write for SharedBufferWriter { 83 | fn write(&mut self, buf: &[u8]) -> Result { 84 | let mut buffer = self.buffer.lock().unwrap(); 85 | buffer.extend_from_slice(buf); 86 | Ok(buf.len()) 87 | } 88 | 89 | fn flush(&mut self) -> Result<()> { 90 | Ok(()) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::model; 3 | use crate::openai::count_token; 4 | use colored::Colorize; 5 | use std::str::FromStr; 6 | use std::{cmp, env, process}; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct Options { 10 | pub n: i32, 11 | pub msg: String, 12 | pub t: f64, 13 | pub f: f64, 14 | pub print_once: bool, 15 | pub model: model::Model, 16 | pub auto_commmit: bool, 17 | pub check_version_only: bool, 18 | pub api_endpoint: String, 19 | pub system_msg: Option, 20 | pub disable_auto_update_check: bool, 21 | pub api_key: Option, 22 | pub reasoning_effort: Option, 23 | pub enable_reasoning: bool, 24 | pub debug: bool, 25 | pub debug_file: Option, 26 | pub always_select_files: bool, 27 | pub config_file: Option, 28 | pub amend: bool, 29 | } 30 | 31 | impl From<&Config> for Options { 32 | fn from(config: &Config) -> Self { 33 | Self { 34 | n: config.default_number_of_choices, 35 | msg: String::new(), 36 | t: config.default_temperature, 37 | f: config.default_frequency_penalty, 38 | print_once: config.disable_print_as_stream, 39 | model: config.model.clone(), 40 | auto_commmit: false, 41 | check_version_only: false, 42 | api_endpoint: config.api_endpoint.clone(), 43 | system_msg: None, 44 | disable_auto_update_check: config.disable_auto_update_check, 45 | api_key: None, 46 | reasoning_effort: None, 47 | enable_reasoning: config.enable_reasoning, 48 | debug: false, 49 | debug_file: None, 50 | always_select_files: false, 51 | config_file: None, 52 | amend: false, 53 | } 54 | } 55 | } 56 | 57 | impl Options { 58 | pub fn new(args: I, conf: &Config) -> Self 59 | where 60 | I: Iterator, 61 | { 62 | let mut opts = Self::from(conf); 63 | let mut iter = args.skip(1); 64 | let mut msg = String::new(); 65 | 66 | while let Some(arg) = iter.next() { 67 | match arg.as_str() { 68 | "-n" => { 69 | if let Some(n) = iter.next() { 70 | opts.n = n.parse().map_or_else( 71 | |_| { 72 | println!( 73 | "{} {}", 74 | "Could not parse n.".red(), 75 | "Please enter an integer.".bright_black() 76 | ); 77 | process::exit(1); 78 | }, 79 | |n| cmp::max(1, n), 80 | ); 81 | } 82 | } 83 | "-t" => { 84 | if let Some(t) = iter.next() { 85 | opts.t = t.parse().map_or_else( 86 | |_| { 87 | println!( 88 | "{} {}", 89 | "Could not parse t.".red(), 90 | "Please enter a float between 0 and 2.".bright_black() 91 | ); 92 | process::exit(1); 93 | }, 94 | |t: f64| t.clamp(0.0, 2.0), 95 | ); 96 | } 97 | } 98 | "-f" => { 99 | if let Some(f) = iter.next() { 100 | opts.f = f.parse().map_or_else( 101 | |_| { 102 | println!( 103 | "{} {}", 104 | "Could not parse f.".red(), 105 | "Please enter a float between -2.0 and 2.0.".bright_black() 106 | ); 107 | process::exit(1); 108 | }, 109 | |f: f64| f.clamp(-2.0, 2.0), 110 | ); 111 | } 112 | } 113 | "-p" | "--print-once" => { 114 | opts.print_once = true; 115 | } 116 | "-m" | "--model" => { 117 | if let Some(model) = iter.next() { 118 | opts.model = match model::Model::from_str(&model) { 119 | Ok(model) => model, 120 | Err(err) => { 121 | println!( 122 | "{} {}", 123 | format!("Could not parse model: {}", err).red(), 124 | "Please enter a valid model.".bright_black() 125 | ); 126 | process::exit(1); 127 | } 128 | }; 129 | } 130 | } 131 | "-a" | "--auto-commit" => { 132 | opts.auto_commmit = true; 133 | opts.n = 1; 134 | opts.print_once = true; 135 | } 136 | "--amend" => { 137 | opts.amend = true; 138 | } 139 | "--check-version" => { 140 | opts.check_version_only = true; 141 | } 142 | "--api-endpoint" => { 143 | if let Some(endpoint) = iter.next() { 144 | opts.api_endpoint = endpoint; 145 | } 146 | } 147 | "--system-msg-file" => { 148 | if let Some(path) = iter.next() { 149 | match std::fs::read_to_string(&path) { 150 | Ok(content) => opts.system_msg = Some(content), 151 | Err(err) => { 152 | println!( 153 | "{} {}", 154 | format!("Could not read system message file: {}", err).red(), 155 | "Please provide a valid file path.".bright_black() 156 | ); 157 | process::exit(1); 158 | } 159 | } 160 | } 161 | } 162 | "--disable-auto-update-check" => { 163 | opts.disable_auto_update_check = true; 164 | } 165 | "--api-key" => { 166 | if let Some(key) = iter.next() { 167 | opts.api_key = Some(key); 168 | } 169 | } 170 | "--reasoning-effort" => { 171 | if let Some(effort) = iter.next() { 172 | if !["low", "medium", "high"].contains(&effort.as_str()) { 173 | println!( 174 | "{} {}", 175 | "Warning: Uncommon reasoning effort value.".yellow(), 176 | "Common values are: low, medium, high (depends on model/service)".bright_black() 177 | ); 178 | } 179 | opts.reasoning_effort = Some(effort); 180 | } 181 | } 182 | "-r" | "--enable-reasoning" => { 183 | opts.enable_reasoning = true; 184 | if opts.reasoning_effort.is_none() { 185 | opts.reasoning_effort = Some("medium".to_string()); 186 | } 187 | } 188 | "-d" | "--debug" => { 189 | opts.debug = true; 190 | opts.print_once = true; 191 | } 192 | "--debug-file" => { 193 | if let Some(path) = iter.next() { 194 | opts.debug_file = Some(path); 195 | opts.debug = true; 196 | opts.print_once = true; 197 | } 198 | } 199 | "--select-files" => { 200 | opts.always_select_files = true; 201 | } 202 | "-c" | "--config" => { 203 | if let Some(path) = iter.next() { 204 | opts.config_file = Some(path); 205 | } 206 | } 207 | "-h" | "--help" => help(), 208 | "-v" | "--version" => { 209 | println!("turbocommit version {}", env!("CARGO_PKG_VERSION").purple()); 210 | process::exit(0); 211 | } 212 | _ => { 213 | if arg.starts_with('-') { 214 | println!( 215 | "{} {} {}", 216 | "Unknown option: ".red(), 217 | arg.purple().bold(), 218 | "\nPlease use -h or --help for help.".bright_black() 219 | ); 220 | process::exit(1); 221 | } 222 | msg.push_str(&arg); 223 | msg.push(' '); 224 | } 225 | } 226 | } 227 | if !msg.is_empty() { 228 | opts.msg = format!("User Explanation/Instruction: '{}'", msg.trim()); 229 | } 230 | opts 231 | } 232 | } 233 | 234 | fn help() { 235 | println!("{}", " __ __".red()); 236 | println!("{}", " / /___ _______/ /_ ____".red()); 237 | println!("{}", " / __/ / / / ___/ __ \\/ __ \\".yellow()); 238 | println!("{}", " / /_/ /_/ / / / /_/ / /_/ /".green()); 239 | println!( 240 | "{}{}", 241 | " \\__/\\__,_/_/ /_.___/\\____/ ".blue(), 242 | "_ __".purple() 243 | ); 244 | println!("{}", " _________ ____ ___ ____ ___ (_) /_".purple()); 245 | println!("{}", " / ___/ __ \\/ __ `__ \\/ __ `__ \\/ / __/".red()); 246 | println!("{}", " / /__/ /_/ / / / / / / / / / / / / /_".yellow()); 247 | println!("{}", " \\___/\\____/_/ /_/ /_/_/ /_/ /_/_/\\__/".green()); 248 | 249 | println!("\nUsage: turbocommit [options] [message]\n"); 250 | println!("Options:"); 251 | println!(" -n Number of choices to generate"); 252 | println!(" Note: Some models (e.g., o-series) may not support multiple choices\n"); 253 | println!(" -m Model to use\n --model ",); 254 | println!(" Model can be any OpenAI compatible model name\n"); 255 | println!(" -p Will not print tokens as they are generated.\n --print-once \n",); 256 | println!( 257 | " -t Temperature (|t| 0.0 < t < 2.0)\n{}\n Note: Has no effect when using reasoning mode\n", 258 | "(https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature)" 259 | .bright_black() 260 | ); 261 | println!( 262 | " -f Frequency penalty (|f| -2.0 < f < 2.0)\n{}\n", 263 | "(https://platform.openai.com/docs/api-reference/chat/create#chat/create-frequency-penalty)" 264 | .bright_black() 265 | ); 266 | println!(" -a, --auto-commit Automatically generate and commit a single message\n"); 267 | println!(" --amend Amend the last commit with the generated message\n"); 268 | println!(" --check-version Check for updates and exit\n"); 269 | println!(" --api-endpoint Set the API endpoint URL\n"); 270 | println!(" --system-msg-file Load system message from a file\n"); 271 | println!(" --disable-auto-update-check Disable automatic update checks\n"); 272 | println!(" --api-key Set the API key\n"); 273 | println!(" -r, --enable-reasoning Enable support for models with reasoning capabilities (like o-series)\n"); 274 | println!(" --reasoning-effort Set the reasoning effort (defaults to 'medium', common values: low, medium, high)\n"); 275 | println!(" Note: Valid values depend on the model and service being used\n"); 276 | println!(" -d, --debug Enable debug mode (prints basic request/response info)\n"); 277 | println!(" --debug-file Write detailed debug logs to specified file (overwrites existing file)\n"); 278 | println!(" Use '-' to write to stdout instead of a file\n"); 279 | println!(" --select-files Always prompt for file selection, regardless of token count\n"); 280 | println!(" -c, --config Set the config file path\n"); 281 | println!("Anything else will be concatenated into an extra message given to the AI\n"); 282 | println!("You can change the defaults for these options and the system message prompt in the config file, that is created the first time running the program\n{}", 283 | home::home_dir().unwrap_or_else(|| "".into()).join(".turbocommit.yaml").display()); 284 | println!("To go back to the default system message, delete the config file.\n"); 285 | println!( 286 | "\nThe system message is about ~{} tokens long", 287 | format!( 288 | "{}", 289 | count_token(&crate::config::Config::load().unwrap_or_else(|e| { 290 | println!("{}", format!("Error loading config: {}", e).red()); 291 | process::exit(1); 292 | }).system_msg).unwrap_or(0) 293 | ) 294 | .green() 295 | ); 296 | process::exit(1); 297 | } 298 | 299 | #[cfg(test)] 300 | mod tests { 301 | use super::*; 302 | use crate::config::Config; 303 | 304 | #[test] 305 | fn test_options_from_config() { 306 | let config = Config::default(); 307 | let options = Options::from(&config); 308 | 309 | assert_eq!(options.n, config.default_number_of_choices); 310 | assert_eq!(options.t, config.default_temperature); 311 | assert_eq!(options.f, config.default_frequency_penalty); 312 | assert_eq!(options.print_once, config.disable_print_as_stream); 313 | assert_eq!(options.model, config.model); 314 | assert_eq!(options.enable_reasoning, config.enable_reasoning); 315 | assert_eq!(options.reasoning_effort, None); 316 | } 317 | 318 | #[test] 319 | fn test_options_new() { 320 | let config = Config::default(); 321 | let args = vec![ 322 | "turbocommit", 323 | "-n", 324 | "3", 325 | "-t", 326 | "1.0", 327 | "-f", 328 | "0.5", 329 | "--print-once", 330 | "--model", 331 | "gpt-4", 332 | "--enable-reasoning", 333 | "--reasoning-effort", 334 | "medium", 335 | "test", 336 | "commit", 337 | ]; 338 | let args = args.into_iter().map(String::from).collect::>(); 339 | let options = Options::new(args.into_iter(), &config); 340 | 341 | assert_eq!(options.n, 3); 342 | assert_eq!(options.t, 1.0); 343 | assert_eq!(options.f, 0.5); 344 | assert_eq!(options.print_once, true); 345 | assert_eq!(options.model.0, "gpt-4"); 346 | assert_eq!(options.enable_reasoning, true); 347 | assert_eq!(options.reasoning_effort, Some("medium".to_string())); 348 | assert_eq!(options.msg, "User Explanation/Instruction: 'test commit'"); 349 | } 350 | 351 | #[test] 352 | fn test_uncommon_reasoning_effort() { 353 | let config = Config::default(); 354 | let args = vec![ 355 | "turbocommit", 356 | "--enable-reasoning", 357 | "--reasoning-effort", 358 | "very-high", 359 | ]; 360 | let args = args.into_iter().map(String::from).collect::>(); 361 | let options = Options::new(args.into_iter(), &config); 362 | 363 | assert_eq!(options.enable_reasoning, true); 364 | assert_eq!(options.reasoning_effort, Some("very-high".to_string())); 365 | } 366 | 367 | #[test] 368 | fn test_debug_mode() { 369 | let config = Config::default(); 370 | let args = vec![ 371 | "turbocommit", 372 | "-d", 373 | "-r", 374 | "--model", 375 | "o3-mini", 376 | ]; 377 | let args = args.into_iter().map(String::from).collect::>(); 378 | let options = Options::new(args.into_iter(), &config); 379 | 380 | assert!(options.debug); 381 | assert!(options.print_once); // Debug mode forces print_once 382 | assert!(options.enable_reasoning); 383 | assert_eq!(options.reasoning_effort, Some("medium".to_string())); // Default effort 384 | assert_eq!(options.model.0, "o3-mini"); 385 | } 386 | 387 | #[test] 388 | fn test_debug_file_options() { 389 | let config = Config::default(); 390 | 391 | // Test debug file to a path 392 | let args = vec![ 393 | "turbocommit", 394 | "--debug-file", 395 | "debug.log", 396 | ]; 397 | let args = args.into_iter().map(String::from).collect::>(); 398 | let options = Options::new(args.into_iter(), &config); 399 | 400 | assert!(options.debug); // Debug mode should be enabled 401 | assert!(options.print_once); // Should force print_once 402 | assert_eq!(options.debug_file, Some("debug.log".to_string())); 403 | 404 | // Test debug file to stdout with "-" 405 | let args = vec![ 406 | "turbocommit", 407 | "--debug-file", 408 | "-", 409 | ]; 410 | let args = args.into_iter().map(String::from).collect::>(); 411 | let options = Options::new(args.into_iter(), &config); 412 | 413 | assert!(options.debug); 414 | assert!(options.print_once); 415 | assert_eq!(options.debug_file, Some("-".to_string())); 416 | 417 | // Test debug mode without file 418 | let args = vec![ 419 | "turbocommit", 420 | "-d", 421 | ]; 422 | let args = args.into_iter().map(String::from).collect::>(); 423 | let options = Options::new(args.into_iter(), &config); 424 | 425 | assert!(options.debug); 426 | assert!(options.print_once); 427 | assert_eq!(options.debug_file, None); 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::model; 2 | use colored::Colorize; 3 | use serde::{Deserialize, Serialize}; 4 | use std::process; 5 | use url::Url; 6 | 7 | #[derive(Debug)] 8 | pub struct ValidationError { 9 | field: String, 10 | message: String, 11 | } 12 | 13 | impl std::fmt::Display for ValidationError { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | write!(f, "{}: {}", self.field.red(), self.message) 16 | } 17 | } 18 | 19 | #[derive(Debug, Serialize, Deserialize, Clone)] 20 | pub struct Config { 21 | #[serde(default)] 22 | pub model: model::Model, 23 | #[serde(default)] 24 | pub api_endpoint: String, 25 | #[serde(default)] 26 | pub api_key_env_var: String, 27 | #[serde(default)] 28 | pub default_temperature: f64, 29 | #[serde(default)] 30 | pub default_frequency_penalty: f64, 31 | #[serde(default)] 32 | pub default_number_of_choices: i32, 33 | #[serde(default)] 34 | pub disable_print_as_stream: bool, 35 | #[serde(default)] 36 | pub disable_auto_update_check: bool, 37 | #[serde(default)] 38 | pub enable_reasoning: bool, 39 | #[serde(default)] 40 | pub system_msg: String, 41 | } 42 | 43 | impl Default for Config { 44 | fn default() -> Self { 45 | Self { 46 | model: model::Model("gpt-4o-mini".to_string()), 47 | api_endpoint: String::from("https://api.openai.com/v1/chat/completions"), 48 | api_key_env_var: String::from("OPENAI_API_KEY"), 49 | default_temperature: 1.05, 50 | default_frequency_penalty: 0.0, 51 | default_number_of_choices: 3, 52 | disable_print_as_stream: false, 53 | disable_auto_update_check: false, 54 | enable_reasoning: false, 55 | system_msg: String::from("You are a specialized AI that generates conventional commit messages based on git diffs. Your ONLY purpose is to produce properly formatted conventional commits that follow the exact specification at conventionalcommits.org. 56 | 57 | # INPUT AND RESPONSE FORMAT 58 | - You will receive a git diff of staged files 59 | - You MUST respond ONLY with a single, properly formatted conventional commit message 60 | - Your response must NOT be formatted as markdown or contain any other markup 61 | - Your response must consist of a single headline and optionally one body paragraph 62 | - Never include multiple commits or bullet points in your response 63 | 64 | # COMMIT PHILOSOPHY 65 | - Focus primarily on WHY the change was made, not WHAT was changed (the diff already shows the what) 66 | - A good commit explains the intent, motivation, and reasoning behind the change 67 | - Commits should provide context that isn't obvious from the code itself 68 | - Think at a higher abstraction level than the code - capture the purpose, not the implementation 69 | 70 | # CONVENTIONAL COMMIT STRUCTURE 71 | [optional scope][!]: 72 | 73 | [optional body] 74 | 75 | [optional footer(s)] 76 | 77 | # COMMIT RULES 78 | 1. Type: MUST be one of these nouns: 79 | - 'feat': introduces a new feature (correlates with MINOR in SemVer) 80 | - 'fix': patches a bug (correlates with PATCH in SemVer) 81 | - 'docs': documentation changes only 82 | - 'style': changes that don't affect code meaning (whitespace, formatting, etc.) 83 | - 'refac': code change that neither fixes a bug nor adds a feature 84 | - 'test': adding or correcting tests 85 | - 'build': changes affecting build system or external dependencies 86 | - 'ci': changes to CI configuration files and scripts 87 | - 'chore': other changes 88 | 89 | 2. Scope: OPTIONAL (but preferred), must be a noun in parentheses describing a section of the codebase 90 | Example: feat(parser): add ability to parse arrays 91 | 92 | 3. Breaking Change: Indicated by adding '!' before the colon or by adding a 'BREAKING CHANGE:' footer 93 | Example: feat(api)!: remove deprecated endpoints 94 | 95 | 4. Description: MUST immediately follow the colon and space after type/scope 96 | - Use imperative, present tense: 'add' not 'added' or 'adds' 97 | - Don't capitalize first letter 98 | - No period at the end 99 | - Focus on the intent rather than implementation details 100 | - Be specific yet concise about the change's purpose 101 | 102 | 5. Body: OPTIONAL but when present MUST: 103 | - Be separated from description by a blank line 104 | - Be a single concise paragraph explaining the motivation and context 105 | - Focus on WHY the change was needed, not what was changed 106 | - Explain the problem being solved, not how you solved it 107 | - Describe intent, rationale, and underlying reasons for the change 108 | - Highlight non-obvious implications or connections to other parts of the system 109 | - Never be a list of changes (the git diff already shows this) 110 | - Follow the KISS principle: brief but meaningful 111 | - Provide context without being verbose 112 | 113 | 6. Footer: OPTIONAL, must be separated from body by blank line 114 | Example: BREAKING CHANGE: configuration format has changed 115 | 116 | # EXAMPLES 117 | feat: add user authentication feature 118 | fix(database): resolve connection timeout issue 119 | refactor!: change API response format 120 | chore: update dependencies to latest versions 121 | 122 | # HIGH-LEVEL COMMIT EXAMPLES WITH BODY 123 | feat(auth): implement OAuth2 login flow 124 | 125 | Enable users to authenticate via third-party providers instead of managing credentials locally, improving security and reducing friction in the sign-up process. 126 | 127 | fix(performance): optimize database query pagination 128 | 129 | Resolves timeout issues during high traffic periods by implementing cursor-based pagination instead of offset-based, dramatically reducing query execution time. 130 | 131 | # ADDITIONAL INSTRUCTIONS 132 | - User may provide specific instructions or additional context - incorporate only if relevant 133 | - User may ask for revisions - be responsive to feedback 134 | - NEVER include explanations about your reasoning or analysis - ONLY output the commit message 135 | 136 | Remember: Always prioritize clarity and precision over verbosity."), 137 | } 138 | } 139 | } 140 | 141 | impl Config { 142 | pub fn load_from_path(path: &std::path::Path) -> anyhow::Result { 143 | //debug log the path we load from 144 | println!("Loading config from path: {}", path.display()); 145 | let config = match std::fs::read_to_string(path) { 146 | Ok(config_str) => { 147 | match serde_yaml::from_str::(&config_str) { 148 | Ok(config) => config, 149 | Err(err) => { 150 | return Err(anyhow::anyhow!("Configuration file parsing error: {}", err)); 151 | } 152 | } 153 | }, 154 | Err(err) => { 155 | match err.kind() { 156 | std::io::ErrorKind::NotFound => { 157 | println!("{}", format!("Config file not found at: {}", path.display()).red()); 158 | process::exit(1); 159 | } 160 | _ => { 161 | return Err(anyhow::anyhow!("Error reading configuration file: {}", err)); 162 | } 163 | } 164 | } 165 | }; 166 | 167 | // Validate the configuration 168 | if let Err(validation_errors) = config.validate() { 169 | let mut error_msg = String::from("Configuration validation errors:\n"); 170 | for error in validation_errors { 171 | error_msg.push_str(&format!(" {}\n", error)); 172 | } 173 | error_msg.push_str(&format!("\nConfiguration file location: {}", path.display())); 174 | 175 | // If system message is empty, show the default 176 | if config.system_msg.trim().is_empty() { 177 | error_msg.push_str("\n\nDefault system message:\n"); 178 | error_msg.push_str(&Self::default().system_msg); 179 | } 180 | 181 | return Err(anyhow::anyhow!(error_msg)); 182 | } 183 | 184 | // After validation passes, fill in empty system message with default 185 | let mut config = config; 186 | if config.system_msg.trim().is_empty() { 187 | config.system_msg = Self::default().system_msg; 188 | } 189 | 190 | Ok(config) 191 | } 192 | 193 | pub fn load() -> anyhow::Result { 194 | let path = home::home_dir().map_or_else( 195 | || { 196 | println!("{}", "Unable to find home directory.".red()); 197 | process::exit(1); 198 | }, 199 | |path| path.join(".turbocommit.yaml"), 200 | ); 201 | 202 | let config = match std::fs::read_to_string(&path) { 203 | Ok(config_str) => { 204 | match serde_yaml::from_str::(&config_str) { 205 | Ok(config) => config, 206 | Err(err) => { 207 | return Err(anyhow::anyhow!("Configuration file parsing error: {}", err)); 208 | } 209 | } 210 | }, 211 | Err(err) => { 212 | match err.kind() { 213 | std::io::ErrorKind::NotFound => { 214 | println!("{}", "No configuration file found, creating one with default values.".bright_black()); 215 | let default = Self::default(); 216 | if let Err(e) = default.save_if_changed() { 217 | println!("{}", format!("Warning: Failed to create default config file: {}", e).yellow()); 218 | } 219 | default 220 | } 221 | _ => { 222 | return Err(anyhow::anyhow!("Error reading configuration file: {}", err)); 223 | } 224 | } 225 | } 226 | }; 227 | 228 | // Validate the configuration 229 | if let Err(validation_errors) = config.validate() { 230 | let mut error_msg = String::from("Configuration validation errors:\n"); 231 | for error in validation_errors { 232 | error_msg.push_str(&format!(" {}\n", error)); 233 | } 234 | error_msg.push_str(&format!("\nConfiguration file location: {}", path.display())); 235 | 236 | // If system message is empty, show the default 237 | if config.system_msg.trim().is_empty() { 238 | error_msg.push_str("\n\nDefault system message:\n"); 239 | error_msg.push_str(&Self::default().system_msg); 240 | } 241 | 242 | return Err(anyhow::anyhow!(error_msg)); 243 | } 244 | 245 | // After validation passes, fill in empty system message with default 246 | let mut config = config; 247 | if config.system_msg.trim().is_empty() { 248 | config.system_msg = Self::default().system_msg; 249 | } 250 | 251 | Ok(config) 252 | } 253 | pub fn save_if_changed(&self) -> Result<(), std::io::Error> { 254 | let path = home::home_dir().map_or_else( 255 | || { 256 | println!("{}", "Unable to find home directory.".red()); 257 | process::exit(1); 258 | }, 259 | |path| path.join(".turbocommit.yaml"), 260 | ); 261 | let config = match serde_yaml::to_string(self) { 262 | Ok(config) => config, 263 | Err(err) => { 264 | println!("{}", format!("Unable to serialize config: {}", err).red()); 265 | return Err(std::io::Error::new( 266 | std::io::ErrorKind::Other, 267 | "Unable to serialize config", 268 | )); 269 | } 270 | }; 271 | 272 | if let Ok(existing_config) = std::fs::read_to_string(&path) { 273 | if existing_config == config { 274 | return Ok(()); 275 | } 276 | } 277 | 278 | std::fs::write(path, config) 279 | } 280 | pub fn path() -> std::path::PathBuf { 281 | home::home_dir().map_or_else( 282 | || { 283 | println!("{}", "Unable to find home directory.".red()); 284 | process::exit(1); 285 | }, 286 | |path| path.join(".turbocommit.yaml"), 287 | ) 288 | } 289 | 290 | fn validate(&self) -> Result<(), Vec> { 291 | let mut errors = Vec::new(); 292 | let default = Self::default(); 293 | 294 | // Validate model 295 | if self.model.0.is_empty() { 296 | errors.push(ValidationError { 297 | field: "model".to_string(), 298 | message: format!("Model cannot be empty (default: {})", default.model.0), 299 | }); 300 | } 301 | 302 | // Validate API endpoint 303 | if let Err(_) = Url::parse(&self.api_endpoint) { 304 | errors.push(ValidationError { 305 | field: "api_endpoint".to_string(), 306 | message: format!("Invalid URL format (default: {})", default.api_endpoint), 307 | }); 308 | } 309 | 310 | // Validate temperature 311 | if !(0.0..=2.0).contains(&self.default_temperature) { 312 | errors.push(ValidationError { 313 | field: "default_temperature".to_string(), 314 | message: format!("Temperature must be between 0.0 and 2.0 (default: {})", default.default_temperature), 315 | }); 316 | } 317 | 318 | // Validate frequency penalty 319 | if !(-2.0..=2.0).contains(&self.default_frequency_penalty) { 320 | errors.push(ValidationError { 321 | field: "default_frequency_penalty".to_string(), 322 | message: format!("Frequency penalty must be between -2.0 and 2.0 (default: {})", default.default_frequency_penalty), 323 | }); 324 | } 325 | 326 | // Validate number of choices 327 | if self.default_number_of_choices < 1 { 328 | errors.push(ValidationError { 329 | field: "default_number_of_choices".to_string(), 330 | message: format!("Number of choices must be at least 1 (default: {})", default.default_number_of_choices), 331 | }); 332 | } 333 | 334 | // Validate system message 335 | if self.system_msg.trim().is_empty() { 336 | errors.push(ValidationError { 337 | field: "system_msg".to_string(), 338 | message: "System message cannot be empty (see default message below)".to_string(), 339 | }); 340 | } 341 | 342 | if errors.is_empty() { 343 | Ok(()) 344 | } else { 345 | Err(errors) 346 | } 347 | } 348 | } 349 | 350 | #[cfg(test)] 351 | mod tests { 352 | use super::*; 353 | use std::fs; 354 | use tempfile::tempdir; 355 | 356 | fn create_test_config(content: &str) -> (std::path::PathBuf, tempfile::TempDir) { 357 | let dir = tempdir().unwrap(); 358 | let file_path = dir.path().join(".turbocommit.yaml"); 359 | fs::write(&file_path, content).unwrap(); 360 | (file_path, dir) 361 | } 362 | 363 | #[test] 364 | fn test_default_config_is_valid() { 365 | let config = Config::default(); 366 | assert!(config.validate().is_ok()); 367 | } 368 | 369 | #[test] 370 | fn test_validate_empty_model() { 371 | let mut config = Config::default(); 372 | config.model = model::Model(String::new()); 373 | let errors = config.validate().unwrap_err(); 374 | assert_eq!(errors.len(), 1); 375 | assert_eq!(errors[0].field, "model"); 376 | } 377 | 378 | #[test] 379 | fn test_validate_invalid_api_endpoint() { 380 | let mut config = Config::default(); 381 | config.api_endpoint = "not a url".to_string(); 382 | let errors = config.validate().unwrap_err(); 383 | assert_eq!(errors.len(), 1); 384 | assert_eq!(errors[0].field, "api_endpoint"); 385 | } 386 | 387 | #[test] 388 | fn test_validate_invalid_temperature() { 389 | let mut config = Config::default(); 390 | config.default_temperature = 2.5; 391 | let errors = config.validate().unwrap_err(); 392 | assert_eq!(errors.len(), 1); 393 | assert_eq!(errors[0].field, "default_temperature"); 394 | } 395 | 396 | #[test] 397 | fn test_validate_invalid_frequency_penalty() { 398 | let mut config = Config::default(); 399 | config.default_frequency_penalty = -3.0; 400 | let errors = config.validate().unwrap_err(); 401 | assert_eq!(errors.len(), 1); 402 | assert_eq!(errors[0].field, "default_frequency_penalty"); 403 | } 404 | 405 | #[test] 406 | fn test_validate_invalid_number_of_choices() { 407 | let mut config = Config::default(); 408 | config.default_number_of_choices = 0; 409 | let errors = config.validate().unwrap_err(); 410 | assert_eq!(errors.len(), 1); 411 | assert_eq!(errors[0].field, "default_number_of_choices"); 412 | } 413 | 414 | #[test] 415 | fn test_validate_empty_system_msg() { 416 | let mut config = Config::default(); 417 | config.system_msg = "".to_string(); 418 | let errors = config.validate().unwrap_err(); 419 | assert_eq!(errors.len(), 1); 420 | assert_eq!(errors[0].field, "system_msg"); 421 | } 422 | 423 | #[test] 424 | fn test_validate_multiple_errors() { 425 | let mut config = Config::default(); 426 | config.model = model::Model(String::new()); 427 | config.default_temperature = 3.0; 428 | config.system_msg = "".to_string(); 429 | let errors = config.validate().unwrap_err(); 430 | assert_eq!(errors.len(), 3); 431 | } 432 | 433 | #[test] 434 | fn test_load_valid_config() { 435 | let config_content = r#" 436 | model: gpt-4 437 | api_endpoint: https://api.openai.com/v1/chat/completions 438 | default_temperature: 1.0 439 | default_frequency_penalty: 0.0 440 | default_number_of_choices: 3 441 | disable_print_as_stream: false 442 | disable_auto_update_check: true 443 | system_msg: "Test message" 444 | "#; 445 | let (_file_path, _dir) = create_test_config(config_content); 446 | 447 | // Set the home directory to our temp directory for this test 448 | std::env::set_var("HOME", _dir.path()); 449 | 450 | let config = Config::load(); 451 | assert!(config.is_ok()); 452 | let config = config.unwrap(); 453 | assert!(config.disable_auto_update_check); 454 | } 455 | 456 | #[test] 457 | fn test_load_invalid_yaml() { 458 | let config_content = "invalid: yaml: content: ["; 459 | let (_file_path, _dir) = create_test_config(config_content); 460 | 461 | // Set the home directory to our temp directory for this test 462 | std::env::set_var("HOME", _dir.path()); 463 | 464 | let config = Config::load(); 465 | assert!(config.is_err(), "Expected config loading to fail with invalid YAML"); 466 | } 467 | 468 | #[test] 469 | fn test_load_missing_file_creates_default() { 470 | let _dir = tempdir().unwrap(); 471 | std::env::set_var("HOME", _dir.path()); 472 | 473 | // First load should create the file 474 | let config = Config::load(); 475 | assert!(config.is_ok()); 476 | 477 | // Verify the file was created 478 | let config_path = _dir.path().join(".turbocommit.yaml"); 479 | assert!(config_path.exists()); 480 | 481 | // Verify content matches default 482 | let content = std::fs::read_to_string(config_path).unwrap(); 483 | let loaded_config: Config = serde_yaml::from_str(&content).unwrap(); 484 | assert_eq!(loaded_config.model.0, Config::default().model.0); 485 | assert_eq!(loaded_config.api_endpoint, Config::default().api_endpoint); 486 | } 487 | 488 | #[test] 489 | fn test_validation_error_includes_defaults() { 490 | let mut config = Config::default(); 491 | config.model = model::Model(String::new()); 492 | config.default_temperature = 3.0; 493 | 494 | let errors = config.validate().unwrap_err(); 495 | let default = Config::default(); 496 | 497 | // Find the model error 498 | let model_error = errors.iter().find(|e| e.field == "model").unwrap(); 499 | assert!(model_error.message.contains(&default.model.0)); 500 | 501 | // Find the temperature error 502 | let temp_error = errors.iter().find(|e| e.field == "default_temperature").unwrap(); 503 | assert!(temp_error.message.contains(&default.default_temperature.to_string())); 504 | } 505 | 506 | #[test] 507 | fn test_empty_system_msg_shows_default() { 508 | let config_content = r#" 509 | model: gpt-4 510 | api_endpoint: https://api.openai.com/v1/chat/completions 511 | default_temperature: 1.0 512 | default_frequency_penalty: 0.0 513 | default_number_of_choices: 3 514 | disable_print_as_stream: false 515 | disable_auto_update_check: false 516 | system_msg: "" 517 | "#; 518 | let (_file_path, _dir) = create_test_config(config_content); 519 | std::env::set_var("HOME", _dir.path()); 520 | 521 | let error = Config::load().unwrap_err(); 522 | let error_msg = error.to_string(); 523 | 524 | // Error should contain the default system message 525 | assert!(error_msg.contains("Default system message:")); 526 | assert!(error_msg.contains(&Config::default().system_msg)); 527 | } 528 | 529 | #[test] 530 | fn test_save_if_changed() { 531 | let _dir = tempdir().unwrap(); 532 | // Set the home directory to our temp directory for this test 533 | std::env::set_var("HOME", _dir.path()); 534 | 535 | // Create a config with some changes 536 | let mut config = Config::default(); 537 | config.model = model::Model("gpt-4".to_string()); 538 | 539 | // First save should succeed 540 | assert!(config.save_if_changed().is_ok()); 541 | 542 | // Second save with no changes should still be ok 543 | assert!(config.save_if_changed().is_ok()); 544 | 545 | // Verify the file was created with correct content 546 | let config_path = _dir.path().join(".turbocommit.yaml"); 547 | assert!(config_path.exists()); 548 | let content = std::fs::read_to_string(config_path).unwrap(); 549 | let loaded_config: Config = serde_yaml::from_str(&content).unwrap(); 550 | assert_eq!(loaded_config.model.0, "gpt-4"); 551 | } 552 | 553 | #[test] 554 | fn test_default_auto_update_check() { 555 | let config = Config::default(); 556 | assert!(!config.disable_auto_update_check, "Auto update check should be enabled by default"); 557 | } 558 | 559 | #[test] 560 | fn test_load_from_path_valid_config() { 561 | let config_content = r#" 562 | model: gpt-4 563 | api_endpoint: https://api.openai.com/v1/chat/completions 564 | default_temperature: 1.0 565 | default_frequency_penalty: 0.0 566 | default_number_of_choices: 3 567 | disable_print_as_stream: false 568 | disable_auto_update_check: true 569 | system_msg: "Test message" 570 | "#; 571 | let (file_path, _dir) = create_test_config(config_content); 572 | 573 | let config = Config::load_from_path(&file_path); 574 | assert!(config.is_ok()); 575 | let config = config.unwrap(); 576 | assert_eq!(config.model.0, "gpt-4"); 577 | assert_eq!(config.default_temperature, 1.0); 578 | assert!(config.disable_auto_update_check); 579 | assert_eq!(config.system_msg, "Test message"); 580 | } 581 | 582 | #[test] 583 | fn test_load_from_path_invalid_yaml() { 584 | let config_content = "invalid: yaml: content: ["; 585 | let (file_path, _dir) = create_test_config(config_content); 586 | 587 | let config = Config::load_from_path(&file_path); 588 | assert!(config.is_err()); 589 | assert!(config.unwrap_err().to_string().contains("Configuration file parsing error")); 590 | } 591 | 592 | #[test] 593 | fn test_load_from_path_nonexistent_file() { 594 | let dir = tempdir().unwrap(); 595 | let nonexistent_path = dir.path().join("nonexistent.yaml"); 596 | 597 | let config = Config::load_from_path(&nonexistent_path); 598 | assert!(config.is_err()); 599 | } 600 | 601 | #[test] 602 | fn test_load_from_path_invalid_config() { 603 | let config_content = r#" 604 | model: "" # Empty model is invalid 605 | api_endpoint: not-a-url 606 | default_temperature: 3.0 # Out of range 607 | default_frequency_penalty: 0.0 608 | default_number_of_choices: 3 609 | disable_print_as_stream: false 610 | disable_auto_update_check: false 611 | system_msg: "Test message" 612 | "#; 613 | let (file_path, _dir) = create_test_config(config_content); 614 | 615 | let config = Config::load_from_path(&file_path); 616 | assert!(config.is_err()); 617 | let err = config.unwrap_err().to_string(); 618 | assert!(err.contains("model")); // Should mention empty model error 619 | assert!(err.contains("api_endpoint")); // Should mention invalid URL error 620 | assert!(err.contains("temperature")); // Should mention temperature range error 621 | } 622 | 623 | #[test] 624 | fn test_load_from_path_empty_system_msg() { 625 | let config_content = r#" 626 | model: "gpt-4" 627 | api_endpoint: "https://api.openai.com/v1/chat/completions" 628 | default_temperature: 1.0 629 | default_frequency_penalty: 0.0 630 | default_number_of_choices: 3 631 | disable_print_as_stream: false 632 | disable_auto_update_check: false 633 | system_msg: "" 634 | "#; 635 | let (file_path, _dir) = create_test_config(config_content); 636 | 637 | let config = Config::load_from_path(&file_path); 638 | assert!(config.is_err()); 639 | let err = config.unwrap_err().to_string(); 640 | assert!(err.contains("system_msg")); // Should mention system message error 641 | assert!(err.contains("Default system message:")); // Should show default message 642 | } 643 | } 644 | -------------------------------------------------------------------------------- /src/debug_log.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, OpenOptions}; 2 | use std::io::{self, Write}; 3 | use std::path::Path; 4 | use std::time::{SystemTime, UNIX_EPOCH}; 5 | 6 | pub struct DebugLogger { 7 | file: Option, 8 | use_stdout: bool, 9 | } 10 | 11 | impl DebugLogger { 12 | pub fn new(debug_file: Option) -> Self { 13 | let (file, use_stdout) = match debug_file { 14 | Some(path) if path == "-" => (None, true), 15 | Some(path) => ( 16 | OpenOptions::new() 17 | .create(true) 18 | .write(true) 19 | .truncate(true) 20 | .open(Path::new(&path)) 21 | .ok(), 22 | false 23 | ), 24 | None => (None, false), 25 | }; 26 | Self { file, use_stdout } 27 | } 28 | 29 | pub fn log(&mut self, category: &str, content: &str) { 30 | let timestamp = SystemTime::now() 31 | .duration_since(UNIX_EPOCH) 32 | .unwrap_or_default() 33 | .as_millis(); 34 | 35 | let log_line = format!("{};{};{}\n", timestamp, category, content); 36 | 37 | if self.use_stdout { 38 | let _ = io::stdout().write_all(log_line.as_bytes()); 39 | let _ = io::stdout().flush(); 40 | } else if let Some(file) = &mut self.file { 41 | let _ = file.write_all(log_line.as_bytes()); 42 | let _ = file.flush(); 43 | } 44 | } 45 | 46 | pub fn log_request(&mut self, request_json: &str) { 47 | self.log("request", request_json); 48 | } 49 | 50 | pub fn log_response(&mut self, response_json: &str) { 51 | self.log("response", response_json); 52 | } 53 | 54 | pub fn log_error(&mut self, error: &str) { 55 | self.log("error", error); 56 | } 57 | 58 | pub fn log_info(&mut self, info: &str) { 59 | self.log("info", info); 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use super::*; 66 | use std::io::Read; 67 | use tempfile::NamedTempFile; 68 | 69 | #[test] 70 | fn test_debug_logger_file() { 71 | let temp_file = NamedTempFile::new().unwrap(); 72 | let path = temp_file.path().to_str().unwrap().to_string(); 73 | 74 | let mut logger = DebugLogger::new(Some(path.clone())); 75 | logger.log_info("test message"); 76 | 77 | let mut content = String::new(); 78 | File::open(path).unwrap().read_to_string(&mut content).unwrap(); 79 | 80 | assert!(content.contains("test message")); 81 | assert!(content.contains(";info;")); 82 | } 83 | 84 | #[test] 85 | fn test_debug_logger_none() { 86 | let mut logger = DebugLogger::new(None); 87 | logger.log_info("test message"); // Should not panic 88 | } 89 | } -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | use git2::{Repository, Tree}; 2 | use std::process::Command; 3 | 4 | pub fn get_repo() -> Result { 5 | Repository::discover(".") 6 | } 7 | 8 | pub fn staged_files(repo: &Repository) -> Result, git2::Error> { 9 | let idx = repo.index()?; 10 | let mut head: Option = None; 11 | if let Ok(h) = repo.head() { 12 | head = Some(h.peel_to_tree()?); 13 | } 14 | let diff = repo.diff_tree_to_index(head.as_ref(), Some(&idx), None)?; 15 | Ok(diff 16 | .deltas() 17 | .map(|d| { 18 | let path = d.new_file().path(); 19 | path.map_or_else(String::new, |path| path.to_str().unwrap_or("").to_string()) 20 | }) 21 | .collect()) 22 | } 23 | 24 | pub fn diff(repo: &Repository, files: &[String]) -> Result { 25 | let mut ret = String::new(); 26 | 27 | let idx = repo.index()?; 28 | let mut head: Option = None; 29 | if let Ok(h) = repo.head() { 30 | head = Some(h.peel_to_tree()?); 31 | } 32 | let diff = repo.diff_tree_to_index(head.as_ref(), Some(&idx), None)?; 33 | diff.print(git2::DiffFormat::Patch, |delta, _, line| { 34 | if let Some(path) = delta.new_file().path() { 35 | if files.contains(&path.to_str().unwrap_or("").to_string()) { 36 | ret.push(line.origin()); 37 | ret.push_str(std::str::from_utf8(line.content()).unwrap_or("")); 38 | } 39 | } 40 | true 41 | })?; 42 | Ok(ret) 43 | } 44 | 45 | // idk how this is really supposed to work 46 | // pub fn commit(repo: &Repository, files: &[String], msg: &str) -> Result<(), git2::Error> { 47 | // let mut index = repo.index()?; 48 | // // let all_files = tracked_files(repo)?; 49 | // // for file in all_files { 50 | // // if !files.contains(&file) { 51 | // // index.remove_path(std::path::Path::new(&file))?; 52 | // // } 53 | // // } 54 | // // index.write()?; 55 | // let oid = index.write_tree()?; 56 | // let parent_commit = repo.head()?.peel_to_commit()?; 57 | // let tree = repo.find_tree(oid)?; 58 | // let sig = repo.signature()?; 59 | // repo.commit(Some("HEAD"), &sig, &sig, msg, &tree, &[&parent_commit])?; 60 | // Ok(()) 61 | // } 62 | 63 | pub fn get_last_commit_diff(repo: &Repository) -> Result { 64 | let mut ret = String::new(); 65 | let head = repo.head()?; 66 | let head_commit = head.peel_to_commit()?; 67 | 68 | if let Some(parent) = head_commit.parent(0).ok() { 69 | let diff = repo.diff_tree_to_tree( 70 | Some(&parent.tree()?), 71 | Some(&head_commit.tree()?), 72 | None, 73 | )?; 74 | 75 | diff.print(git2::DiffFormat::Patch, |_, _, line| { 76 | ret.push(line.origin()); 77 | ret.push_str(std::str::from_utf8(line.content()).unwrap_or("")); 78 | true 79 | })?; 80 | } 81 | Ok(ret) 82 | } 83 | 84 | pub fn has_staged_changes(repo: &Repository) -> Result { 85 | let idx = repo.index()?; 86 | let mut head: Option = None; 87 | if let Ok(h) = repo.head() { 88 | head = Some(h.peel_to_tree()?); 89 | } 90 | let diff = repo.diff_tree_to_index(head.as_ref(), Some(&idx), None)?; 91 | Ok(diff.deltas().len() > 0) 92 | } 93 | 94 | pub fn commit(message: String, amend: bool) -> anyhow::Result<()> { 95 | let mut cmd = Command::new("git"); 96 | cmd.arg("commit"); 97 | if amend { 98 | cmd.arg("--amend"); 99 | // When amending, we don't need staged changes 100 | cmd.arg("--no-edit"); // This prevents git from opening the editor 101 | } 102 | cmd.arg("-m").arg(message); 103 | 104 | let output = cmd.output()?; 105 | if !output.status.success() { 106 | return Err(anyhow::anyhow!( 107 | "{}", 108 | String::from_utf8_lossy(&output.stderr) 109 | )); 110 | } 111 | Ok(()) 112 | } 113 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use actor::Actor; 2 | use colored::Colorize; 3 | use config::Config; 4 | 5 | use openai::Message; 6 | 7 | use std::{env, process, time::Duration}; 8 | 9 | mod actor; 10 | mod animation; 11 | mod cli; 12 | mod config; 13 | mod git; 14 | mod model; 15 | mod openai; 16 | mod util; 17 | mod debug_log; 18 | 19 | #[tokio::main] 20 | async fn main() -> anyhow::Result<()> { 21 | // First get the default config just to parse CLI options 22 | let default_config = Config::load()?; 23 | let mut options = cli::Options::new(env::args(), &default_config); 24 | 25 | // If check_version_only is set, just check version and exit 26 | if options.check_version_only { 27 | util::check_version().await; 28 | return Ok(()); 29 | } 30 | 31 | // Load the actual config we'll use (either custom or default) 32 | let config = if let Some(config_path) = options.config_file.as_ref() { 33 | Config::load_from_path(std::path::Path::new(config_path))? 34 | } else { 35 | default_config 36 | }; 37 | 38 | // Update options with the final config values 39 | options = cli::Options::new(env::args(), &config); 40 | 41 | let api_key = match &options.api_key { 42 | Some(ref key) => key.clone(), 43 | None => { 44 | let env_var = &config.api_key_env_var; 45 | if env_var.trim().is_empty() { 46 | // If env_var is empty, no API key is needed 47 | String::new() 48 | } else { 49 | // Only check environment variable if env_var is not empty 50 | match env::var(env_var) { 51 | Ok(key) => key, 52 | Err(_) => { 53 | println!("{}", format!("No API key found. Either:").red()); 54 | println!(" 1. Set the {} environment variable", env_var.purple()); 55 | println!(" 2. Use the {} option", "--api-key ".purple()); 56 | println!("\n{}", "For API key safety best practices, see: https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety".bright_black()); 57 | process::exit(1); 58 | } 59 | } 60 | } 61 | } 62 | }; 63 | 64 | let mut actor = Actor::new( 65 | options.clone(), 66 | api_key, 67 | options.api_endpoint.clone(), 68 | ); 69 | 70 | let repo = git::get_repo()?; 71 | 72 | let system_len = openai::count_token(options.system_msg.as_ref().unwrap_or(&config.system_msg)).unwrap_or(0); 73 | let extra_len = openai::count_token(&options.msg).unwrap_or(0); 74 | 75 | // Add system message first 76 | actor.add_message(Message::system(options.system_msg.unwrap_or(config.system_msg.clone()))); 77 | 78 | // Handle amend mode 79 | if options.amend { 80 | // When amending, we don't want any staged files 81 | if git::has_staged_changes(&repo)? { 82 | println!("{}", "Error: You have staged changes.".red()); 83 | println!("{}", "When using --amend, you should not have any staged changes.".bright_black()); 84 | println!("{}", "The --amend option only changes the commit message of the last commit.".bright_black()); 85 | println!("{}", "If you want to include new changes, either:".bright_black()); 86 | println!("{}", "1. Commit them first normally, then amend that commit".bright_black()); 87 | println!("{}", "2. Or use git commit --amend manually to include them".bright_black()); 88 | process::exit(1); 89 | } 90 | 91 | // Get the diff from the last commit 92 | let diff = git::get_last_commit_diff(&repo)?; 93 | if diff.is_empty() { 94 | println!("{}", "Error: Could not get changes from the last commit.".red()); 95 | println!("{}", "Make sure you have at least one commit in your repository.".bright_black()); 96 | process::exit(1); 97 | } 98 | actor.add_message(Message::user(diff)); 99 | actor.used_tokens = system_len + extra_len; 100 | } else { 101 | // Normal commit mode - get diff from staged changes 102 | let (diff, diff_tokens) = util::decide_diff(&repo, system_len + extra_len, options.model.context_size(), options.always_select_files)?; 103 | actor.add_message(Message::user(diff)); 104 | actor.used_tokens = system_len + extra_len + diff_tokens; 105 | } 106 | 107 | // Add any extra message from command line 108 | if !options.msg.is_empty() { 109 | actor.add_message(Message::user(options.msg)); 110 | } 111 | 112 | if options.auto_commmit { 113 | let _ = actor.auto_commit().await?; 114 | } else { 115 | let _ = actor.start().await; 116 | } 117 | 118 | // Only check for updates if not disabled in config or CLI 119 | if !options.disable_auto_update_check { 120 | util::check_version().await; 121 | } 122 | 123 | if util::check_config_age(Duration::from_secs(60 * 60 * 24 * 30 * 6)) { 124 | if !util::is_system_prompt_same_as_default(&config.system_msg) { 125 | println!( 126 | "\n{}\n{}\n{}", 127 | "Your system prompt seems to be old.".yellow(), 128 | "There is a new default recommended system prompt. To apply it, delete the `system_msg` field in your config file.".bright_black(), 129 | "To get rid of this message, simply save your config file to change the last modified date.".bright_black() 130 | ); 131 | } 132 | } 133 | 134 | Ok(()) 135 | } 136 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::str::FromStr; 3 | 4 | #[derive(Debug, Clone, Default, PartialEq)] 5 | pub struct Model(pub String); 6 | 7 | impl FromStr for Model { 8 | type Err = String; 9 | 10 | fn from_str(s: &str) -> Result { 11 | Ok(Self(s.to_string())) 12 | } 13 | } 14 | 15 | impl ToString for Model { 16 | fn to_string(&self) -> String { 17 | self.0.clone() 18 | } 19 | } 20 | 21 | impl Serialize for Model { 22 | fn serialize(&self, serializer: S) -> Result 23 | where 24 | S: serde::Serializer, 25 | { 26 | serializer.serialize_str(&self.0) 27 | } 28 | } 29 | 30 | impl<'de> Deserialize<'de> for Model { 31 | fn deserialize(deserializer: D) -> Result 32 | where 33 | D: serde::Deserializer<'de>, 34 | { 35 | let s = String::deserialize(deserializer)?; 36 | Ok(Self(s)) 37 | } 38 | } 39 | 40 | impl Model { 41 | pub fn context_size(&self) -> usize { 42 | match self.0.as_str() { 43 | "gpt-4" => 8192, 44 | "gpt-4-turbo" => 128000, 45 | "gpt-4o" => 128000, 46 | "gpt-4o-mini" => 128000, 47 | "o1" => 200000, 48 | "o1-mini" => 128000, 49 | "o1-preview" => 128000, 50 | "o3-mini" => 200000, 51 | _ => usize::MAX, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/openai.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use colored::Colorize; 4 | use crossterm::cursor::{MoveToColumn, MoveToPreviousLine}; 5 | use crossterm::style::Print; 6 | use crossterm::terminal::{Clear, ClearType}; 7 | use crossterm::{execute, terminal}; 8 | use futures::StreamExt; 9 | use reqwest_eventsource::{Event, EventSource}; 10 | use serde::{Deserialize, Serialize}; 11 | use std::{fmt, process}; 12 | 13 | use crate::animation; 14 | use crate::util::count_lines; 15 | 16 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 17 | #[serde(rename_all = "lowercase")] 18 | pub enum Role { 19 | System, 20 | User, 21 | Assistant, 22 | Developer, 23 | } 24 | 25 | impl fmt::Display for Role { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | match self { 28 | Role::System => write!(f, "system"), 29 | Role::User => write!(f, "user"), 30 | Role::Assistant => write!(f, "assistant"), 31 | Role::Developer => write!(f, "developer"), 32 | } 33 | } 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] 37 | pub struct Message { 38 | pub role: Role, 39 | pub content: String, 40 | } 41 | 42 | impl Message { 43 | pub const fn system(content: String) -> Self { 44 | Self { 45 | role: Role::System, 46 | content, 47 | } 48 | } 49 | pub const fn developer(content: String) -> Self { 50 | Self { 51 | role: Role::Developer, 52 | content, 53 | } 54 | } 55 | pub const fn user(content: String) -> Self { 56 | Self { 57 | role: Role::User, 58 | content, 59 | } 60 | } 61 | pub const fn assistant(content: String) -> Self { 62 | Self { 63 | role: Role::Assistant, 64 | content, 65 | } 66 | } 67 | } 68 | 69 | #[derive(Serialize, Deserialize, Debug)] 70 | #[serde(rename_all = "camelCase")] 71 | pub struct ErrorRoot { 72 | pub error: Error, 73 | } 74 | 75 | #[derive(Serialize, Deserialize, Debug)] 76 | #[serde(rename_all = "camelCase")] 77 | pub struct Error { 78 | pub message: String, 79 | #[serde(rename = "type")] 80 | pub type_field: String, 81 | pub param: Option, 82 | pub code: Option, 83 | } 84 | 85 | impl fmt::Display for Error { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | write!( 88 | f, 89 | "{} ({:?}): {:?}", 90 | self.type_field.red(), 91 | self.code, 92 | self.message 93 | ) 94 | } 95 | } 96 | 97 | #[derive(Debug, Serialize)] 98 | #[serde(untagged)] 99 | pub enum Request { 100 | Standard(StandardRequest), 101 | OSeries(OSeriesRequest), 102 | } 103 | 104 | #[derive(Debug, Serialize)] 105 | pub struct StandardRequest { 106 | pub model: String, 107 | pub messages: Vec, 108 | pub n: i32, 109 | pub temperature: f64, 110 | pub frequency_penalty: f64, 111 | #[serde(skip_serializing_if = "Option::is_none")] 112 | pub reasoning_effort: Option, 113 | stream: bool, 114 | } 115 | 116 | #[derive(Debug, Serialize)] 117 | pub struct OSeriesRequest { 118 | pub model: String, 119 | pub messages: Vec, 120 | pub n: i32, 121 | #[serde(skip_serializing_if = "Option::is_none")] 122 | pub reasoning_effort: Option, 123 | stream: bool, 124 | } 125 | 126 | impl Request { 127 | pub fn new( 128 | model: String, 129 | messages: Vec, 130 | n: i32, 131 | temperature: f64, 132 | frequency_penalty: f64, 133 | ) -> Self { 134 | if model.starts_with("o1") || model.starts_with("o3") { 135 | Self::OSeries(OSeriesRequest { 136 | model, 137 | messages, 138 | n, 139 | reasoning_effort: None, 140 | stream: true, 141 | }) 142 | } else { 143 | Self::Standard(StandardRequest { 144 | model, 145 | messages, 146 | n, 147 | temperature, 148 | frequency_penalty, 149 | reasoning_effort: None, 150 | stream: true, 151 | }) 152 | } 153 | } 154 | 155 | pub fn with_reasoning_effort(self, effort: Option) -> Self { 156 | match self { 157 | Self::Standard(mut req) => { 158 | req.reasoning_effort = effort; 159 | Self::Standard(req) 160 | } 161 | Self::OSeries(mut req) => { 162 | req.reasoning_effort = effort; 163 | Self::OSeries(req) 164 | } 165 | } 166 | } 167 | 168 | fn model(&self) -> &str { 169 | match self { 170 | Self::Standard(req) => &req.model, 171 | Self::OSeries(req) => &req.model, 172 | } 173 | } 174 | 175 | fn n(&self) -> i32 { 176 | match self { 177 | Self::Standard(req) => req.n, 178 | Self::OSeries(req) => req.n, 179 | } 180 | } 181 | 182 | pub async fn execute( 183 | &self, 184 | api_key: String, 185 | no_animations: bool, 186 | prompt_tokens: usize, 187 | api_endpoint: String, 188 | debug: bool, 189 | debug_logger: &mut crate::debug_log::DebugLogger, 190 | ) -> anyhow::Result> { 191 | let mut choices = vec![String::new(); self.n() as usize]; 192 | let json = serde_json::to_string(self)?; 193 | 194 | // First make a regular request to check if it will be accepted 195 | let client = reqwest::Client::new(); 196 | let response = client 197 | .post(&api_endpoint) 198 | .header("Content-Type", "application/json") 199 | .bearer_auth(&api_key) 200 | .body(json.clone()) 201 | .send() 202 | .await?; 203 | 204 | if !response.status().is_success() { 205 | let status = response.status(); 206 | let error_body = response.text().await?; 207 | 208 | // Try to parse as OpenAI error 209 | let error_details = match serde_json::from_str::(&error_body) { 210 | Ok(error_root) => format!( 211 | "OpenAI Error:\n Type: {}\n Message: {}\n Code: {:?}\n Parameter: {:?}\n\nFull Response:\n{}", 212 | error_root.error.type_field, 213 | error_root.error.message, 214 | error_root.error.code, 215 | error_root.error.param, 216 | error_body 217 | ), 218 | Err(_) => format!("Raw Response:\n{}", error_body), 219 | }; 220 | 221 | let error_msg = format!( 222 | "API request failed (HTTP {}):\nEndpoint: {}\n\n{}", 223 | status, api_endpoint, error_details 224 | ); 225 | debug_logger.log_error(&error_msg); 226 | println!("{}", "API Error:".red().bold()); 227 | println!("{}", error_msg); 228 | process::exit(1); 229 | } 230 | 231 | let loading_ai_animation = animation::start( 232 | String::from("Asking AI..."), 233 | no_animations || debug, 234 | std::io::stdout(), 235 | ) 236 | .await; 237 | 238 | let request_builder = client 239 | .post(api_endpoint.clone()) 240 | .header("Content-Type", "application/json") 241 | .bearer_auth(api_key) 242 | .body(json); 243 | 244 | let term_width = terminal::size()?.0 as usize; 245 | let mut stdout = std::io::stdout(); 246 | let mut es = EventSource::new(request_builder)?; 247 | let mut lines_to_move_up = 0; 248 | let mut response_tokens = 0; 249 | 250 | // Only show minimal info in regular debug mode 251 | if debug && !no_animations { 252 | println!("\n{}", "Request Info:".blue().bold()); 253 | println!(" Model: {}", self.model().purple()); 254 | println!(" API: {}", api_endpoint.purple()); 255 | println!(" Input tokens: {}", prompt_tokens.to_string().purple()); 256 | } 257 | 258 | while let Some(event) = es.next().await { 259 | if no_animations || debug { 260 | match event { 261 | Ok(Event::Message(message)) => { 262 | if message.data == "[DONE]" { 263 | break; 264 | } 265 | let resp = serde_json::from_str::(&message.data) 266 | .map_or_else(|_| Response::default(), |r| r); 267 | response_tokens += 1; 268 | for choice in resp.choices { 269 | if let Some(content) = choice.delta.content { 270 | choices[choice.index as usize].push_str(&content); 271 | } 272 | } 273 | } 274 | Err(e) => { 275 | // The error string from reqwest_eventsource includes the full response 276 | let error_str = e.to_string(); 277 | let error_details = if let Some(error_json) = error_str.strip_prefix("Error response: ") { 278 | // Try to parse as OpenAI error format 279 | match serde_json::from_str::(error_json) { 280 | Ok(error_root) => format!( 281 | "OpenAI Error:\n Type: {}\n Message: {}\n Code: {:?}\n\nFull Response:\n{}", 282 | error_root.error.type_field, 283 | error_root.error.message, 284 | error_root.error.code, 285 | error_json 286 | ), 287 | Err(_) => format!("Raw Response:\n{}", error_json) 288 | } 289 | } else { 290 | format!("Error: {}", error_str) 291 | }; 292 | 293 | let error_msg = format!( 294 | "API request failed:\nEndpoint: {}\n\n{}", 295 | api_endpoint, error_details 296 | ); 297 | debug_logger.log_error(&error_msg); 298 | println!("{}", "API Error:".red().bold()); 299 | println!("{}", error_msg); 300 | process::exit(1); 301 | } 302 | _ => {} 303 | } 304 | } else { 305 | if !loading_ai_animation.is_finished() { 306 | loading_ai_animation.abort(); 307 | execute!( 308 | std::io::stdout(), 309 | Clear(ClearType::CurrentLine), 310 | MoveToColumn(0), 311 | )?; 312 | print!("\n\n") 313 | } 314 | match event { 315 | Ok(Event::Message(message)) => { 316 | if message.data == "[DONE]" { 317 | break; 318 | } 319 | execute!(stdout, MoveToPreviousLine(lines_to_move_up),)?; 320 | lines_to_move_up = 0; 321 | execute!(stdout, Clear(ClearType::FromCursorDown),)?; 322 | let resp = serde_json::from_str::(&message.data) 323 | .map_or_else(|_| Response::default(), |r| r); 324 | response_tokens += 1; 325 | for choice in resp.choices { 326 | if let Some(content) = choice.delta.content { 327 | choices[choice.index as usize].push_str(&content); 328 | } 329 | } 330 | for (i, choice) in choices.iter().enumerate() { 331 | let outp = format!( 332 | "{}{}\n{}\n", 333 | if i == 0 { 334 | format!( 335 | "Tokens used: {} input, {} output\n", 336 | crate::util::format_token_count(prompt_tokens).purple(), 337 | crate::util::format_token_count(response_tokens).purple(), 338 | ) 339 | .bright_black() 340 | } else { 341 | "".bright_black() 342 | }, 343 | format!("[{}]====================", format!("{i}").purple()) 344 | .bright_black(), 345 | choice, 346 | ); 347 | print!("{outp}"); 348 | lines_to_move_up += count_lines(&outp, term_width) - 1; 349 | } 350 | } 351 | Err(e) => { 352 | println!("{e}"); 353 | process::exit(1); 354 | } 355 | _ => {} 356 | } 357 | } 358 | } 359 | 360 | if no_animations || debug { 361 | println!( 362 | "Tokens: {} in, {} out (total: {})", 363 | crate::util::format_token_count(prompt_tokens).purple(), 364 | crate::util::format_token_count(response_tokens).purple(), 365 | crate::util::format_token_count(prompt_tokens + response_tokens).purple(), 366 | ); 367 | for (i, choice) in choices.iter().enumerate() { 368 | println!( 369 | "[{}] {}\n{}\n", 370 | format!("{i}").purple(), 371 | "=".repeat(77 - i.to_string().len()), 372 | choice 373 | ); 374 | } 375 | } else { 376 | // For regular mode (non-debug), show the final messages nicely formatted 377 | // Only show the messages header if we have multiple choices 378 | if choices.len() > 1 { 379 | println!("\n{}", "Generated Commit Messages:".blue().bold()); 380 | } 381 | // Don't show the messages here if it's a reasoning response (has tag) 382 | // as it will be handled by process_response 383 | if !choices[0].contains("") { 384 | for (i, choice) in choices.iter().enumerate() { 385 | println!( 386 | "[{}] {}\n{}", 387 | format!("{i}").purple(), 388 | "=".repeat(77 - i.to_string().len()), 389 | choice 390 | ); 391 | } 392 | } 393 | } 394 | 395 | execute!( 396 | stdout, 397 | Print(format!("{}\n", "=======================".bright_black())), 398 | )?; 399 | 400 | execute!( 401 | stdout, 402 | MoveToPreviousLine(lines_to_move_up), 403 | Clear(ClearType::FromCursorDown), 404 | )?; 405 | 406 | Ok(choices) 407 | } 408 | } 409 | 410 | #[derive(Debug, Serialize, Deserialize, Default)] 411 | pub struct Response { 412 | pub id: String, 413 | pub object: String, 414 | pub created: i64, 415 | pub model: String, 416 | pub choices: Vec, 417 | pub usage: Option, 418 | } 419 | 420 | #[derive(Debug, Serialize, Deserialize, Default)] 421 | pub struct Choice { 422 | pub index: i64, 423 | pub finish_reason: Option, 424 | pub delta: Delta, 425 | } 426 | 427 | #[derive(Debug, Serialize, Deserialize, Default)] 428 | pub struct Delta { 429 | pub role: Option, 430 | pub content: Option, 431 | } 432 | 433 | #[derive(Debug, Serialize, Deserialize)] 434 | pub struct Usage { 435 | pub prompt_tokens: usize, 436 | pub completion_tokens: usize, 437 | pub total_tokens: usize, 438 | #[serde(default)] 439 | pub completion_tokens_details: CompletionTokensDetails, 440 | } 441 | 442 | #[derive(Serialize, Deserialize, Debug, Default)] 443 | pub struct CompletionTokensDetails { 444 | pub reasoning_tokens: usize, 445 | pub accepted_prediction_tokens: usize, 446 | pub rejected_prediction_tokens: usize, 447 | } 448 | 449 | pub fn count_token(s: &str) -> anyhow::Result { 450 | let bpe = tiktoken_rs::cl100k_base()?; 451 | let tokens = bpe.encode_with_special_tokens(s); 452 | Ok(tokens.len()) 453 | } 454 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use colored::Colorize; 4 | use inquire::MultiSelect; 5 | use unicode_segmentation::UnicodeSegmentation; 6 | 7 | use crate::{config::Config, git, openai}; 8 | 9 | pub fn decide_diff( 10 | repo: &git2::Repository, 11 | used_tokens: usize, 12 | context: usize, 13 | always_select_files: bool, 14 | ) -> anyhow::Result<(String, usize)> { 15 | let staged_files = git::staged_files(&repo)?; 16 | let mut diff = git::diff(&repo, &staged_files)?; 17 | let mut diff_tokens = openai::count_token(&diff)?; 18 | 19 | if diff_tokens == 0 { 20 | println!( 21 | "{} {}", 22 | "No staged files.".red(), 23 | "Please stage the files you want to commit.".bright_black() 24 | ); 25 | std::process::exit(1); 26 | } 27 | 28 | if always_select_files || used_tokens + diff_tokens > context { 29 | if always_select_files { 30 | println!( 31 | "{} {}", 32 | "File selection mode:".blue(), 33 | "Select the files you want to include in the commit.".bright_black() 34 | ); 35 | } else { 36 | println!( 37 | "{} {}", 38 | "The request is too long!".red(), 39 | format!( 40 | "The request is ~{} tokens long, while the maximum is {}.", 41 | used_tokens + diff_tokens, 42 | context 43 | ) 44 | .bright_black() 45 | ); 46 | } 47 | let selected_files = MultiSelect::new( 48 | "Select the files you want to include in the diff:", 49 | staged_files.clone(), 50 | ) 51 | .prompt()?; 52 | diff = git::diff(&repo, &selected_files)?; 53 | diff_tokens = openai::count_token(&diff)?; 54 | } 55 | Ok((diff, diff_tokens)) 56 | } 57 | 58 | #[must_use] 59 | pub fn count_lines(text: &str, max_width: usize) -> u16 { 60 | if text.is_empty() { 61 | return 0; 62 | } 63 | let mut line_count = 0; 64 | let mut current_line_width = 0; 65 | for cluster in UnicodeSegmentation::graphemes(text, true) { 66 | match cluster { 67 | "\r" | "\u{FEFF}" => {} 68 | "\n" => { 69 | line_count += 1; 70 | current_line_width = 0; 71 | } 72 | _ => { 73 | current_line_width += 1; 74 | if current_line_width > max_width { 75 | line_count += 1; 76 | current_line_width = cluster.chars().count(); 77 | } 78 | } 79 | } 80 | } 81 | 82 | line_count + 1 83 | } 84 | 85 | pub fn check_config_age(max_age: Duration) -> bool { 86 | let path = Config::path(); 87 | let metadata = match std::fs::metadata(&path) { 88 | Ok(metadata) => metadata, 89 | Err(_) => { 90 | return false; 91 | } 92 | }; 93 | let last_modified = metadata.modified().unwrap(); 94 | let now = std::time::SystemTime::now(); 95 | match now.duration_since(last_modified) { 96 | Ok(duration) => duration > max_age, 97 | Err(_) => false, 98 | } 99 | } 100 | 101 | pub fn is_system_prompt_same_as_default(system_msg: &str) -> bool { 102 | let default = Config::default().system_msg; 103 | system_msg == default 104 | } 105 | 106 | pub async fn check_version() { 107 | let client = match crates_io_api::AsyncClient::new( 108 | "turbocommit latest version checker", 109 | Duration::from_millis(1000), 110 | ) { 111 | Ok(client) => client, 112 | Err(_) => { 113 | return; 114 | } 115 | }; 116 | let turbo = match client.get_crate("turbocommit").await { 117 | Ok(turbo) => turbo, 118 | Err(_) => { 119 | return; 120 | } 121 | }; 122 | let newest_version = turbo.versions[0].num.clone(); 123 | let current_version = env!("CARGO_PKG_VERSION"); 124 | 125 | if current_version != newest_version { 126 | println!( 127 | "\n{} {}", 128 | "New version available!".yellow(), 129 | format!("v{}", newest_version).purple() 130 | ); 131 | println!( 132 | "To update, run\n{}", 133 | "cargo install --force turbocommit".purple() 134 | ); 135 | } 136 | } 137 | 138 | pub fn choose_message(choices: Vec) -> Option { 139 | if choices.len() == 1 { 140 | return Some(process_response(&choices[0])); 141 | } 142 | let max_index = choices.len(); 143 | let commit_index = match inquire::CustomType::::new(&format!( 144 | "Which commit message do you want to use? {}", 145 | " to cancel".bright_black() 146 | )) 147 | .with_validator(move |i: &usize| { 148 | if *i >= max_index { 149 | Err(inquire::CustomUserError::from("Invalid index")) 150 | } else { 151 | Ok(inquire::validator::Validation::Valid) 152 | } 153 | }) 154 | .prompt() 155 | { 156 | Ok(index) => index, 157 | Err(_) => { 158 | return None; 159 | } 160 | }; 161 | Some(process_response(&choices[commit_index])) 162 | } 163 | 164 | pub fn format_token_count(tokens: usize) -> String { 165 | format!("{:.2}k", tokens as f64 / 1000.0) 166 | } 167 | 168 | fn process_response(response: &str) -> String { 169 | // If response contains tag, extract and process it 170 | if let Some(think_start) = response.find("") { 171 | if let Some(think_end) = response.find("") { 172 | let thinking = &response[think_start + 7..think_end]; 173 | // Get message part and trim any whitespace including newlines at start/end 174 | let message_part = response[think_end + 8..].trim_matches(|c: char| c.is_whitespace()); 175 | 176 | // Print the thinking section nicely 177 | println!("\n{}", "AI's Thought Process:".blue().bold()); 178 | println!("{}", thinking.bright_black()); 179 | println!("\n{}", "Generated Commit Message:".blue().bold()); 180 | println!("[0] {}", "=".repeat(76)); 181 | println!("{}", message_part); 182 | 183 | return message_part.to_string(); 184 | } 185 | } 186 | // If no think tags found, return the original response trimmed of all whitespace including newlines 187 | response.trim_matches(|c: char| c.is_whitespace()).to_string() 188 | } 189 | --------------------------------------------------------------------------------