overleaf-comments-export 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/PKG-INFO +51 -8
  2. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/README.md +49 -7
  3. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/__main__.py +76 -2
  4. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/export.py +13 -1
  5. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/gui.py +12 -0
  6. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/render.py +97 -0
  7. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/PKG-INFO +51 -8
  8. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/SOURCES.txt +2 -1
  9. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/pyproject.toml +6 -2
  10. overleaf_comments_export-0.4.0/tests/test_response_letter.py +111 -0
  11. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/LICENSE +0 -0
  12. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/__init__.py +0 -0
  13. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/anchors.py +0 -0
  14. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/client.py +0 -0
  15. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/model.py +0 -0
  16. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export/sections.py +0 -0
  17. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/dependency_links.txt +0 -0
  18. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/entry_points.txt +0 -0
  19. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/requires.txt +0 -0
  20. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/overleaf_comments_export.egg-info/top_level.txt +0 -0
  21. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/setup.cfg +0 -0
  22. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_anchors_sections.py +0 -0
  23. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_client.py +0 -0
  24. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_errors_and_auth.py +0 -0
  25. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_export.py +0 -0
  26. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_render.py +0 -0
  27. {overleaf_comments_export-0.3.0 → overleaf_comments_export-0.4.0}/tests/test_replies_and_changes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overleaf-comments-export
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Export comment threads and tracked changes from an Overleaf project to Markdown + JSON, optimized for AI-agent consumption.
5
5
  Author: Shivang
6
6
  License: MIT
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
22
23
  Classifier: Topic :: Text Processing :: Markup :: LaTeX
23
24
  Classifier: Topic :: Scientific/Engineering
24
25
  Requires-Python: >=3.10
@@ -69,7 +70,13 @@ an Overleaf project URL and a logged-in browser session.
69
70
  pip install overleaf-comments-export
70
71
  ```
71
72
 
72
- Requires Python 3.10+. Works on macOS, Linux, and Windows.
73
+ Requires Python 3.10 or newer (tested up to 3.14). Works on macOS, Linux, and
74
+ Windows.
75
+
76
+ The graphical window needs Python's Tk toolkit, which most Linux distributions
77
+ package separately (`sudo apt install python3-tk` on Debian/Ubuntu). The
78
+ command line never needs it, and `--gui` tells you what to install if it is
79
+ missing.
73
80
 
74
81
  ## Quick start
75
82
 
@@ -104,6 +111,7 @@ In your output folder, by default:
104
111
  | `comments.json` | Structured data — `summary`, top-level `threads`, `files`, `comments`, `tracked_changes`, etc. Schema described in `agents.md`. |
105
112
  | `comments.jsonl` | One self-contained JSON record per comment for streaming/pipelines. |
106
113
  | `agents.md` | A brief instruction file telling an AI agent how to consume the batch. |
114
+ | `response-letter.md` | (Optional, `--response-letter`) A point-by-point reply document, pre-filled with every open comment grouped by who raised it, with blanks for your response. |
107
115
  | `by-reviewer/<name>.md` | (Optional, `--per-reviewer`) One Markdown per reviewer with only their threads. |
108
116
  | `comments.log` | Diagnostic log for the run. |
109
117
 
@@ -121,6 +129,9 @@ overleaf-comments-export --project-url … --out ./out --render-mode detailed
121
129
 
122
130
  # Per-reviewer sub-reports under ./out/by-reviewer/
123
131
  overleaf-comments-export --project-url … --out ./out --per-reviewer
132
+
133
+ # Draft a point-by-point response letter for the open comments
134
+ overleaf-comments-export --project-url … --out ./out --response-letter
124
135
  ```
125
136
 
126
137
  Full flag reference: `overleaf-comments-export --help`.
@@ -162,14 +173,46 @@ Treat it like a password; it stops working when you sign out.
162
173
  | "Could not read Overleaf cookies from chrome" | Use the paste-the-cookie method above. |
163
174
  | "Overleaf could not find that project" | Wrong link, or this account has no access. |
164
175
 
165
- ## Status & maintenance
176
+ ## Feedback, questions, and contributing
177
+
178
+ This tool is actively maintained, and feedback shapes what gets built next.
179
+
180
+ - **Something broke, or the output was wrong?**
181
+ [Open an issue.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
182
+ You do not need to be a programmer — paste what the tool said and that is
183
+ plenty. If Overleaf changes something, everything here stops working at once,
184
+ and you may be the first person to notice.
185
+ - **Want it to do something it doesn't?**
186
+ [Suggest a feature.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
187
+ Tell me what you are trying to do, not just the feature — the real task
188
+ usually leads somewhere better.
189
+ - **Just a question, or want to show what you built with it?**
190
+ [Discussions.](https://github.com/Mangluu/overleaf-comments-export/discussions)
191
+ - **Want to contribute code?** See [CONTRIBUTING.md](CONTRIBUTING.md). It takes
192
+ about two minutes to get the tests running, and there are items marked
193
+ *help wanted* in [ROADMAP.md](ROADMAP.md).
194
+
195
+ Never include your session cookie in an issue — it is a password for your
196
+ Overleaf account, and nobody needs it to fix a bug.
197
+
198
+ Maintained by [Shivang Gupta](https://github.com/Mangluu), who wrote it to deal
199
+ with the review comments on his own papers.
200
+
201
+ ## What's coming next
202
+
203
+ See [ROADMAP.md](ROADMAP.md). Short version: a response-letter scaffold,
204
+ writing out the full source so an AI can see more than a snippet, and a diff
205
+ between two exports so you can work through review comments in waves.
206
+
207
+ Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
166
208
 
167
- This is a personal research utility published in case it's useful to others.
168
- It is provided as-is, with no guaranteed maintenance, no SLA, and no roadmap.
169
- Pull requests are welcome; issues may or may not be acted upon.
209
+ ## A caution
170
210
 
171
- If Overleaf changes their internal API, this tool may stop working until
172
- someone (you?) adapts it.
211
+ This tool uses Overleaf's internal endpoints, which are undocumented and can
212
+ change without notice. It identifies itself honestly in every request, backs
213
+ off when asked to, and only ever reads — it cannot modify your project. Even
214
+ so, it may stop working the day Overleaf changes something. If that happens,
215
+ please say so in an issue.
173
216
 
174
217
  ## License
175
218
 
@@ -33,7 +33,13 @@ an Overleaf project URL and a logged-in browser session.
33
33
  pip install overleaf-comments-export
34
34
  ```
35
35
 
36
- Requires Python 3.10+. Works on macOS, Linux, and Windows.
36
+ Requires Python 3.10 or newer (tested up to 3.14). Works on macOS, Linux, and
37
+ Windows.
38
+
39
+ The graphical window needs Python's Tk toolkit, which most Linux distributions
40
+ package separately (`sudo apt install python3-tk` on Debian/Ubuntu). The
41
+ command line never needs it, and `--gui` tells you what to install if it is
42
+ missing.
37
43
 
38
44
  ## Quick start
39
45
 
@@ -68,6 +74,7 @@ In your output folder, by default:
68
74
  | `comments.json` | Structured data — `summary`, top-level `threads`, `files`, `comments`, `tracked_changes`, etc. Schema described in `agents.md`. |
69
75
  | `comments.jsonl` | One self-contained JSON record per comment for streaming/pipelines. |
70
76
  | `agents.md` | A brief instruction file telling an AI agent how to consume the batch. |
77
+ | `response-letter.md` | (Optional, `--response-letter`) A point-by-point reply document, pre-filled with every open comment grouped by who raised it, with blanks for your response. |
71
78
  | `by-reviewer/<name>.md` | (Optional, `--per-reviewer`) One Markdown per reviewer with only their threads. |
72
79
  | `comments.log` | Diagnostic log for the run. |
73
80
 
@@ -85,6 +92,9 @@ overleaf-comments-export --project-url … --out ./out --render-mode detailed
85
92
 
86
93
  # Per-reviewer sub-reports under ./out/by-reviewer/
87
94
  overleaf-comments-export --project-url … --out ./out --per-reviewer
95
+
96
+ # Draft a point-by-point response letter for the open comments
97
+ overleaf-comments-export --project-url … --out ./out --response-letter
88
98
  ```
89
99
 
90
100
  Full flag reference: `overleaf-comments-export --help`.
@@ -126,14 +136,46 @@ Treat it like a password; it stops working when you sign out.
126
136
  | "Could not read Overleaf cookies from chrome" | Use the paste-the-cookie method above. |
127
137
  | "Overleaf could not find that project" | Wrong link, or this account has no access. |
128
138
 
129
- ## Status & maintenance
139
+ ## Feedback, questions, and contributing
140
+
141
+ This tool is actively maintained, and feedback shapes what gets built next.
142
+
143
+ - **Something broke, or the output was wrong?**
144
+ [Open an issue.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
145
+ You do not need to be a programmer — paste what the tool said and that is
146
+ plenty. If Overleaf changes something, everything here stops working at once,
147
+ and you may be the first person to notice.
148
+ - **Want it to do something it doesn't?**
149
+ [Suggest a feature.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
150
+ Tell me what you are trying to do, not just the feature — the real task
151
+ usually leads somewhere better.
152
+ - **Just a question, or want to show what you built with it?**
153
+ [Discussions.](https://github.com/Mangluu/overleaf-comments-export/discussions)
154
+ - **Want to contribute code?** See [CONTRIBUTING.md](CONTRIBUTING.md). It takes
155
+ about two minutes to get the tests running, and there are items marked
156
+ *help wanted* in [ROADMAP.md](ROADMAP.md).
157
+
158
+ Never include your session cookie in an issue — it is a password for your
159
+ Overleaf account, and nobody needs it to fix a bug.
160
+
161
+ Maintained by [Shivang Gupta](https://github.com/Mangluu), who wrote it to deal
162
+ with the review comments on his own papers.
163
+
164
+ ## What's coming next
165
+
166
+ See [ROADMAP.md](ROADMAP.md). Short version: a response-letter scaffold,
167
+ writing out the full source so an AI can see more than a snippet, and a diff
168
+ between two exports so you can work through review comments in waves.
169
+
170
+ Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
130
171
 
131
- This is a personal research utility published in case it's useful to others.
132
- It is provided as-is, with no guaranteed maintenance, no SLA, and no roadmap.
133
- Pull requests are welcome; issues may or may not be acted upon.
172
+ ## A caution
134
173
 
135
- If Overleaf changes their internal API, this tool may stop working until
136
- someone (you?) adapts it.
174
+ This tool uses Overleaf's internal endpoints, which are undocumented and can
175
+ change without notice. It identifies itself honestly in every request, backs
176
+ off when asked to, and only ever reads — it cannot modify your project. Even
177
+ so, it may stop working the day Overleaf changes something. If that happens,
178
+ please say so in an issue.
137
179
 
138
180
  ## License
139
181
 
@@ -4,6 +4,7 @@ import argparse
4
4
  import logging
5
5
  import os
6
6
  import sys
7
+ import traceback
7
8
  from pathlib import Path
8
9
 
9
10
  from . import __version__
@@ -11,6 +12,35 @@ from .client import OverleafClient, UserFacingError
11
12
  from .export import ExportResult, run_export
12
13
 
13
14
 
15
+ def _no_tkinter_message() -> str:
16
+ """Platform-specific instructions for installing Python's GUI toolkit."""
17
+ if sys.platform == "darwin":
18
+ fix = (
19
+ "Install Python from python.org (it includes the GUI toolkit), or\n"
20
+ "with Homebrew run:\n\n"
21
+ " brew install python-tk"
22
+ )
23
+ elif sys.platform.startswith("win"):
24
+ fix = (
25
+ "Re-run the Python installer from python.org, choose \"Modify\", and\n"
26
+ "tick \"tcl/tk and IDLE\"."
27
+ )
28
+ else:
29
+ fix = (
30
+ "Install it with your package manager, for example:\n\n"
31
+ " Debian/Ubuntu : sudo apt install python3-tk\n"
32
+ " Fedora : sudo dnf install python3-tkinter\n"
33
+ " Arch : sudo pacman -S tk"
34
+ )
35
+ return (
36
+ "The window cannot open because this Python has no GUI toolkit "
37
+ "installed.\n\n"
38
+ f"{fix}\n\n"
39
+ "Or skip the window entirely and use the command line:\n\n"
40
+ " overleaf-comments-export --project-url <your project link> --out ./comments"
41
+ )
42
+
43
+
14
44
  def main(argv: list[str] | None = None) -> int:
15
45
  parser = argparse.ArgumentParser(
16
46
  prog="overleaf-comments-export",
@@ -106,6 +136,13 @@ def main(argv: list[str] | None = None) -> int:
106
136
  action="store_false",
107
137
  help="Skip writing comments.jsonl (the streaming-friendly companion).",
108
138
  )
139
+ parser.add_argument(
140
+ "--response-letter",
141
+ action="store_true",
142
+ help="Also write response-letter.md: a point-by-point reply document "
143
+ "pre-filled with every open comment, grouped by who raised it, with "
144
+ "blanks for your response.",
145
+ )
109
146
  parser.add_argument(
110
147
  "--per-reviewer",
111
148
  action="store_true",
@@ -120,8 +157,31 @@ def main(argv: list[str] | None = None) -> int:
120
157
  args = parser.parse_args(argv)
121
158
 
122
159
  if args.gui or (not args.project_url and not args.out):
123
- from .gui import launch_gui
124
- return launch_gui()
160
+ try:
161
+ from .gui import launch_gui
162
+ except ImportError:
163
+ # tkinter is NOT bundled with Python everywhere — most Linux
164
+ # distributions ship it as a separate system package, and it is
165
+ # missing from some minimal/conda builds.
166
+ print(_no_tkinter_message(), file=sys.stderr)
167
+ return 1
168
+ try:
169
+ return launch_gui()
170
+ except Exception as e:
171
+ # Typically TclError on a headless machine (SSH, server, container).
172
+ if "display" in str(e).lower() or type(e).__name__ == "TclError":
173
+ print(
174
+ "There is no screen to open a window on.\n\n"
175
+ "This looks like a computer without a desktop (a server, or "
176
+ "a remote session). Use the command line instead, for "
177
+ "example:\n\n"
178
+ " overleaf-comments-export --project-url <your project link> "
179
+ "--out ./comments\n\n"
180
+ "Run with --help to see every option.",
181
+ file=sys.stderr,
182
+ )
183
+ return 1
184
+ raise
125
185
 
126
186
  if not args.project_url or not args.out:
127
187
  parser.error("--project-url and --out are required in CLI mode (or pass --gui).")
@@ -151,12 +211,26 @@ def main(argv: list[str] | None = None) -> int:
151
211
  render_mode=args.render_mode,
152
212
  write_jsonl=args.write_jsonl,
153
213
  per_reviewer_reports=args.per_reviewer,
214
+ response_letter=args.response_letter,
154
215
  progress=lambda msg: print(msg, file=sys.stderr),
155
216
  )
156
217
  except UserFacingError as e:
157
218
  # Expected, explainable failures: no traceback, just what to do next.
158
219
  print(f"\n{e}", file=sys.stderr)
159
220
  return 1
221
+ except Exception:
222
+ # Unexpected: show the traceback, but also tell people where to send it.
223
+ # The moment something breaks is the only moment we have their attention.
224
+ traceback.print_exc()
225
+ print(
226
+ f"\nThat looks like a bug in overleaf-comments-export {__version__}.\n"
227
+ "Please report it (copy the lines above) at\n"
228
+ " https://github.com/Mangluu/overleaf-comments-export/issues/new/choose\n"
229
+ "It probably affects other people too, and it cannot be fixed if "
230
+ "nobody says anything.",
231
+ file=sys.stderr,
232
+ )
233
+ return 2
160
234
  print(f"\nDone. Open: {result.markdown_path}")
161
235
  return 0
162
236
 
@@ -19,7 +19,7 @@ from .model import (
19
19
  Thread,
20
20
  TrackedChange,
21
21
  )
22
- from .render import render_markdown
22
+ from .render import render_markdown, render_response_letter
23
23
  from .sections import find_headings, nearest_heading
24
24
 
25
25
  SCHEMA_VERSION = "1.3"
@@ -76,6 +76,7 @@ class ExportResult:
76
76
  jsonl_path: Path | None = None
77
77
  by_reviewer_dir: Path | None = None
78
78
  agents_path: Path | None = None
79
+ response_letter_path: Path | None = None
79
80
 
80
81
 
81
82
  def _build_user_map(threads_raw: dict[str, Any]) -> dict[str, dict[str, str]]:
@@ -340,6 +341,7 @@ def run_export(
340
341
  render_mode: str = "compact",
341
342
  write_jsonl: bool = True,
342
343
  per_reviewer_reports: bool = False,
344
+ response_letter: bool = False,
343
345
  progress: ProgressCallback | None = None,
344
346
  ) -> ExportResult:
345
347
  """Programmatic entry point used by both the CLI and the GUI."""
@@ -668,6 +670,15 @@ def run_export(
668
670
  written += 1
669
671
  progress(f"Wrote {written} per-reviewer report(s) into by-reviewer/")
670
672
 
673
+ letter_path: Path | None = None
674
+ if response_letter:
675
+ letter_path = out_dir / "response-letter.md"
676
+ letter_path.write_text(
677
+ render_response_letter(title, project_id, threads, anchored),
678
+ encoding="utf-8",
679
+ )
680
+ progress(f"Wrote {letter_path.name}")
681
+
671
682
  agents_path = out_dir / "agents.md"
672
683
  agents_path.write_text(_build_agents_md(title, project_id, json_path.name, md_path.name), encoding="utf-8")
673
684
  progress(f"Wrote {agents_path.name}")
@@ -688,6 +699,7 @@ def run_export(
688
699
  jsonl_path=(out_dir / "comments.jsonl") if write_jsonl else None,
689
700
  by_reviewer_dir=(out_dir / "by-reviewer") if per_reviewer_reports else None,
690
701
  agents_path=agents_path,
702
+ response_letter_path=letter_path,
691
703
  )
692
704
 
693
705
 
@@ -425,6 +425,9 @@ class App:
425
425
  self.per_reviewer_var = tk.BooleanVar(
426
426
  value=bool(self.config.get("per_reviewer_reports", False))
427
427
  )
428
+ self.response_letter_var = tk.BooleanVar(
429
+ value=bool(self.config.get("response_letter", False))
430
+ )
428
431
  self.include_raw_var = tk.BooleanVar(
429
432
  value=bool(self.config.get("include_raw", False))
430
433
  )
@@ -433,6 +436,11 @@ class App:
433
436
  text="comments.jsonl (streaming companion)",
434
437
  variable=self.write_jsonl_var,
435
438
  ).pack(anchor="w")
439
+ ttk.Checkbutton(
440
+ extras_row,
441
+ text="Response letter draft (response-letter.md)",
442
+ variable=self.response_letter_var,
443
+ ).pack(anchor="w")
436
444
  ttk.Checkbutton(
437
445
  extras_row,
438
446
  text="Per-reviewer reports (by-reviewer/<name>.md)",
@@ -579,6 +587,7 @@ class App:
579
587
  "render_mode": self.render_mode_var.get(),
580
588
  "write_jsonl": bool(self.write_jsonl_var.get()),
581
589
  "per_reviewer_reports": bool(self.per_reviewer_var.get()),
590
+ "response_letter": bool(self.response_letter_var.get()),
582
591
  "include_raw": bool(self.include_raw_var.get()),
583
592
  }
584
593
  )
@@ -603,6 +612,7 @@ class App:
603
612
  render_mode=self.render_mode_var.get(),
604
613
  write_jsonl=bool(self.write_jsonl_var.get()),
605
614
  per_reviewer_reports=bool(self.per_reviewer_var.get()),
615
+ response_letter=bool(self.response_letter_var.get()),
606
616
  include_raw=bool(self.include_raw_var.get()),
607
617
  )
608
618
  self.worker = threading.Thread(
@@ -653,6 +663,8 @@ class App:
653
663
  self._append_log(f"JSONL: {result.jsonl_path}")
654
664
  if result.agents_path is not None:
655
665
  self._append_log(f"Agents: {result.agents_path}")
666
+ if result.response_letter_path is not None:
667
+ self._append_log(f"Letter: {result.response_letter_path}")
656
668
  if result.by_reviewer_dir is not None:
657
669
  self._append_log(f"Per-reviewer: {result.by_reviewer_dir}")
658
670
 
@@ -277,6 +277,103 @@ def render_markdown(
277
277
  return "\n".join(out).rstrip() + "\n"
278
278
 
279
279
 
280
+ def render_response_letter(
281
+ project_title: str,
282
+ project_id: str,
283
+ threads: dict[str, Thread],
284
+ anchored: list[AnchoredComment],
285
+ ) -> str:
286
+ """A point-by-point reply document, pre-filled with every open comment.
287
+
288
+ Grouped by the person who raised each point, because that is how journals
289
+ ask for rebuttals. Each entry carries its `C###` id so it can be traced
290
+ back to the full export.
291
+ """
292
+ out: list[str] = []
293
+ pulled = datetime.now(timezone.utc).strftime("%Y-%m-%d")
294
+
295
+ # Only points that still need answering, keyed by whoever raised them.
296
+ by_reviewer: dict[str, list[AnchoredComment]] = defaultdict(list)
297
+ for c in anchored:
298
+ thread = threads.get(c.thread_id)
299
+ if thread is None or thread.resolved or not thread.messages:
300
+ continue
301
+ first = min(thread.messages, key=lambda m: m.timestamp_ms)
302
+ who = _humanize_user(first.user_name, first.user_email, first.user_id)
303
+ by_reviewer[who].append(c)
304
+
305
+ total = sum(len(v) for v in by_reviewer.values())
306
+
307
+ out.append(f"# Response to reviewers — {project_title}")
308
+ out.append("")
309
+ out.append(f"_Draft generated {pulled}. {total} point(s) to address._")
310
+ out.append("")
311
+ out.append(
312
+ "Fill in the **Response** and **Change made** lines under each point. "
313
+ "The `C###` ids match `comments.json`, so you can ask an AI assistant "
314
+ "to draft any of them by id."
315
+ )
316
+ out.append("")
317
+ out.append("---")
318
+ out.append("")
319
+
320
+ if not total:
321
+ out.append("No open comments. Nothing to respond to.")
322
+ out.append("")
323
+ return "\n".join(out).rstrip() + "\n"
324
+
325
+ out.append("## Summary of changes")
326
+ out.append("")
327
+ out.append("_A short paragraph on the main revisions goes here._")
328
+ out.append("")
329
+
330
+ for reviewer in sorted(by_reviewer):
331
+ items = by_reviewer[reviewer]
332
+ out.append(f"## {reviewer} — {len(items)} point(s)")
333
+ out.append("")
334
+ for c in items:
335
+ thread = threads[c.thread_id]
336
+ ordered = sorted(thread.messages, key=lambda m: m.timestamp_ms)
337
+ where = c.nearest_heading or "no enclosing section"
338
+ # Don't leak "<unknown-6a21dec…>" into a document someone sends to
339
+ # an editor; the line number alone is enough there.
340
+ if c.pathname.startswith("<unknown-"):
341
+ locus = f"line {c.line_no}"
342
+ else:
343
+ locus = f"`{c.pathname}` line {c.line_no}"
344
+ out.append(f"### {c.short_id} — § {where} ({locus})")
345
+ out.append("")
346
+ quote = (c.anchored_text or "").strip().replace("\n", " ")
347
+ if quote:
348
+ out.append(f"**Referring to:** “{quote}”")
349
+ out.append("")
350
+ out.append("**Comment:**")
351
+ out.append("")
352
+ for line in (ordered[0].content or "").strip().splitlines() or [""]:
353
+ out.append(f"> {line}")
354
+ out.append("")
355
+ if len(ordered) > 1:
356
+ out.append("**Discussion so far:**")
357
+ out.append("")
358
+ for msg in ordered[1:]:
359
+ who = _humanize_user(msg.user_name, msg.user_email, msg.user_id)
360
+ body = (msg.content or "").strip().replace("\n", " ")
361
+ out.append(f"> ↳ {who}: {body}")
362
+ out.append("")
363
+ out.append("**Response:**")
364
+ out.append("")
365
+ out.append("_TODO_")
366
+ out.append("")
367
+ out.append("**Change made:**")
368
+ out.append("")
369
+ out.append("_TODO — what changed, and where._")
370
+ out.append("")
371
+ out.append("---")
372
+ out.append("")
373
+
374
+ return "\n".join(out).rstrip() + "\n"
375
+
376
+
280
377
  def _status_badge(thread: Thread | None, stale: bool) -> str:
281
378
  bits = []
282
379
  if thread and thread.resolved:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overleaf-comments-export
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Export comment threads and tracked changes from an Overleaf project to Markdown + JSON, optimized for AI-agent consumption.
5
5
  Author: Shivang
6
6
  License: MIT
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
22
23
  Classifier: Topic :: Text Processing :: Markup :: LaTeX
23
24
  Classifier: Topic :: Scientific/Engineering
24
25
  Requires-Python: >=3.10
@@ -69,7 +70,13 @@ an Overleaf project URL and a logged-in browser session.
69
70
  pip install overleaf-comments-export
70
71
  ```
71
72
 
72
- Requires Python 3.10+. Works on macOS, Linux, and Windows.
73
+ Requires Python 3.10 or newer (tested up to 3.14). Works on macOS, Linux, and
74
+ Windows.
75
+
76
+ The graphical window needs Python's Tk toolkit, which most Linux distributions
77
+ package separately (`sudo apt install python3-tk` on Debian/Ubuntu). The
78
+ command line never needs it, and `--gui` tells you what to install if it is
79
+ missing.
73
80
 
74
81
  ## Quick start
75
82
 
@@ -104,6 +111,7 @@ In your output folder, by default:
104
111
  | `comments.json` | Structured data — `summary`, top-level `threads`, `files`, `comments`, `tracked_changes`, etc. Schema described in `agents.md`. |
105
112
  | `comments.jsonl` | One self-contained JSON record per comment for streaming/pipelines. |
106
113
  | `agents.md` | A brief instruction file telling an AI agent how to consume the batch. |
114
+ | `response-letter.md` | (Optional, `--response-letter`) A point-by-point reply document, pre-filled with every open comment grouped by who raised it, with blanks for your response. |
107
115
  | `by-reviewer/<name>.md` | (Optional, `--per-reviewer`) One Markdown per reviewer with only their threads. |
108
116
  | `comments.log` | Diagnostic log for the run. |
109
117
 
@@ -121,6 +129,9 @@ overleaf-comments-export --project-url … --out ./out --render-mode detailed
121
129
 
122
130
  # Per-reviewer sub-reports under ./out/by-reviewer/
123
131
  overleaf-comments-export --project-url … --out ./out --per-reviewer
132
+
133
+ # Draft a point-by-point response letter for the open comments
134
+ overleaf-comments-export --project-url … --out ./out --response-letter
124
135
  ```
125
136
 
126
137
  Full flag reference: `overleaf-comments-export --help`.
@@ -162,14 +173,46 @@ Treat it like a password; it stops working when you sign out.
162
173
  | "Could not read Overleaf cookies from chrome" | Use the paste-the-cookie method above. |
163
174
  | "Overleaf could not find that project" | Wrong link, or this account has no access. |
164
175
 
165
- ## Status & maintenance
176
+ ## Feedback, questions, and contributing
177
+
178
+ This tool is actively maintained, and feedback shapes what gets built next.
179
+
180
+ - **Something broke, or the output was wrong?**
181
+ [Open an issue.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
182
+ You do not need to be a programmer — paste what the tool said and that is
183
+ plenty. If Overleaf changes something, everything here stops working at once,
184
+ and you may be the first person to notice.
185
+ - **Want it to do something it doesn't?**
186
+ [Suggest a feature.](https://github.com/Mangluu/overleaf-comments-export/issues/new/choose)
187
+ Tell me what you are trying to do, not just the feature — the real task
188
+ usually leads somewhere better.
189
+ - **Just a question, or want to show what you built with it?**
190
+ [Discussions.](https://github.com/Mangluu/overleaf-comments-export/discussions)
191
+ - **Want to contribute code?** See [CONTRIBUTING.md](CONTRIBUTING.md). It takes
192
+ about two minutes to get the tests running, and there are items marked
193
+ *help wanted* in [ROADMAP.md](ROADMAP.md).
194
+
195
+ Never include your session cookie in an issue — it is a password for your
196
+ Overleaf account, and nobody needs it to fix a bug.
197
+
198
+ Maintained by [Shivang Gupta](https://github.com/Mangluu), who wrote it to deal
199
+ with the review comments on his own papers.
200
+
201
+ ## What's coming next
202
+
203
+ See [ROADMAP.md](ROADMAP.md). Short version: a response-letter scaffold,
204
+ writing out the full source so an AI can see more than a snippet, and a diff
205
+ between two exports so you can work through review comments in waves.
206
+
207
+ Changes are recorded in [CHANGELOG.md](CHANGELOG.md).
166
208
 
167
- This is a personal research utility published in case it's useful to others.
168
- It is provided as-is, with no guaranteed maintenance, no SLA, and no roadmap.
169
- Pull requests are welcome; issues may or may not be acted upon.
209
+ ## A caution
170
210
 
171
- If Overleaf changes their internal API, this tool may stop working until
172
- someone (you?) adapts it.
211
+ This tool uses Overleaf's internal endpoints, which are undocumented and can
212
+ change without notice. It identifies itself honestly in every request, backs
213
+ off when asked to, and only ever reads — it cannot modify your project. Even
214
+ so, it may stop working the day Overleaf changes something. If that happens,
215
+ please say so in an issue.
173
216
 
174
217
  ## License
175
218
 
@@ -21,4 +21,5 @@ tests/test_client.py
21
21
  tests/test_errors_and_auth.py
22
22
  tests/test_export.py
23
23
  tests/test_render.py
24
- tests/test_replies_and_changes.py
24
+ tests/test_replies_and_changes.py
25
+ tests/test_response_letter.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "overleaf-comments-export"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Export comment threads and tracked changes from an Overleaf project to Markdown + JSON, optimized for AI-agent consumption."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -34,6 +34,7 @@ classifiers = [
34
34
  "Programming Language :: Python :: 3.11",
35
35
  "Programming Language :: Python :: 3.12",
36
36
  "Programming Language :: Python :: 3.13",
37
+ "Programming Language :: Python :: 3.14",
37
38
  "Topic :: Text Processing :: Markup :: LaTeX",
38
39
  "Topic :: Scientific/Engineering",
39
40
  ]
@@ -46,7 +47,10 @@ dependencies = [
46
47
 
47
48
  [project.optional-dependencies]
48
49
  gui = [
49
- # tkinter ships with Python; sv-ttk adds a modern look-and-feel.
50
+ # NOTE: tkinter itself cannot be installed from PyPI. It ships with the
51
+ # python.org installers, but most Linux distributions package it
52
+ # separately (python3-tk / python3-tkinter / tk). The CLI never needs it,
53
+ # and `--gui` explains how to install it if it is missing.
50
54
  "sv-ttk>=2.6",
51
55
  ]
52
56
  test = [
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from overleaf_comments_export.model import (
4
+ AnchoredComment,
5
+ Message,
6
+ SourceContext,
7
+ Thread,
8
+ )
9
+ from overleaf_comments_export.render import render_response_letter
10
+
11
+
12
+ def _thread(tid, author, body, resolved=False, reply=None):
13
+ msgs = [
14
+ Message(id=f"{tid}m1", content=body, timestamp_ms=1_000, user_id=author,
15
+ user_name=author.title(), user_email=f"{author}@x.com")
16
+ ]
17
+ if reply:
18
+ msgs.append(
19
+ Message(id=f"{tid}m2", content=reply, timestamp_ms=2_000, user_id="co",
20
+ user_name="Co Author", user_email="co@x.com")
21
+ )
22
+ return Thread(id=tid, messages=msgs, resolved=resolved)
23
+
24
+
25
+ def _comment(tid, short_id, line=10, heading="Introduction", anchor="some phrase"):
26
+ return AnchoredComment(
27
+ thread_id=tid, short_id=short_id, doc_id="d1", pathname="main.tex",
28
+ offset=0, anchored_text=anchor, line_no=line, col=0,
29
+ nearest_heading=heading, stale=False,
30
+ context=SourceContext(before="a", anchor=anchor, after="b", line_no=line),
31
+ )
32
+
33
+
34
+ def test_letter_has_a_slot_for_every_open_comment():
35
+ threads = {
36
+ "t1": _thread("t1", "emma", "needs a citation"),
37
+ "t2": _thread("t2", "emma", "unclear phrasing"),
38
+ }
39
+ md = render_response_letter(
40
+ "My Paper", "abc", threads, [_comment("t1", "C001"), _comment("t2", "C002")]
41
+ )
42
+ assert "C001" in md and "C002" in md
43
+ assert md.count("**Response:**") == 2
44
+ assert md.count("**Change made:**") == 2
45
+ assert "2 point(s) to address" in md
46
+
47
+
48
+ def test_letter_groups_by_who_raised_the_point():
49
+ threads = {
50
+ "t1": _thread("t1", "emma", "point one"),
51
+ "t2": _thread("t2", "xinyi", "point two"),
52
+ }
53
+ md = render_response_letter(
54
+ "P", "abc", threads, [_comment("t1", "C001"), _comment("t2", "C002")]
55
+ )
56
+ assert "## Emma — 1 point(s)" in md
57
+ assert "## Xinyi — 1 point(s)" in md
58
+
59
+
60
+ def test_letter_skips_resolved_comments():
61
+ threads = {
62
+ "t1": _thread("t1", "emma", "already handled", resolved=True),
63
+ "t2": _thread("t2", "emma", "still open"),
64
+ }
65
+ md = render_response_letter(
66
+ "P", "abc", threads, [_comment("t1", "C001"), _comment("t2", "C002")]
67
+ )
68
+ assert "C002" in md
69
+ assert "C001" not in md
70
+ assert "1 point(s) to address" in md
71
+
72
+
73
+ def test_letter_includes_the_discussion_so_far():
74
+ threads = {"t1": _thread("t1", "emma", "the ask", reply="we could cite Smith")}
75
+ md = render_response_letter("P", "abc", threads, [_comment("t1", "C001")])
76
+ assert "**Discussion so far:**" in md
77
+ assert "we could cite Smith" in md
78
+ assert "↳ Co Author" in md
79
+
80
+
81
+ def test_letter_carries_location_and_quote():
82
+ threads = {"t1": _thread("t1", "emma", "vague")}
83
+ md = render_response_letter(
84
+ "P", "abc", threads,
85
+ [_comment("t1", "C001", line=42, heading="Method", anchor="novel framework")],
86
+ )
87
+ assert "§ Method" in md
88
+ assert "`main.tex` line 42" in md
89
+ assert "“novel framework”" in md
90
+
91
+
92
+ def test_letter_with_nothing_open_says_so():
93
+ threads = {"t1": _thread("t1", "emma", "done", resolved=True)}
94
+ md = render_response_letter("P", "abc", threads, [_comment("t1", "C001")])
95
+ assert "No open comments" in md
96
+ assert "**Response:**" not in md
97
+
98
+
99
+ def test_letter_survives_a_comment_whose_thread_is_missing():
100
+ md = render_response_letter("P", "abc", {}, [_comment("gone", "C001")])
101
+ assert "No open comments" in md
102
+
103
+
104
+ def test_letter_hides_unmapped_filenames():
105
+ """A reader of the letter should never see "<unknown-6a21dec…>"."""
106
+ threads = {"t1": _thread("t1", "emma", "vague")}
107
+ c = _comment("t1", "C001", line=42)
108
+ c.pathname = "<unknown-6a21dec6cb39c30917f5c477>"
109
+ md = render_response_letter("P", "abc", threads, [c])
110
+ assert "unknown-" not in md
111
+ assert "line 42" in md