1use std::path::{Path, PathBuf};
35
36use crate::error::{Result, SqzError};
37
38pub fn codex_config_path() -> PathBuf {
47 if let Ok(home) = std::env::var("CODEX_HOME") {
48 return PathBuf::from(home).join("config.toml");
49 }
50 let home = std::env::var("HOME")
51 .or_else(|_| std::env::var("USERPROFILE"))
52 .map(PathBuf::from)
53 .unwrap_or_else(|_| PathBuf::from("."));
54 home.join(".codex").join("config.toml")
55}
56
57pub fn agents_md_path(project_dir: &Path) -> PathBuf {
64 project_dir.join("AGENTS.md")
65}
66
67const AGENTS_MD_BEGIN: &str = "<!-- BEGIN sqz-agents-guidance (auto-installed by sqz init; remove this block to disable) -->";
76const AGENTS_MD_END: &str = "<!-- END sqz-agents-guidance -->";
77
78pub fn agents_md_guidance_block(sqz_path: &str) -> String {
87 format!(
88 "\
89{begin}
90
91## sqz — Token-Optimized CLI Output
92
93When running shell commands whose output may be long (directory listings,
94git log/diff, test runners, build logs, `docker ps`, `kubectl get`, etc.),
95pipe the output through `sqz compress` to reduce token consumption.
96
97`sqz` is a stdin-to-stdout compressor, not a command wrapper. The correct
98usage is to pipe the command's output into `sqz compress`:
99
100```bash
101# Instead of: Use:
102git status git status 2>&1 | {sqz_path} compress
103cargo test cargo test 2>&1 | {sqz_path} compress
104git log -10 git log -10 2>&1 | {sqz_path} compress
105docker ps docker ps 2>&1 | {sqz_path} compress
106ls -la ls -la 2>&1 | {sqz_path} compress
107```
108
109The `2>&1` captures stderr too, which is useful for commands like `cargo
110test` where diagnostics go to stderr. `sqz compress` filters and compresses
111the combined output while preserving filenames, paths, and identifiers.
112It typically saves 60-90% tokens on verbose commands.
113
114Do NOT pipe output for:
115- Interactive commands (`vim`, `ssh`, `python`, REPLs)
116- Compound commands with shell operators (`cmd && other`, `cmd > file.txt`,
117 `cmd; other`) — run those directly
118- Short commands whose output is already a few lines
119
120If `sqz` is not on PATH, run commands normally.
121
122The `sqz-mcp` MCP server is also available — Codex reads it from
123`~/.codex/config.toml` under `[mcp_servers.sqz]`. It exposes three
124tools: `compress` (the default pipeline), `passthrough` (return text
125unchanged — the escape hatch below), and `expand` (resolve a
126`§ref:HASH§` token back to the original bytes).
127
128## Escape hatch — when sqz output confuses you
129
130If you see a `§ref:HASH§` token and can't parse it, or compressed
131output is leading you to make lots of small retries instead of one
132big request, use one of these:
133
134- **`{sqz_path} expand <prefix>`** — resolve a dedup ref back to the
135 original bytes. Accepts bare hex (`sqz expand a1b2c3d4`) or the full
136 token pasted verbatim (`sqz expand §ref:a1b2c3d4§`).
137- **`SQZ_NO_DEDUP=1`** — set this env var for one command to disable
138 dedup: `SQZ_NO_DEDUP=1 git status 2>&1 | sqz compress`. You'll get
139 the full compressed output with no `§ref:…§` tokens.
140- **`--no-cache`** — same opt-out as a CLI flag:
141 `git status 2>&1 | sqz compress --no-cache`.
142
143If you're using the MCP server, the `passthrough` tool returns raw
144text and the `expand` tool resolves refs — call them when you need
145data sqz hasn't touched.
146
147{end}
148",
149 begin = AGENTS_MD_BEGIN,
150 end = AGENTS_MD_END,
151 )
152}
153
154fn agents_md_has_sqz_block(content: &str) -> bool {
159 content.contains(AGENTS_MD_BEGIN)
160}
161
162pub fn install_agents_md_guidance(project_dir: &Path, sqz_path: &str) -> Result<bool> {
173 let path = agents_md_path(project_dir);
174 let block = agents_md_guidance_block(sqz_path);
175
176 if path.exists() {
177 let existing = std::fs::read_to_string(&path).map_err(|e| {
178 SqzError::Other(format!("failed to read {}: {e}", path.display()))
179 })?;
180 if agents_md_has_sqz_block(&existing) {
181 return Ok(false);
182 }
183 let mut new_content = existing;
186 if !new_content.ends_with('\n') {
187 new_content.push('\n');
188 }
189 if !new_content.ends_with("\n\n") {
190 new_content.push('\n');
191 }
192 new_content.push_str(&block);
193 std::fs::write(&path, new_content).map_err(|e| {
194 SqzError::Other(format!("failed to write {}: {e}", path.display()))
195 })?;
196 return Ok(true);
197 }
198
199 let preamble = "\
203# AGENTS.md
204
205Instructions for AI coding agents (OpenAI Codex, GitHub Copilot, Cursor,
206Windsurf, Amp, Devin) working in this repository. See <https://agentsmd.io>.
207
208";
209 let content = format!("{preamble}{block}");
210 std::fs::write(&path, content)
211 .map_err(|e| SqzError::Other(format!("failed to create {}: {e}", path.display())))?;
212 Ok(true)
213}
214
215pub fn remove_agents_md_guidance(project_dir: &Path) -> Result<Option<(PathBuf, bool)>> {
230 let path = agents_md_path(project_dir);
231 if !path.exists() {
232 return Ok(None);
233 }
234 let content = std::fs::read_to_string(&path).map_err(|e| {
235 SqzError::Other(format!("failed to read {}: {e}", path.display()))
236 })?;
237 if !agents_md_has_sqz_block(&content) {
238 return Ok(Some((path, false)));
239 }
240
241 let begin_idx = match content.find(AGENTS_MD_BEGIN) {
245 Some(i) => i,
246 None => return Ok(Some((path, false))),
247 };
248 let after_end_idx = match content.find(AGENTS_MD_END) {
249 Some(i) => i + AGENTS_MD_END.len(),
250 None => {
251 return Ok(Some((path, false)));
255 }
256 };
257
258 let mut new_content = String::with_capacity(content.len());
259 new_content.push_str(&content[..begin_idx]);
260 while new_content.ends_with("\n\n\n") {
264 new_content.pop();
265 }
266 let tail = &content[after_end_idx..];
267 let trimmed_tail = tail.trim_start_matches('\n');
270 if !trimmed_tail.is_empty() {
271 if !new_content.ends_with('\n') {
272 new_content.push('\n');
273 }
274 new_content.push_str(trimmed_tail);
275 }
276
277 let essentially_empty = is_essentially_empty_agents_md(&new_content);
281 if essentially_empty {
282 std::fs::remove_file(&path).map_err(|e| {
283 SqzError::Other(format!("failed to remove {}: {e}", path.display()))
284 })?;
285 return Ok(Some((path, true)));
286 }
287
288 std::fs::write(&path, new_content).map_err(|e| {
289 SqzError::Other(format!("failed to write {}: {e}", path.display()))
290 })?;
291 Ok(Some((path, true)))
292}
293
294fn is_essentially_empty_agents_md(s: &str) -> bool {
299 let trimmed = s.trim();
300 if trimmed.is_empty() {
301 return true;
302 }
303 const PREAMBLE_MARKERS: &[&str] = &[
306 "# AGENTS.md",
307 "Instructions for AI coding agents",
308 "See <https://agentsmd.io>.",
309 ];
310 let has_only_preamble = PREAMBLE_MARKERS.iter().all(|m| trimmed.contains(m))
311 && !trimmed
312 .lines()
313 .any(|l| {
314 let l = l.trim();
315 !l.is_empty()
316 && !l.starts_with('#')
317 && !PREAMBLE_MARKERS.iter().any(|m| l.contains(m))
318 });
319 has_only_preamble
320}
321
322pub fn install_codex_mcp_config() -> Result<bool> {
349 let path = codex_config_path();
350
351 let existing = if path.exists() {
354 std::fs::read_to_string(&path).map_err(|e| {
355 SqzError::Other(format!("failed to read {}: {e}", path.display()))
356 })?
357 } else {
358 String::new()
359 };
360
361 let mut doc: toml_edit::DocumentMut = existing
362 .parse()
363 .map_err(|e| SqzError::Other(format!(
364 "failed to parse {} as TOML: {e}",
365 path.display()
366 )))?;
367
368 if let Some(existing_cmd) = doc
373 .get("mcp_servers")
374 .and_then(|v| v.get("sqz"))
375 .and_then(|v| v.get("command"))
376 {
377 if existing_cmd.is_value() {
378 return Ok(false);
379 }
380 }
381
382 let mcp_servers = doc
392 .entry("mcp_servers")
393 .or_insert_with(|| {
394 let mut t = toml_edit::Table::new();
397 t.set_implicit(true);
398 toml_edit::Item::Table(t)
399 })
400 .as_table_mut()
401 .ok_or_else(|| SqzError::Other(format!(
402 "{}: `mcp_servers` is not a table — refusing to overwrite",
403 path.display()
404 )))?;
405
406 let sqz = mcp_servers
407 .entry("sqz")
408 .or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new()))
409 .as_table_mut()
410 .ok_or_else(|| SqzError::Other(format!(
411 "{}: `mcp_servers.sqz` is not a table — refusing to overwrite",
412 path.display()
413 )))?;
414
415 if !sqz.contains_key("command") {
418 sqz["command"] = toml_edit::value("sqz-mcp");
419 }
420 if !sqz.contains_key("args") {
421 let mut args = toml_edit::Array::new();
422 args.push("--transport");
423 args.push("stdio");
424 sqz["args"] = toml_edit::Item::Value(toml_edit::Value::Array(args));
425 }
426
427 if let Some(parent) = path.parent() {
429 std::fs::create_dir_all(parent).map_err(|e| {
430 SqzError::Other(format!(
431 "failed to create {}: {e}",
432 parent.display()
433 ))
434 })?;
435 }
436
437 std::fs::write(&path, doc.to_string()).map_err(|e| {
438 SqzError::Other(format!("failed to write {}: {e}", path.display()))
439 })?;
440 Ok(true)
441}
442
443pub fn remove_codex_mcp_config() -> Result<Option<(PathBuf, bool)>> {
461 let path = codex_config_path();
462 if !path.exists() {
463 return Ok(None);
464 }
465 let raw = std::fs::read_to_string(&path)
466 .map_err(|e| SqzError::Other(format!("failed to read {}: {e}", path.display())))?;
467
468 let mut doc: toml_edit::DocumentMut = match raw.parse() {
469 Ok(d) => d,
470 Err(_) => {
471 return Ok(Some((path, false)));
473 }
474 };
475
476 let mcp_table = match doc.get_mut("mcp_servers").and_then(|v| v.as_table_mut()) {
477 Some(t) => t,
478 None => return Ok(Some((path, false))),
479 };
480 if !mcp_table.contains_key("sqz") {
481 return Ok(Some((path, false)));
482 }
483 mcp_table.remove("sqz");
484
485 let mcp_is_empty = mcp_table.iter().count() == 0;
488 if mcp_is_empty {
489 doc.remove("mcp_servers");
490 }
491
492 let doc_is_empty = doc.iter().count() == 0;
494 if doc_is_empty {
495 std::fs::remove_file(&path).map_err(|e| {
496 SqzError::Other(format!("failed to remove {}: {e}", path.display()))
497 })?;
498 return Ok(Some((path, true)));
499 }
500
501 std::fs::write(&path, doc.to_string()).map_err(|e| {
502 SqzError::Other(format!("failed to write {}: {e}", path.display()))
503 })?;
504 Ok(Some((path, true)))
505}
506
507#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn codex_config_path_honours_codex_home() {
515 let dir = tempfile::tempdir().unwrap();
516 let prev = std::env::var_os("CODEX_HOME");
517 std::env::set_var("CODEX_HOME", dir.path());
518 let got = codex_config_path();
519 assert_eq!(got, dir.path().join("config.toml"));
520 match prev {
521 Some(v) => std::env::set_var("CODEX_HOME", v),
522 None => std::env::remove_var("CODEX_HOME"),
523 }
524 }
525
526 #[test]
527 fn codex_config_path_falls_back_to_home_dot_codex() {
528 let prev_codex = std::env::var_os("CODEX_HOME");
529 let prev_home = std::env::var_os("HOME");
530 std::env::remove_var("CODEX_HOME");
531 std::env::set_var("HOME", "/tmp/codex-home-test");
532 let got = codex_config_path();
533 assert_eq!(got, PathBuf::from("/tmp/codex-home-test/.codex/config.toml"));
534 match prev_codex {
535 Some(v) => std::env::set_var("CODEX_HOME", v),
536 None => std::env::remove_var("CODEX_HOME"),
537 }
538 match prev_home {
539 Some(v) => std::env::set_var("HOME", v),
540 None => std::env::remove_var("HOME"),
541 }
542 }
543
544 #[test]
547 fn agents_md_guidance_block_contains_sqz_invocation() {
548 let block = agents_md_guidance_block("/usr/local/bin/sqz");
549 assert!(block.contains(AGENTS_MD_BEGIN));
550 assert!(block.contains(AGENTS_MD_END));
551 assert!(block.contains("| /usr/local/bin/sqz compress"));
552 assert!(block.contains("sqz-mcp"));
553 }
554
555 #[test]
556 fn install_agents_md_creates_file_with_preamble() {
557 let dir = tempfile::tempdir().unwrap();
558 let created = install_agents_md_guidance(dir.path(), "sqz").unwrap();
559 assert!(created);
560 let content = std::fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
561 assert!(content.starts_with("# AGENTS.md"));
562 assert!(content.contains(AGENTS_MD_BEGIN));
563 assert!(content.contains(AGENTS_MD_END));
564 }
565
566 #[test]
567 fn install_agents_md_appends_without_clobbering_user_content() {
568 let dir = tempfile::tempdir().unwrap();
569 let path = dir.path().join("AGENTS.md");
570 std::fs::write(
571 &path,
572 "# My project rules\n\nBe polite. Run tests before committing.\n",
573 ).unwrap();
574
575 let created = install_agents_md_guidance(dir.path(), "sqz").unwrap();
576 assert!(created);
577
578 let content = std::fs::read_to_string(&path).unwrap();
579 assert!(content.contains("# My project rules"),
580 "original heading must survive");
581 assert!(content.contains("Be polite. Run tests before committing."),
582 "original body must survive");
583 let user_idx = content.find("Be polite").unwrap();
584 let sqz_idx = content.find(AGENTS_MD_BEGIN).unwrap();
585 assert!(sqz_idx > user_idx,
586 "sqz's block must append after user content, not prepend");
587 }
588
589 #[test]
590 fn install_agents_md_is_idempotent() {
591 let dir = tempfile::tempdir().unwrap();
592 let first = install_agents_md_guidance(dir.path(), "sqz").unwrap();
593 assert!(first);
594 let second = install_agents_md_guidance(dir.path(), "sqz").unwrap();
595 assert!(!second, "second install must be a no-op");
596
597 let content = std::fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
598 let occurrences = content.matches(AGENTS_MD_BEGIN).count();
599 assert_eq!(occurrences, 1, "must not duplicate the block on re-install");
600 }
601
602 #[test]
603 fn remove_agents_md_preserves_user_content() {
604 let dir = tempfile::tempdir().unwrap();
605 let path = dir.path().join("AGENTS.md");
606 std::fs::write(
607 &path,
608 "# My project rules\n\nBe polite. Run tests before committing.\n",
609 ).unwrap();
610 install_agents_md_guidance(dir.path(), "sqz").unwrap();
611
612 let (returned_path, changed) =
613 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
614 assert_eq!(returned_path, path);
615 assert!(changed);
616 assert!(path.exists(),
617 "file must NOT be deleted — it has user content");
618
619 let content = std::fs::read_to_string(&path).unwrap();
620 assert!(!content.contains(AGENTS_MD_BEGIN));
621 assert!(!content.contains(AGENTS_MD_END));
622 assert!(content.contains("# My project rules"),
623 "user heading must survive the uninstall");
624 assert!(content.contains("Be polite. Run tests before committing."),
625 "user body must survive the uninstall");
626 }
627
628 #[test]
629 fn remove_agents_md_deletes_file_when_only_sqz_preamble_remains() {
630 let dir = tempfile::tempdir().unwrap();
631 install_agents_md_guidance(dir.path(), "sqz").unwrap();
632 let path = dir.path().join("AGENTS.md");
633 assert!(path.exists());
634
635 let (_returned, changed) =
636 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
637 assert!(changed);
638 assert!(
639 !path.exists(),
640 "fresh-install AGENTS.md must be removed when sqz block is stripped"
641 );
642 }
643
644 #[test]
645 fn remove_agents_md_noop_when_block_missing() {
646 let dir = tempfile::tempdir().unwrap();
647 let path = dir.path().join("AGENTS.md");
648 std::fs::write(&path, "# User-authored, sqz never touched this.\n").unwrap();
649
650 let (returned_path, changed) =
651 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
652 assert_eq!(returned_path, path);
653 assert!(!changed, "no sqz block means no change");
654 assert!(path.exists(), "user file must be untouched");
655 }
656
657 #[test]
658 fn remove_agents_md_returns_none_when_file_missing() {
659 let dir = tempfile::tempdir().unwrap();
660 let result = remove_agents_md_guidance(dir.path()).unwrap();
661 assert!(result.is_none());
662 }
663
664 fn with_codex_home<F: FnOnce()>(home: &Path, f: F) {
669 let prev = std::env::var_os("CODEX_HOME");
670 std::env::set_var("CODEX_HOME", home);
671 f();
672 match prev {
673 Some(v) => std::env::set_var("CODEX_HOME", v),
674 None => std::env::remove_var("CODEX_HOME"),
675 }
676 }
677
678 #[test]
679 fn install_codex_mcp_config_creates_file_with_sqz_entry() {
680 let dir = tempfile::tempdir().unwrap();
681 with_codex_home(dir.path(), || {
682 let created = install_codex_mcp_config().unwrap();
683 assert!(created);
684 let content = std::fs::read_to_string(dir.path().join("config.toml")).unwrap();
685 assert!(
686 content.contains("[mcp_servers.sqz]"),
687 "config.toml must contain [mcp_servers.sqz] header; got:\n{content}"
688 );
689 assert!(content.contains("command = \"sqz-mcp\""),
690 "command must be sqz-mcp");
691 assert!(content.contains("--transport"));
692 assert!(content.contains("stdio"));
693 });
694 }
695
696 #[test]
697 fn install_codex_mcp_config_preserves_existing_other_servers() {
698 let dir = tempfile::tempdir().unwrap();
699 let cfg = dir.path().join("config.toml");
700 std::fs::write(
701 &cfg,
702 "# User's existing Codex config, with a comment.\n\
703 model = \"gpt-5\"\n\
704 \n\
705 [mcp_servers.other]\n\
706 command = \"other-server\"\n\
707 args = [\"--flag\"]\n",
708 ).unwrap();
709
710 with_codex_home(dir.path(), || {
711 let created = install_codex_mcp_config().unwrap();
712 assert!(created);
713 });
714
715 let after = std::fs::read_to_string(&cfg).unwrap();
716 assert!(after.contains("# User's existing Codex config"),
717 "comment must survive: {after}");
718 assert!(after.contains("model = \"gpt-5\""),
719 "top-level key must survive: {after}");
720 assert!(after.contains("[mcp_servers.other]"),
721 "existing server entry must survive: {after}");
722 assert!(after.contains("command = \"other-server\""),
723 "existing server command must survive: {after}");
724 assert!(after.contains("[mcp_servers.sqz]"),
725 "sqz entry must be added: {after}");
726 }
727
728 #[test]
729 fn install_codex_mcp_config_is_idempotent() {
730 let dir = tempfile::tempdir().unwrap();
731 with_codex_home(dir.path(), || {
732 assert!(install_codex_mcp_config().unwrap());
733 assert!(
734 !install_codex_mcp_config().unwrap(),
735 "second install with complete [mcp_servers.sqz] must be a no-op"
736 );
737 });
738 }
739
740 #[test]
741 fn install_codex_mcp_config_does_not_overwrite_user_tuned_entry() {
742 let dir = tempfile::tempdir().unwrap();
743 let cfg = dir.path().join("config.toml");
744 std::fs::write(
745 &cfg,
746 "[mcp_servers.sqz]\n\
747 command = \"/custom/path/sqz-mcp\"\n\
748 args = [\"--transport\", \"sse\", \"--port\", \"3999\"]\n",
749 ).unwrap();
750
751 with_codex_home(dir.path(), || {
752 let changed = install_codex_mcp_config().unwrap();
753 assert!(!changed, "existing complete entry must be idempotent-skipped");
754 });
755 let after = std::fs::read_to_string(&cfg).unwrap();
756 assert!(after.contains("/custom/path/sqz-mcp"),
757 "user's custom command must survive re-init");
758 assert!(after.contains("\"sse\""),
759 "user's custom transport must survive");
760 }
761
762 #[test]
763 fn remove_codex_mcp_config_removes_only_sqz_entry() {
764 let dir = tempfile::tempdir().unwrap();
765 let cfg = dir.path().join("config.toml");
766 std::fs::write(
767 &cfg,
768 "# keep this comment\n\
769 model = \"gpt-5\"\n\
770 \n\
771 [mcp_servers.other]\n\
772 command = \"other-server\"\n\
773 \n\
774 [mcp_servers.sqz]\n\
775 command = \"sqz-mcp\"\n\
776 args = [\"--transport\", \"stdio\"]\n",
777 ).unwrap();
778
779 with_codex_home(dir.path(), || {
780 let (path, changed) = remove_codex_mcp_config().unwrap().unwrap();
781 assert_eq!(path, cfg);
782 assert!(changed);
783 });
784
785 let after = std::fs::read_to_string(&cfg).unwrap();
786 assert!(after.contains("# keep this comment"),
787 "comment must survive: {after}");
788 assert!(after.contains("model = \"gpt-5\""),
789 "top-level key must survive: {after}");
790 assert!(after.contains("[mcp_servers.other]"),
791 "other server entry must survive: {after}");
792 assert!(!after.contains("[mcp_servers.sqz]"),
793 "sqz entry must be gone: {after}");
794 }
795
796 #[test]
797 fn remove_codex_mcp_config_deletes_file_when_sqz_was_the_only_entry() {
798 let dir = tempfile::tempdir().unwrap();
799 let cfg = dir.path().join("config.toml");
800 with_codex_home(dir.path(), || {
801 install_codex_mcp_config().unwrap();
802 let (path, changed) = remove_codex_mcp_config().unwrap().unwrap();
803 assert_eq!(path, cfg);
804 assert!(changed);
805 });
806 assert!(!cfg.exists(),
807 "config.toml with only sqz must be deleted on uninstall");
808 }
809
810 #[test]
811 fn remove_codex_mcp_config_returns_none_when_file_missing() {
812 let dir = tempfile::tempdir().unwrap();
813 with_codex_home(dir.path(), || {
814 let result = remove_codex_mcp_config().unwrap();
815 assert!(result.is_none());
816 });
817 }
818
819 #[test]
820 fn remove_codex_mcp_config_noop_when_sqz_entry_missing() {
821 let dir = tempfile::tempdir().unwrap();
822 let cfg = dir.path().join("config.toml");
823 std::fs::write(&cfg, "[mcp_servers.other]\ncommand = \"x\"\n").unwrap();
824 with_codex_home(dir.path(), || {
825 let (path, changed) = remove_codex_mcp_config().unwrap().unwrap();
826 assert_eq!(path, cfg);
827 assert!(!changed);
828 });
829 let after = std::fs::read_to_string(&cfg).unwrap();
830 assert!(after.contains("[mcp_servers.other]"));
831 }
832}