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 install_codex_mcp_config_at(None)
350}
351
352pub(crate) fn install_codex_mcp_config_at(home_override: Option<&Path>) -> Result<bool> {
358 let path = match home_override {
359 Some(h) => h.join("config.toml"),
360 None => codex_config_path(),
361 };
362
363 let existing = if path.exists() {
366 std::fs::read_to_string(&path).map_err(|e| {
367 SqzError::Other(format!("failed to read {}: {e}", path.display()))
368 })?
369 } else {
370 String::new()
371 };
372
373 let mut doc: toml_edit::DocumentMut = existing
374 .parse()
375 .map_err(|e| SqzError::Other(format!(
376 "failed to parse {} as TOML: {e}",
377 path.display()
378 )))?;
379
380 if let Some(existing_cmd) = doc
385 .get("mcp_servers")
386 .and_then(|v| v.get("sqz"))
387 .and_then(|v| v.get("command"))
388 {
389 if existing_cmd.is_value() {
390 return Ok(false);
391 }
392 }
393
394 let mcp_servers = doc
404 .entry("mcp_servers")
405 .or_insert_with(|| {
406 let mut t = toml_edit::Table::new();
409 t.set_implicit(true);
410 toml_edit::Item::Table(t)
411 })
412 .as_table_mut()
413 .ok_or_else(|| SqzError::Other(format!(
414 "{}: `mcp_servers` is not a table — refusing to overwrite",
415 path.display()
416 )))?;
417
418 let sqz = mcp_servers
419 .entry("sqz")
420 .or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new()))
421 .as_table_mut()
422 .ok_or_else(|| SqzError::Other(format!(
423 "{}: `mcp_servers.sqz` is not a table — refusing to overwrite",
424 path.display()
425 )))?;
426
427 if !sqz.contains_key("command") {
430 sqz["command"] = toml_edit::value("sqz-mcp");
431 }
432 if !sqz.contains_key("args") {
433 let mut args = toml_edit::Array::new();
434 args.push("--transport");
435 args.push("stdio");
436 sqz["args"] = toml_edit::Item::Value(toml_edit::Value::Array(args));
437 }
438
439 if let Some(parent) = path.parent() {
441 std::fs::create_dir_all(parent).map_err(|e| {
442 SqzError::Other(format!(
443 "failed to create {}: {e}",
444 parent.display()
445 ))
446 })?;
447 }
448
449 std::fs::write(&path, doc.to_string()).map_err(|e| {
450 SqzError::Other(format!("failed to write {}: {e}", path.display()))
451 })?;
452 Ok(true)
453}
454
455pub fn remove_codex_mcp_config() -> Result<Option<(PathBuf, bool)>> {
473 remove_codex_mcp_config_at(None)
474}
475
476pub(crate) fn remove_codex_mcp_config_at(
479 home_override: Option<&Path>,
480) -> Result<Option<(PathBuf, bool)>> {
481 let path = match home_override {
482 Some(h) => h.join("config.toml"),
483 None => codex_config_path(),
484 };
485 if !path.exists() {
486 return Ok(None);
487 }
488 let raw = std::fs::read_to_string(&path)
489 .map_err(|e| SqzError::Other(format!("failed to read {}: {e}", path.display())))?;
490
491 let mut doc: toml_edit::DocumentMut = match raw.parse() {
492 Ok(d) => d,
493 Err(_) => {
494 return Ok(Some((path, false)));
496 }
497 };
498
499 let mcp_table = match doc.get_mut("mcp_servers").and_then(|v| v.as_table_mut()) {
500 Some(t) => t,
501 None => return Ok(Some((path, false))),
502 };
503 if !mcp_table.contains_key("sqz") {
504 return Ok(Some((path, false)));
505 }
506 mcp_table.remove("sqz");
507
508 let mcp_is_empty = mcp_table.iter().count() == 0;
511 if mcp_is_empty {
512 doc.remove("mcp_servers");
513 }
514
515 let doc_is_empty = doc.iter().count() == 0;
517 if doc_is_empty {
518 std::fs::remove_file(&path).map_err(|e| {
519 SqzError::Other(format!("failed to remove {}: {e}", path.display()))
520 })?;
521 return Ok(Some((path, true)));
522 }
523
524 std::fs::write(&path, doc.to_string()).map_err(|e| {
525 SqzError::Other(format!("failed to write {}: {e}", path.display()))
526 })?;
527 Ok(Some((path, true)))
528}
529
530#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
541 fn codex_config_path_ends_with_config_toml() {
542 let got = codex_config_path();
543 assert!(
544 got.ends_with("config.toml"),
545 "codex_config_path() must end with config.toml, got: {}",
546 got.display()
547 );
548 }
549
550 #[test]
553 fn agents_md_guidance_block_contains_sqz_invocation() {
554 let block = agents_md_guidance_block("/usr/local/bin/sqz");
555 assert!(block.contains(AGENTS_MD_BEGIN));
556 assert!(block.contains(AGENTS_MD_END));
557 assert!(block.contains("| /usr/local/bin/sqz compress"));
558 assert!(block.contains("sqz-mcp"));
559 }
560
561 #[test]
562 fn install_agents_md_creates_file_with_preamble() {
563 let dir = tempfile::tempdir().unwrap();
564 let created = install_agents_md_guidance(dir.path(), "sqz").unwrap();
565 assert!(created);
566 let content = std::fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
567 assert!(content.starts_with("# AGENTS.md"));
568 assert!(content.contains(AGENTS_MD_BEGIN));
569 assert!(content.contains(AGENTS_MD_END));
570 }
571
572 #[test]
573 fn install_agents_md_appends_without_clobbering_user_content() {
574 let dir = tempfile::tempdir().unwrap();
575 let path = dir.path().join("AGENTS.md");
576 std::fs::write(
577 &path,
578 "# My project rules\n\nBe polite. Run tests before committing.\n",
579 ).unwrap();
580
581 let created = install_agents_md_guidance(dir.path(), "sqz").unwrap();
582 assert!(created);
583
584 let content = std::fs::read_to_string(&path).unwrap();
585 assert!(content.contains("# My project rules"),
586 "original heading must survive");
587 assert!(content.contains("Be polite. Run tests before committing."),
588 "original body must survive");
589 let user_idx = content.find("Be polite").unwrap();
590 let sqz_idx = content.find(AGENTS_MD_BEGIN).unwrap();
591 assert!(sqz_idx > user_idx,
592 "sqz's block must append after user content, not prepend");
593 }
594
595 #[test]
596 fn install_agents_md_is_idempotent() {
597 let dir = tempfile::tempdir().unwrap();
598 let first = install_agents_md_guidance(dir.path(), "sqz").unwrap();
599 assert!(first);
600 let second = install_agents_md_guidance(dir.path(), "sqz").unwrap();
601 assert!(!second, "second install must be a no-op");
602
603 let content = std::fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
604 let occurrences = content.matches(AGENTS_MD_BEGIN).count();
605 assert_eq!(occurrences, 1, "must not duplicate the block on re-install");
606 }
607
608 #[test]
609 fn remove_agents_md_preserves_user_content() {
610 let dir = tempfile::tempdir().unwrap();
611 let path = dir.path().join("AGENTS.md");
612 std::fs::write(
613 &path,
614 "# My project rules\n\nBe polite. Run tests before committing.\n",
615 ).unwrap();
616 install_agents_md_guidance(dir.path(), "sqz").unwrap();
617
618 let (returned_path, changed) =
619 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
620 assert_eq!(returned_path, path);
621 assert!(changed);
622 assert!(path.exists(),
623 "file must NOT be deleted — it has user content");
624
625 let content = std::fs::read_to_string(&path).unwrap();
626 assert!(!content.contains(AGENTS_MD_BEGIN));
627 assert!(!content.contains(AGENTS_MD_END));
628 assert!(content.contains("# My project rules"),
629 "user heading must survive the uninstall");
630 assert!(content.contains("Be polite. Run tests before committing."),
631 "user body must survive the uninstall");
632 }
633
634 #[test]
635 fn remove_agents_md_deletes_file_when_only_sqz_preamble_remains() {
636 let dir = tempfile::tempdir().unwrap();
637 install_agents_md_guidance(dir.path(), "sqz").unwrap();
638 let path = dir.path().join("AGENTS.md");
639 assert!(path.exists());
640
641 let (_returned, changed) =
642 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
643 assert!(changed);
644 assert!(
645 !path.exists(),
646 "fresh-install AGENTS.md must be removed when sqz block is stripped"
647 );
648 }
649
650 #[test]
651 fn remove_agents_md_noop_when_block_missing() {
652 let dir = tempfile::tempdir().unwrap();
653 let path = dir.path().join("AGENTS.md");
654 std::fs::write(&path, "# User-authored, sqz never touched this.\n").unwrap();
655
656 let (returned_path, changed) =
657 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
658 assert_eq!(returned_path, path);
659 assert!(!changed, "no sqz block means no change");
660 assert!(path.exists(), "user file must be untouched");
661 }
662
663 #[test]
664 fn remove_agents_md_returns_none_when_file_missing() {
665 let dir = tempfile::tempdir().unwrap();
666 let result = remove_agents_md_guidance(dir.path()).unwrap();
667 assert!(result.is_none());
668 }
669
670 #[test]
673 fn install_codex_mcp_config_creates_file_with_sqz_entry() {
674 let dir = tempfile::tempdir().unwrap();
675 let created = install_codex_mcp_config_at(Some(dir.path())).unwrap();
676 assert!(created);
677 let content = std::fs::read_to_string(dir.path().join("config.toml")).unwrap();
678 assert!(
679 content.contains("[mcp_servers.sqz]"),
680 "config.toml must contain [mcp_servers.sqz] header; got:\n{content}"
681 );
682 assert!(content.contains("command = \"sqz-mcp\""),
683 "command must be sqz-mcp");
684 assert!(content.contains("--transport"));
685 assert!(content.contains("stdio"));
686 }
687
688 #[test]
689 fn install_codex_mcp_config_preserves_existing_other_servers() {
690 let dir = tempfile::tempdir().unwrap();
691 let cfg = dir.path().join("config.toml");
692 std::fs::write(
693 &cfg,
694 "# User's existing Codex config, with a comment.\n\
695 model = \"gpt-5\"\n\
696 \n\
697 [mcp_servers.other]\n\
698 command = \"other-server\"\n\
699 args = [\"--flag\"]\n",
700 ).unwrap();
701
702 let created = install_codex_mcp_config_at(Some(dir.path())).unwrap();
703 assert!(created);
704
705 let after = std::fs::read_to_string(&cfg).unwrap();
706 assert!(after.contains("# User's existing Codex config"),
707 "comment must survive: {after}");
708 assert!(after.contains("model = \"gpt-5\""),
709 "top-level key must survive: {after}");
710 assert!(after.contains("[mcp_servers.other]"),
711 "existing server entry must survive: {after}");
712 assert!(after.contains("command = \"other-server\""),
713 "existing server command must survive: {after}");
714 assert!(after.contains("[mcp_servers.sqz]"),
715 "sqz entry must be added: {after}");
716 }
717
718 #[test]
719 fn install_codex_mcp_config_is_idempotent() {
720 let dir = tempfile::tempdir().unwrap();
721 assert!(install_codex_mcp_config_at(Some(dir.path())).unwrap());
722 assert!(
723 !install_codex_mcp_config_at(Some(dir.path())).unwrap(),
724 "second install with complete [mcp_servers.sqz] must be a no-op"
725 );
726 }
727
728 #[test]
729 fn install_codex_mcp_config_does_not_overwrite_user_tuned_entry() {
730 let dir = tempfile::tempdir().unwrap();
731 let cfg = dir.path().join("config.toml");
732 std::fs::write(
733 &cfg,
734 "[mcp_servers.sqz]\n\
735 command = \"/custom/path/sqz-mcp\"\n\
736 args = [\"--transport\", \"sse\", \"--port\", \"3999\"]\n",
737 ).unwrap();
738
739 let changed = install_codex_mcp_config_at(Some(dir.path())).unwrap();
740 assert!(!changed, "existing complete entry must be idempotent-skipped");
741 let after = std::fs::read_to_string(&cfg).unwrap();
742 assert!(after.contains("/custom/path/sqz-mcp"),
743 "user's custom command must survive re-init");
744 assert!(after.contains("\"sse\""),
745 "user's custom transport must survive");
746 }
747
748 #[test]
749 fn remove_codex_mcp_config_removes_only_sqz_entry() {
750 let dir = tempfile::tempdir().unwrap();
751 let cfg = dir.path().join("config.toml");
752 std::fs::write(
753 &cfg,
754 "# keep this comment\n\
755 model = \"gpt-5\"\n\
756 \n\
757 [mcp_servers.other]\n\
758 command = \"other-server\"\n\
759 \n\
760 [mcp_servers.sqz]\n\
761 command = \"sqz-mcp\"\n\
762 args = [\"--transport\", \"stdio\"]\n",
763 ).unwrap();
764
765 let (path, changed) = remove_codex_mcp_config_at(Some(dir.path())).unwrap().unwrap();
766 assert_eq!(path, cfg);
767 assert!(changed);
768
769 let after = std::fs::read_to_string(&cfg).unwrap();
770 assert!(after.contains("# keep this comment"),
771 "comment must survive: {after}");
772 assert!(after.contains("model = \"gpt-5\""),
773 "top-level key must survive: {after}");
774 assert!(after.contains("[mcp_servers.other]"),
775 "other server entry must survive: {after}");
776 assert!(!after.contains("[mcp_servers.sqz]"),
777 "sqz entry must be gone: {after}");
778 }
779
780 #[test]
781 fn remove_codex_mcp_config_deletes_file_when_sqz_was_the_only_entry() {
782 let dir = tempfile::tempdir().unwrap();
783 let cfg = dir.path().join("config.toml");
784 install_codex_mcp_config_at(Some(dir.path())).unwrap();
785 let (path, changed) = remove_codex_mcp_config_at(Some(dir.path())).unwrap().unwrap();
786 assert_eq!(path, cfg);
787 assert!(changed);
788 assert!(!cfg.exists(),
789 "config.toml with only sqz must be deleted on uninstall");
790 }
791
792 #[test]
793 fn remove_codex_mcp_config_returns_none_when_file_missing() {
794 let dir = tempfile::tempdir().unwrap();
795 let result = remove_codex_mcp_config_at(Some(dir.path())).unwrap();
796 assert!(result.is_none());
797 }
798
799 #[test]
800 fn remove_codex_mcp_config_noop_when_sqz_entry_missing() {
801 let dir = tempfile::tempdir().unwrap();
802 let cfg = dir.path().join("config.toml");
803 std::fs::write(&cfg, "[mcp_servers.other]\ncommand = \"x\"\n").unwrap();
804 let (path, changed) = remove_codex_mcp_config_at(Some(dir.path())).unwrap().unwrap();
805 assert_eq!(path, cfg);
806 assert!(!changed);
807 let after = std::fs::read_to_string(&cfg).unwrap();
808 assert!(after.contains("[mcp_servers.other]"));
809 }
810}