1use std::path::{Path, PathBuf};
34
35use crate::error::{Result, SqzError};
36
37pub fn codex_config_path() -> PathBuf {
46 if let Ok(home) = std::env::var("CODEX_HOME") {
47 return PathBuf::from(home).join("config.toml");
48 }
49 let home = std::env::var("HOME")
50 .or_else(|_| std::env::var("USERPROFILE"))
51 .map(PathBuf::from)
52 .unwrap_or_else(|_| PathBuf::from("."));
53 home.join(".codex").join("config.toml")
54}
55
56pub fn agents_md_path(project_dir: &Path) -> PathBuf {
63 project_dir.join("AGENTS.md")
64}
65
66const AGENTS_MD_BEGIN: &str = "<!-- BEGIN sqz-agents-guidance (auto-installed by sqz init; remove this block to disable) -->";
75const AGENTS_MD_END: &str = "<!-- END sqz-agents-guidance -->";
76
77pub fn agents_md_guidance_block(sqz_path: &str) -> String {
86 format!(
87 "\
88{begin}
89
90## sqz — Token-Optimized CLI Output
91
92When running shell commands whose output may be long (directory listings,
93git log/diff, test runners, build logs, `docker ps`, `kubectl get`, etc.),
94pipe the output through `sqz compress` to reduce token consumption.
95
96`sqz` is a stdin-to-stdout compressor, not a command wrapper. The correct
97usage is to pipe the command's output into `sqz compress`:
98
99```bash
100# Instead of: Use:
101git status git status 2>&1 | {sqz_path} compress
102cargo test cargo test 2>&1 | {sqz_path} compress
103git log -10 git log -10 2>&1 | {sqz_path} compress
104docker ps docker ps 2>&1 | {sqz_path} compress
105ls -la ls -la 2>&1 | {sqz_path} compress
106```
107
108The `2>&1` captures stderr too, which is useful for commands like `cargo
109test` where diagnostics go to stderr. `sqz compress` filters and compresses
110the combined output while preserving filenames, paths, and identifiers.
111It typically saves 60-90% tokens on verbose commands.
112
113Do NOT pipe output for:
114- Interactive commands (`vim`, `ssh`, `python`, REPLs)
115- Compound commands with shell operators (`cmd && other`, `cmd > file.txt`,
116 `cmd; other`) — run those directly
117- Short commands whose output is already a few lines
118
119If `sqz` is not on PATH, run commands normally.
120
121The `sqz-mcp` MCP server is also available — Codex reads it from
122`~/.codex/config.toml` under `[mcp_servers.sqz]`. It exposes
123`compress` (the default pipeline), `passthrough` (return text
124unchanged — the escape hatch below), `expand` (resolve a
125`§ref:HASH§` token back to the original bytes), `sqz_recall`
126(full-text search over everything sqz has compressed — recover
127context lost to compaction), and `sqz_read_file` / `sqz_grep` /
128`sqz_list_dir` (file access with a dedup cache: a repeat read of an
129unchanged file costs ~13 tokens, and a ranged re-read via `offset` /
130`limit` of a file you already have in full returns a
131`§ref:HASH:L40-80§` line-range reference). Prefer those three over
132`cat`, `grep` and `ls` for anything you might read more than once.
133
134## Escape hatch — when sqz output confuses you
135
136If you see a `§ref:HASH§` token and can't parse it, or compressed
137output is leading you to make lots of small retries instead of one
138big request, use one of these:
139
140- **`{sqz_path} expand <prefix>`** — resolve a dedup ref back to the
141 original bytes. Accepts bare hex (`sqz expand a1b2c3d4`) or the full
142 token pasted verbatim (`sqz expand §ref:a1b2c3d4§`).
143- **`SQZ_NO_DEDUP=1`** — set this env var for one command to disable
144 dedup: `SQZ_NO_DEDUP=1 git status 2>&1 | sqz compress`. You'll get
145 the full compressed output with no `§ref:…§` tokens.
146- **`--no-cache`** — same opt-out as a CLI flag:
147 `git status 2>&1 | sqz compress --no-cache`.
148- **`SQZ_NO_ABBREV=1`** (or **`--no-abbrev`**) — disable n-gram phrase
149 abbreviation, which rewrites repeated phrases to `«A1»` symbols and
150 keeps only the first occurrence. Use it when output is full of
151 SHAs/paths/URLs you'll copy-paste verbatim:
152 `SQZ_NO_ABBREV=1 git log 2>&1 | sqz compress`.
153
154If you're using the MCP server, the `passthrough` tool returns raw
155text and the `expand` tool resolves refs — call them when you need
156data sqz hasn't touched.
157
158{end}
159",
160 begin = AGENTS_MD_BEGIN,
161 end = AGENTS_MD_END,
162 )
163}
164
165fn agents_md_has_sqz_block(content: &str) -> bool {
170 content.contains(AGENTS_MD_BEGIN)
171}
172
173pub fn install_agents_md_guidance(project_dir: &Path, sqz_path: &str) -> Result<bool> {
184 let path = agents_md_path(project_dir);
185 let block = agents_md_guidance_block(sqz_path);
186
187 if path.exists() {
188 let existing = std::fs::read_to_string(&path).map_err(|e| {
189 SqzError::Other(format!("failed to read {}: {e}", path.display()))
190 })?;
191 if agents_md_has_sqz_block(&existing) {
192 return Ok(false);
193 }
194 let mut new_content = existing;
197 if !new_content.ends_with('\n') {
198 new_content.push('\n');
199 }
200 if !new_content.ends_with("\n\n") {
201 new_content.push('\n');
202 }
203 new_content.push_str(&block);
204 std::fs::write(&path, new_content).map_err(|e| {
205 SqzError::Other(format!("failed to write {}: {e}", path.display()))
206 })?;
207 return Ok(true);
208 }
209
210 let preamble = "\
214# AGENTS.md
215
216Instructions for AI coding agents (OpenAI Codex, GitHub Copilot, Cursor,
217Windsurf, Amp, Devin) working in this repository. See <https://agentsmd.io>.
218
219";
220 let content = format!("{preamble}{block}");
221 std::fs::write(&path, content)
222 .map_err(|e| SqzError::Other(format!("failed to create {}: {e}", path.display())))?;
223 Ok(true)
224}
225
226pub fn remove_agents_md_guidance(project_dir: &Path) -> Result<Option<(PathBuf, bool)>> {
241 let path = agents_md_path(project_dir);
242 if !path.exists() {
243 return Ok(None);
244 }
245 let content = std::fs::read_to_string(&path).map_err(|e| {
246 SqzError::Other(format!("failed to read {}: {e}", path.display()))
247 })?;
248 if !agents_md_has_sqz_block(&content) {
249 return Ok(Some((path, false)));
250 }
251
252 let begin_idx = match content.find(AGENTS_MD_BEGIN) {
256 Some(i) => i,
257 None => return Ok(Some((path, false))),
258 };
259 let after_end_idx = match content.find(AGENTS_MD_END) {
260 Some(i) => i + AGENTS_MD_END.len(),
261 None => {
262 return Ok(Some((path, false)));
266 }
267 };
268
269 let mut new_content = String::with_capacity(content.len());
270 new_content.push_str(&content[..begin_idx]);
271 while new_content.ends_with("\n\n\n") {
275 new_content.pop();
276 }
277 let tail = &content[after_end_idx..];
278 let trimmed_tail = tail.trim_start_matches('\n');
281 if !trimmed_tail.is_empty() {
282 if !new_content.ends_with('\n') {
283 new_content.push('\n');
284 }
285 new_content.push_str(trimmed_tail);
286 }
287
288 let essentially_empty = is_essentially_empty_agents_md(&new_content);
292 if essentially_empty {
293 std::fs::remove_file(&path).map_err(|e| {
294 SqzError::Other(format!("failed to remove {}: {e}", path.display()))
295 })?;
296 return Ok(Some((path, true)));
297 }
298
299 std::fs::write(&path, new_content).map_err(|e| {
300 SqzError::Other(format!("failed to write {}: {e}", path.display()))
301 })?;
302 Ok(Some((path, true)))
303}
304
305fn is_essentially_empty_agents_md(s: &str) -> bool {
310 let trimmed = s.trim();
311 if trimmed.is_empty() {
312 return true;
313 }
314 const PREAMBLE_MARKERS: &[&str] = &[
317 "# AGENTS.md",
318 "Instructions for AI coding agents",
319 "See <https://agentsmd.io>.",
320 ];
321 let has_only_preamble = PREAMBLE_MARKERS.iter().all(|m| trimmed.contains(m))
322 && !trimmed
323 .lines()
324 .any(|l| {
325 let l = l.trim();
326 !l.is_empty()
327 && !l.starts_with('#')
328 && !PREAMBLE_MARKERS.iter().any(|m| l.contains(m))
329 });
330 has_only_preamble
331}
332
333pub fn install_codex_mcp_config() -> Result<bool> {
360 install_codex_mcp_config_at(None)
361}
362
363pub(crate) fn install_codex_mcp_config_at(home_override: Option<&Path>) -> Result<bool> {
369 let path = match home_override {
370 Some(h) => h.join("config.toml"),
371 None => codex_config_path(),
372 };
373
374 let existing = if path.exists() {
377 std::fs::read_to_string(&path).map_err(|e| {
378 SqzError::Other(format!("failed to read {}: {e}", path.display()))
379 })?
380 } else {
381 String::new()
382 };
383
384 let mut doc: toml_edit::DocumentMut = existing
385 .parse()
386 .map_err(|e| SqzError::Other(format!(
387 "failed to parse {} as TOML: {e}",
388 path.display()
389 )))?;
390
391 if let Some(existing_cmd) = doc
396 .get("mcp_servers")
397 .and_then(|v| v.get("sqz"))
398 .and_then(|v| v.get("command"))
399 {
400 if existing_cmd.is_value() {
401 return Ok(false);
402 }
403 }
404
405 let mcp_servers = doc
415 .entry("mcp_servers")
416 .or_insert_with(|| {
417 let mut t = toml_edit::Table::new();
420 t.set_implicit(true);
421 toml_edit::Item::Table(t)
422 })
423 .as_table_mut()
424 .ok_or_else(|| SqzError::Other(format!(
425 "{}: `mcp_servers` is not a table — refusing to overwrite",
426 path.display()
427 )))?;
428
429 let sqz = mcp_servers
430 .entry("sqz")
431 .or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new()))
432 .as_table_mut()
433 .ok_or_else(|| SqzError::Other(format!(
434 "{}: `mcp_servers.sqz` is not a table — refusing to overwrite",
435 path.display()
436 )))?;
437
438 if !sqz.contains_key("command") {
441 sqz["command"] = toml_edit::value("sqz-mcp");
442 }
443 if !sqz.contains_key("args") {
444 let mut args = toml_edit::Array::new();
445 args.push("--transport");
446 args.push("stdio");
447 sqz["args"] = toml_edit::Item::Value(toml_edit::Value::Array(args));
448 }
449
450 if let Some(parent) = path.parent() {
452 std::fs::create_dir_all(parent).map_err(|e| {
453 SqzError::Other(format!(
454 "failed to create {}: {e}",
455 parent.display()
456 ))
457 })?;
458 }
459
460 std::fs::write(&path, doc.to_string()).map_err(|e| {
461 SqzError::Other(format!("failed to write {}: {e}", path.display()))
462 })?;
463 Ok(true)
464}
465
466pub fn remove_codex_mcp_config() -> Result<Option<(PathBuf, bool)>> {
484 remove_codex_mcp_config_at(None)
485}
486
487pub(crate) fn remove_codex_mcp_config_at(
490 home_override: Option<&Path>,
491) -> Result<Option<(PathBuf, bool)>> {
492 let path = match home_override {
493 Some(h) => h.join("config.toml"),
494 None => codex_config_path(),
495 };
496 if !path.exists() {
497 return Ok(None);
498 }
499 let raw = std::fs::read_to_string(&path)
500 .map_err(|e| SqzError::Other(format!("failed to read {}: {e}", path.display())))?;
501
502 let mut doc: toml_edit::DocumentMut = match raw.parse() {
503 Ok(d) => d,
504 Err(_) => {
505 return Ok(Some((path, false)));
507 }
508 };
509
510 let mcp_table = match doc.get_mut("mcp_servers").and_then(|v| v.as_table_mut()) {
511 Some(t) => t,
512 None => return Ok(Some((path, false))),
513 };
514 if !mcp_table.contains_key("sqz") {
515 return Ok(Some((path, false)));
516 }
517 mcp_table.remove("sqz");
518
519 let mcp_is_empty = mcp_table.iter().count() == 0;
522 if mcp_is_empty {
523 doc.remove("mcp_servers");
524 }
525
526 let doc_is_empty = doc.iter().count() == 0;
528 if doc_is_empty {
529 std::fs::remove_file(&path).map_err(|e| {
530 SqzError::Other(format!("failed to remove {}: {e}", path.display()))
531 })?;
532 return Ok(Some((path, true)));
533 }
534
535 std::fs::write(&path, doc.to_string()).map_err(|e| {
536 SqzError::Other(format!("failed to write {}: {e}", path.display()))
537 })?;
538 Ok(Some((path, true)))
539}
540
541#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
552 fn codex_config_path_ends_with_config_toml() {
553 let got = codex_config_path();
554 assert!(
555 got.ends_with("config.toml"),
556 "codex_config_path() must end with config.toml, got: {}",
557 got.display()
558 );
559 }
560
561 #[test]
564 fn agents_md_guidance_block_contains_sqz_invocation() {
565 let block = agents_md_guidance_block("/usr/local/bin/sqz");
566 assert!(block.contains(AGENTS_MD_BEGIN));
567 assert!(block.contains(AGENTS_MD_END));
568 assert!(block.contains("| /usr/local/bin/sqz compress"));
569 assert!(block.contains("sqz-mcp"));
570 }
571
572 #[test]
573 fn install_agents_md_creates_file_with_preamble() {
574 let dir = tempfile::tempdir().unwrap();
575 let created = install_agents_md_guidance(dir.path(), "sqz").unwrap();
576 assert!(created);
577 let content = std::fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
578 assert!(content.starts_with("# AGENTS.md"));
579 assert!(content.contains(AGENTS_MD_BEGIN));
580 assert!(content.contains(AGENTS_MD_END));
581 }
582
583 #[test]
584 fn install_agents_md_appends_without_clobbering_user_content() {
585 let dir = tempfile::tempdir().unwrap();
586 let path = dir.path().join("AGENTS.md");
587 std::fs::write(
588 &path,
589 "# My project rules\n\nBe polite. Run tests before committing.\n",
590 ).unwrap();
591
592 let created = install_agents_md_guidance(dir.path(), "sqz").unwrap();
593 assert!(created);
594
595 let content = std::fs::read_to_string(&path).unwrap();
596 assert!(content.contains("# My project rules"),
597 "original heading must survive");
598 assert!(content.contains("Be polite. Run tests before committing."),
599 "original body must survive");
600 let user_idx = content.find("Be polite").unwrap();
601 let sqz_idx = content.find(AGENTS_MD_BEGIN).unwrap();
602 assert!(sqz_idx > user_idx,
603 "sqz's block must append after user content, not prepend");
604 }
605
606 #[test]
607 fn install_agents_md_is_idempotent() {
608 let dir = tempfile::tempdir().unwrap();
609 let first = install_agents_md_guidance(dir.path(), "sqz").unwrap();
610 assert!(first);
611 let second = install_agents_md_guidance(dir.path(), "sqz").unwrap();
612 assert!(!second, "second install must be a no-op");
613
614 let content = std::fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
615 let occurrences = content.matches(AGENTS_MD_BEGIN).count();
616 assert_eq!(occurrences, 1, "must not duplicate the block on re-install");
617 }
618
619 #[test]
620 fn remove_agents_md_preserves_user_content() {
621 let dir = tempfile::tempdir().unwrap();
622 let path = dir.path().join("AGENTS.md");
623 std::fs::write(
624 &path,
625 "# My project rules\n\nBe polite. Run tests before committing.\n",
626 ).unwrap();
627 install_agents_md_guidance(dir.path(), "sqz").unwrap();
628
629 let (returned_path, changed) =
630 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
631 assert_eq!(returned_path, path);
632 assert!(changed);
633 assert!(path.exists(),
634 "file must NOT be deleted — it has user content");
635
636 let content = std::fs::read_to_string(&path).unwrap();
637 assert!(!content.contains(AGENTS_MD_BEGIN));
638 assert!(!content.contains(AGENTS_MD_END));
639 assert!(content.contains("# My project rules"),
640 "user heading must survive the uninstall");
641 assert!(content.contains("Be polite. Run tests before committing."),
642 "user body must survive the uninstall");
643 }
644
645 #[test]
646 fn remove_agents_md_deletes_file_when_only_sqz_preamble_remains() {
647 let dir = tempfile::tempdir().unwrap();
648 install_agents_md_guidance(dir.path(), "sqz").unwrap();
649 let path = dir.path().join("AGENTS.md");
650 assert!(path.exists());
651
652 let (_returned, changed) =
653 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
654 assert!(changed);
655 assert!(
656 !path.exists(),
657 "fresh-install AGENTS.md must be removed when sqz block is stripped"
658 );
659 }
660
661 #[test]
662 fn remove_agents_md_noop_when_block_missing() {
663 let dir = tempfile::tempdir().unwrap();
664 let path = dir.path().join("AGENTS.md");
665 std::fs::write(&path, "# User-authored, sqz never touched this.\n").unwrap();
666
667 let (returned_path, changed) =
668 remove_agents_md_guidance(dir.path()).unwrap().unwrap();
669 assert_eq!(returned_path, path);
670 assert!(!changed, "no sqz block means no change");
671 assert!(path.exists(), "user file must be untouched");
672 }
673
674 #[test]
675 fn remove_agents_md_returns_none_when_file_missing() {
676 let dir = tempfile::tempdir().unwrap();
677 let result = remove_agents_md_guidance(dir.path()).unwrap();
678 assert!(result.is_none());
679 }
680
681 #[test]
684 fn install_codex_mcp_config_creates_file_with_sqz_entry() {
685 let dir = tempfile::tempdir().unwrap();
686 let created = install_codex_mcp_config_at(Some(dir.path())).unwrap();
687 assert!(created);
688 let content = std::fs::read_to_string(dir.path().join("config.toml")).unwrap();
689 assert!(
690 content.contains("[mcp_servers.sqz]"),
691 "config.toml must contain [mcp_servers.sqz] header; got:\n{content}"
692 );
693 assert!(content.contains("command = \"sqz-mcp\""),
694 "command must be sqz-mcp");
695 assert!(content.contains("--transport"));
696 assert!(content.contains("stdio"));
697 }
698
699 #[test]
700 fn install_codex_mcp_config_preserves_existing_other_servers() {
701 let dir = tempfile::tempdir().unwrap();
702 let cfg = dir.path().join("config.toml");
703 std::fs::write(
704 &cfg,
705 "# User's existing Codex config, with a comment.\n\
706 model = \"gpt-5\"\n\
707 \n\
708 [mcp_servers.other]\n\
709 command = \"other-server\"\n\
710 args = [\"--flag\"]\n",
711 ).unwrap();
712
713 let created = install_codex_mcp_config_at(Some(dir.path())).unwrap();
714 assert!(created);
715
716 let after = std::fs::read_to_string(&cfg).unwrap();
717 assert!(after.contains("# User's existing Codex config"),
718 "comment must survive: {after}");
719 assert!(after.contains("model = \"gpt-5\""),
720 "top-level key must survive: {after}");
721 assert!(after.contains("[mcp_servers.other]"),
722 "existing server entry must survive: {after}");
723 assert!(after.contains("command = \"other-server\""),
724 "existing server command must survive: {after}");
725 assert!(after.contains("[mcp_servers.sqz]"),
726 "sqz entry must be added: {after}");
727 }
728
729 #[test]
730 fn install_codex_mcp_config_is_idempotent() {
731 let dir = tempfile::tempdir().unwrap();
732 assert!(install_codex_mcp_config_at(Some(dir.path())).unwrap());
733 assert!(
734 !install_codex_mcp_config_at(Some(dir.path())).unwrap(),
735 "second install with complete [mcp_servers.sqz] must be a no-op"
736 );
737 }
738
739 #[test]
740 fn install_codex_mcp_config_does_not_overwrite_user_tuned_entry() {
741 let dir = tempfile::tempdir().unwrap();
742 let cfg = dir.path().join("config.toml");
743 std::fs::write(
744 &cfg,
745 "[mcp_servers.sqz]\n\
746 command = \"/custom/path/sqz-mcp\"\n\
747 args = [\"--transport\", \"sse\", \"--port\", \"3999\"]\n",
748 ).unwrap();
749
750 let changed = install_codex_mcp_config_at(Some(dir.path())).unwrap();
751 assert!(!changed, "existing complete entry must be idempotent-skipped");
752 let after = std::fs::read_to_string(&cfg).unwrap();
753 assert!(after.contains("/custom/path/sqz-mcp"),
754 "user's custom command must survive re-init");
755 assert!(after.contains("\"sse\""),
756 "user's custom transport must survive");
757 }
758
759 #[test]
760 fn remove_codex_mcp_config_removes_only_sqz_entry() {
761 let dir = tempfile::tempdir().unwrap();
762 let cfg = dir.path().join("config.toml");
763 std::fs::write(
764 &cfg,
765 "# keep this comment\n\
766 model = \"gpt-5\"\n\
767 \n\
768 [mcp_servers.other]\n\
769 command = \"other-server\"\n\
770 \n\
771 [mcp_servers.sqz]\n\
772 command = \"sqz-mcp\"\n\
773 args = [\"--transport\", \"stdio\"]\n",
774 ).unwrap();
775
776 let (path, changed) = remove_codex_mcp_config_at(Some(dir.path())).unwrap().unwrap();
777 assert_eq!(path, cfg);
778 assert!(changed);
779
780 let after = std::fs::read_to_string(&cfg).unwrap();
781 assert!(after.contains("# keep this comment"),
782 "comment must survive: {after}");
783 assert!(after.contains("model = \"gpt-5\""),
784 "top-level key must survive: {after}");
785 assert!(after.contains("[mcp_servers.other]"),
786 "other server entry must survive: {after}");
787 assert!(!after.contains("[mcp_servers.sqz]"),
788 "sqz entry must be gone: {after}");
789 }
790
791 #[test]
792 fn remove_codex_mcp_config_deletes_file_when_sqz_was_the_only_entry() {
793 let dir = tempfile::tempdir().unwrap();
794 let cfg = dir.path().join("config.toml");
795 install_codex_mcp_config_at(Some(dir.path())).unwrap();
796 let (path, changed) = remove_codex_mcp_config_at(Some(dir.path())).unwrap().unwrap();
797 assert_eq!(path, cfg);
798 assert!(changed);
799 assert!(!cfg.exists(),
800 "config.toml with only sqz must be deleted on uninstall");
801 }
802
803 #[test]
804 fn remove_codex_mcp_config_returns_none_when_file_missing() {
805 let dir = tempfile::tempdir().unwrap();
806 let result = remove_codex_mcp_config_at(Some(dir.path())).unwrap();
807 assert!(result.is_none());
808 }
809
810 #[test]
811 fn remove_codex_mcp_config_noop_when_sqz_entry_missing() {
812 let dir = tempfile::tempdir().unwrap();
813 let cfg = dir.path().join("config.toml");
814 std::fs::write(&cfg, "[mcp_servers.other]\ncommand = \"x\"\n").unwrap();
815 let (path, changed) = remove_codex_mcp_config_at(Some(dir.path())).unwrap().unwrap();
816 assert_eq!(path, cfg);
817 assert!(!changed);
818 let after = std::fs::read_to_string(&cfg).unwrap();
819 assert!(after.contains("[mcp_servers.other]"));
820 }
821}