Skip to content

Commit 042b69c

Browse files
committed
fix: filename timestamp, slug cap, tool content, and capping in exports (Closes #12)
- Add session timestamp prefix to exported filenames (YYYY-MM-DDTHH-MM-SS__) - Remove 60-char cap on title slugs in bulk and single-session export paths - Preserve file read content, glob filename lists, and grep match content from tool results in jsonl_parser.py (previously silently discarded) - Remove all content caps in md_exporter.py: Files Touched list (was 20), Commands Run list (was 30, strings capped at 120), URLs Accessed list (was 15, strings capped at 150), bash stdout (was 2000), stderr (was 1000), Write content (was 500), Edit old/new (was 300), Task prompts (was 500), generic tool inputs (was 500), generic tool result (was 2000) - Fix timestamp format: now renders as YYYY-MM-DD HH:MM:SS.mmm UTC instead of stripping timezone and sub-second precision - Include total_cache_creation_tokens in frontmatter when non-zero Made-with: Cursor
1 parent 178a33a commit 042b69c

3 files changed

Lines changed: 54 additions & 36 deletions

File tree

scripts/export.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -370,21 +370,22 @@ def cmd_export(args):
370370
)
371371
meta["first_timestamp"] = ts
372372
date_str = ts[:10]
373-
title_slug = _slugify(session["title"])[:60]
373+
ts_file = ts[:19].replace(":", "-") # 2026-02-10T01-46-15
374+
title_slug = _slugify(session["title"])
374375
short_id = sid[:8]
375376
project_slug = _slugify(project["name"])
376377

377378
if fmt in ("md", "both"):
378379
md = session_to_markdown(session, stats)
379380
rel_path = os.path.join(
380-
date_str, project_slug, f"{title_slug}__{short_id}.md"
381+
date_str, project_slug, f"{ts_file}__{title_slug}__{short_id}.md"
381382
)
382383
all_exports.append((rel_path, md))
383384

384385
if fmt in ("json", "both"):
385386
js = session_to_json(session, stats)
386387
rel_path = os.path.join(
387-
date_str, project_slug, f"{title_slug}__{short_id}.json"
388+
date_str, project_slug, f"{ts_file}__{title_slug}__{short_id}.json"
388389
)
389390
all_exports.append((rel_path, js))
390391

@@ -448,16 +449,18 @@ def cmd_export(args):
448449

449450
def _export_single(session: dict, stats: dict, fmt: str, out_dir: str):
450451
"""Write one session to disk as md, json, or both."""
451-
title_slug = _slugify(session["title"])[:60]
452+
title_slug = _slugify(session["title"])
452453
short_id = session["session_id"][:8]
454+
ts = session["metadata"].get("first_timestamp", "")
455+
ts_file = ts[:19].replace(":", "-") if ts else "0000-00-00T00-00-00"
453456

454457
files = []
455458
if fmt in ("md", "both"):
456459
md = session_to_markdown(session, stats)
457-
files.append((f"{title_slug}__{short_id}.md", md))
460+
files.append((f"{ts_file}__{title_slug}__{short_id}.md", md))
458461
if fmt in ("json", "both"):
459462
js = session_to_json(session, stats)
460-
files.append((f"{title_slug}__{short_id}.json", js))
463+
files.append((f"{ts_file}__{title_slug}__{short_id}.json", js))
461464

462465
os.makedirs(out_dir, exist_ok=True)
463466
for fname, content in files:

utils/jsonl_parser.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,9 +363,11 @@ def _parse_tool_result(tool_result, slug: str = None) -> dict | None:
363363
tool_result.get("filenames"), list
364364
):
365365
result["result_type"] = "glob"
366-
result["num_files"] = tool_result.get("numFiles", len(tool_result["filenames"]))
366+
filenames = tool_result["filenames"]
367+
result["num_files"] = tool_result.get("numFiles", len(filenames))
367368
result["truncated"] = tool_result.get("truncated", False)
368369
result["duration_ms"] = tool_result.get("durationMs")
370+
result["filenames"] = filenames
369371
return result
370372

371373
# Grep results: have mode + numFiles/numLines
@@ -375,13 +377,20 @@ def _parse_tool_result(tool_result, slug: str = None) -> dict | None:
375377
result["num_files"] = tool_result.get("numFiles", 0)
376378
result["num_lines"] = tool_result.get("numLines", 0)
377379
result["duration_ms"] = tool_result.get("durationMs")
380+
content = tool_result.get("content", "")
381+
if content and isinstance(content, str):
382+
result["content"] = content
378383
return result
379384

380385
# Read result: have file dict with content
381386
if "file" in tool_result and isinstance(tool_result["file"], dict):
387+
file_obj = tool_result["file"]
382388
result["result_type"] = "file_read"
383-
result["file_path"] = tool_result["file"].get("filePath", "")
384-
result["num_lines"] = tool_result["file"].get("numLines")
389+
result["file_path"] = file_obj.get("filePath", "")
390+
result["num_lines"] = file_obj.get("numLines")
391+
content = file_obj.get("content", "")
392+
if content and isinstance(content, str):
393+
result["content"] = content
385394
return result
386395

387396
# WebSearch results

utils/md_exporter.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def _build_frontmatter(session: dict) -> str:
3232
lines.append(f"total_input_tokens: {meta['total_input_tokens']}")
3333
lines.append(f"total_output_tokens: {meta['total_output_tokens']}")
3434
lines.append(f"total_cache_read_tokens: {meta['total_cache_read_tokens']}")
35+
if meta.get("total_cache_creation_tokens", 0) > 0:
36+
lines.append(f"total_cache_creation_tokens: {meta['total_cache_creation_tokens']}")
3537
lines.append(f"total_tool_calls: {meta['total_tool_calls']}")
3638
if meta["tool_call_counts"]:
3739
lines.append("tool_call_breakdown:")
@@ -129,20 +131,18 @@ def _build_summary(session: dict, stats: dict) -> str:
129131
lines.append("| Action | File |")
130132
lines.append("|--------|------|")
131133
for fp in created_files:
132-
lines.append(f"| Create | `{_truncate(fp, 100)}` |")
134+
lines.append(f"| Create | `{fp}` |")
133135
for fp in written_files:
134-
lines.append(f"| Edit | `{_truncate(fp, 100)}` |")
135-
for fp in read_files[:20]:
136-
lines.append(f"| Read | `{_truncate(fp, 100)}` |")
137-
if len(read_files) > 20:
138-
lines.append(f"| Read | _...and {len(read_files) - 20} more_ |")
136+
lines.append(f"| Edit | `{fp}` |")
137+
for fp in read_files:
138+
lines.append(f"| Read | `{fp}` |")
139139
lines.append("")
140140

141141
# Commands run
142142
commands = stats.get("commands_run", [])
143143
if commands:
144144
lines.append("### Commands Run\n")
145-
for i, cmd in enumerate(commands[:30], 1):
145+
for i, cmd in enumerate(commands, 1):
146146
status = ""
147147
if cmd.get("is_error"):
148148
status = " -- **error**"
@@ -152,19 +152,15 @@ def _build_summary(session: dict, stats: dict) -> str:
152152
status = " -- success"
153153
elif cmd.get("return_code_interpretation"):
154154
status = f" -- {cmd['return_code_interpretation']}"
155-
lines.append(f"{i}. `{_truncate(cmd['command'], 120)}`{status}")
156-
if len(commands) > 30:
157-
lines.append(f"\n_...and {len(commands) - 30} more commands_")
155+
lines.append(f"{i}. `{cmd['command']}`{status}")
158156
lines.append("")
159157

160158
# URLs accessed
161159
urls = stats.get("urls_accessed", [])
162160
if urls:
163161
lines.append("### URLs Accessed\n")
164-
for url in urls[:15]:
165-
lines.append(f"- `{_truncate(url, 150)}`")
166-
if len(urls) > 15:
167-
lines.append(f"- _...and {len(urls) - 15} more_")
162+
for url in urls:
163+
lines.append(f"- `{url}`")
168164
lines.append("")
169165

170166
# Tool result summary
@@ -221,7 +217,7 @@ def _render_user(msg: dict) -> str:
221217
tr = msg["tool_result"]
222218
if isinstance(tr, dict):
223219
lines.append("\n**Tool Result:**")
224-
lines.append(f"```\n{_truncate(str(tr), 2000)}\n```")
220+
lines.append(f"```\n{str(tr)}\n```")
225221

226222
lines.append("\n---\n")
227223
return "\n".join(lines)
@@ -281,16 +277,16 @@ def _render_tool_use(tool: dict) -> str:
281277
fp = inp.get("file_path", "")
282278
content = inp.get("content", "")
283279
lines.append(f">\n> File: `{fp}`")
284-
lines.append(f">\n> ```\n> {_truncate(content, 500)}\n> ```")
280+
lines.append(f">\n> ```\n> {content}\n> ```")
285281
elif name == "Edit":
286282
fp = inp.get("file_path", "")
287283
old = inp.get("old_string", "")
288284
new = inp.get("new_string", "")
289285
lines.append(f">\n> File: `{fp}`")
290286
if old:
291-
lines.append(f">\n> Old:\n> ```\n> {_truncate(old, 300)}\n> ```")
287+
lines.append(f">\n> Old:\n> ```\n> {old}\n> ```")
292288
if new:
293-
lines.append(f">\n> New:\n> ```\n> {_truncate(new, 300)}\n> ```")
289+
lines.append(f">\n> New:\n> ```\n> {new}\n> ```")
294290
elif name == "Glob":
295291
lines.append(f">\n> Pattern: `{inp.get('pattern', '')}`")
296292
if inp.get("path"):
@@ -305,7 +301,7 @@ def _render_tool_use(tool: dict) -> str:
305301
lines.append(f">\n> Description: {inp.get('description', '')}")
306302
lines.append(f"> Agent: {inp.get('subagent_type', '')}")
307303
if inp.get("prompt"):
308-
lines.append(f">\n> **Prompt:**\n> ```\n> {_truncate(inp['prompt'], 500)}\n> ```")
304+
lines.append(f">\n> **Prompt:**\n> ```\n> {inp['prompt']}\n> ```")
309305
elif name == "TodoWrite":
310306
todos = inp.get("todos", [])
311307
for t in todos:
@@ -319,10 +315,7 @@ def _render_tool_use(tool: dict) -> str:
319315
for q in questions:
320316
lines.append(f">\n> Q: {q.get('question', '')}")
321317
else:
322-
inp_str = str(inp)
323-
if len(inp_str) > 500:
324-
inp_str = inp_str[:500] + "..."
325-
lines.append(f">\n> Input: `{inp_str}`")
318+
lines.append(f">\n> Input: `{str(inp)}`")
326319

327320
return "\n".join(lines)
328321

@@ -346,15 +339,18 @@ def _render_tool_result(parsed: dict) -> str:
346339

347340
lines.append(f"\n**Bash Result{status}:**")
348341
if stdout:
349-
lines.append(f"```\n{_truncate(stdout, 2000)}\n```")
342+
lines.append(f"```\n{stdout}\n```")
350343
if stderr:
351-
lines.append(f"**stderr:**\n```\n{_truncate(stderr, 1000)}\n```")
344+
lines.append(f"**stderr:**\n```\n{stderr}\n```")
352345

353346
elif rt == "file_read":
354347
fp = parsed.get("file_path", "")
355348
num_lines = parsed.get("num_lines")
356349
detail = f" ({num_lines} lines)" if num_lines else ""
357350
lines.append(f"\n**Read:** `{fp}`{detail}")
351+
content = parsed.get("content", "")
352+
if content:
353+
lines.append(f"```\n{content}\n```")
358354

359355
elif rt == "file_edit":
360356
fp = parsed.get("file_path", "")
@@ -368,11 +364,18 @@ def _render_tool_result(parsed: dict) -> str:
368364
n = parsed.get("num_files", 0)
369365
trunc = " (truncated)" if parsed.get("truncated") else ""
370366
lines.append(f"\n**Glob:** {n} files found{trunc}")
367+
filenames = parsed.get("filenames", [])
368+
if filenames:
369+
for fn in filenames:
370+
lines.append(f"- `{fn}`")
371371

372372
elif rt == "grep":
373373
n = parsed.get("num_files", 0)
374374
nl = parsed.get("num_lines", 0)
375375
lines.append(f"\n**Grep:** {n} files, {nl} lines matched")
376+
content = parsed.get("content", "")
377+
if content:
378+
lines.append(f"```\n{content}\n```")
376379

377380
elif rt == "web_search":
378381
q = parsed.get("query", "")
@@ -434,10 +437,13 @@ def _render_system(msg: dict) -> str:
434437

435438

436439
def _format_ts(ts: str) -> str:
437-
"""2024-01-15T10:30:00Z -> 2024-01-15 10:30:00"""
440+
"""2024-01-15T10:30:00.123Z -> 2024-01-15 10:30:00.123 UTC"""
438441
try:
439442
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
440-
return dt.strftime("%Y-%m-%d %H:%M:%S")
443+
ms = dt.microsecond // 1000
444+
if ms:
445+
return dt.strftime(f"%Y-%m-%d %H:%M:%S.{ms:03d} UTC")
446+
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
441447
except (ValueError, AttributeError):
442448
return ts
443449

0 commit comments

Comments
 (0)