inscript 0.4.0__tar.gz → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Universal agent activity ledger. Records what AI agents do.
5
5
  Author: Andrew Park
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Universal agent activity ledger. Records what AI agents do.
5
5
  Author: Andrew Park
6
6
  License-Expression: MIT
@@ -17,7 +17,7 @@ import time
17
17
  from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
 
20
- __version__ = "0.4.0"
20
+ __version__ = "0.5.0"
21
21
 
22
22
  INSCRIPT_DIR = Path.home() / ".inscript"
23
23
  ACTIVE_PROJECT_FILE = INSCRIPT_DIR / "active_project"
@@ -86,6 +86,15 @@ def session_dir(session_id: str) -> Path:
86
86
  return SESSIONS_DIR / session_id
87
87
 
88
88
 
89
+ def _format_tokens(n: int) -> str:
90
+ """Format token count: 1234 -> 1.2k, 1234567 -> 1.2M."""
91
+ if n >= 1_000_000:
92
+ return f"{n / 1_000_000:.1f}M"
93
+ elif n >= 1_000:
94
+ return f"{n / 1_000:.1f}k"
95
+ return str(n)
96
+
97
+
89
98
  def list_sessions() -> list[dict]:
90
99
  sessions = []
91
100
  if not SESSIONS_DIR.exists():
@@ -279,7 +288,20 @@ def _cmd_log(session_id: str | None):
279
288
  except json.JSONDecodeError:
280
289
  pass
281
290
 
282
- print(f" {len(files_seen)} files, {edits} edits, {len(prompts)} prompts")
291
+ # Show token usage if summary exists
292
+ summary_file = sdir / "summary.json"
293
+ token_str = ""
294
+ if summary_file.exists():
295
+ try:
296
+ s = json.loads(summary_file.read_text())
297
+ tokens = s.get("tokens")
298
+ if tokens:
299
+ total = tokens.get("total_tokens", 0)
300
+ token_str = f", {_format_tokens(total)} tokens"
301
+ except (json.JSONDecodeError, OSError):
302
+ pass
303
+
304
+ print(f" {len(files_seen)} files, {edits} edits, {len(prompts)} prompts{token_str}")
283
305
 
284
306
 
285
307
  def _cmd_tag(tag_name: str | None):
@@ -529,10 +551,18 @@ def _cmd_export(session_id: str):
529
551
  if summary_file.exists():
530
552
  s = json.loads(summary_file.read_text())
531
553
  print(f"- **Ended**: {s.get('end_time', '?')}")
554
+ if s.get("duration_seconds"):
555
+ print(f"- **Duration**: {_format_duration(s['duration_seconds'])}")
532
556
  print(f"- **Prompts**: {s.get('prompts', '?')}")
533
557
  print(f"- **Files read**: {s.get('files_read', '?')}")
534
558
  print(f"- **Files written**: {s.get('files_written', '?')}")
535
559
  print(f"- **Total edits**: {s.get('total_edits', '?')}")
560
+ tokens = s.get("tokens")
561
+ if tokens:
562
+ print(f"- **Model**: {tokens.get('model', '?')}")
563
+ print(f"- **Tokens**: {_format_tokens(tokens.get('total_tokens', 0))} total ({_format_tokens(tokens.get('input_tokens', 0))} in, {_format_tokens(tokens.get('output_tokens', 0))} out)")
564
+ if tokens.get("cache_read_tokens"):
565
+ print(f"- **Cache**: {_format_tokens(tokens['cache_read_tokens'])} read, {_format_tokens(tokens.get('cache_write_tokens', 0))} written")
536
566
 
537
567
  # Load prompts, touches, and diffs
538
568
  prompts = _load_prompts(sdir)
@@ -137,6 +137,12 @@ def handle_session_start(data: dict) -> None:
137
137
  "project": proj,
138
138
  "status": "active",
139
139
  }
140
+
141
+ # Save transcript path if provided (for token counting on Stop)
142
+ transcript_path = data.get("transcript_path")
143
+ if transcript_path:
144
+ meta["transcript_path"] = transcript_path
145
+
140
146
  (sdir / "meta.json").write_text(json.dumps(meta, indent=2))
141
147
 
142
148
  # Mark as active session
@@ -216,17 +222,23 @@ def handle_post_tool_use(data: dict) -> None:
216
222
 
217
223
  sdir = SESSIONS_DIR / session_id
218
224
 
219
- # Update session meta with project if it changed
220
- if root:
221
- meta_file = sdir / "meta.json"
222
- if meta_file.exists():
223
- try:
224
- meta = json.loads(meta_file.read_text())
225
- if meta.get("project") != str(root):
226
- meta["project"] = str(root)
227
- meta_file.write_text(json.dumps(meta, indent=2))
228
- except (json.JSONDecodeError, OSError):
229
- pass
225
+ # Update session meta with project + transcript_path if needed
226
+ meta_file = sdir / "meta.json"
227
+ if meta_file.exists():
228
+ try:
229
+ meta = json.loads(meta_file.read_text())
230
+ changed = False
231
+ if root and meta.get("project") != str(root):
232
+ meta["project"] = str(root)
233
+ changed = True
234
+ transcript_path = data.get("transcript_path")
235
+ if transcript_path and not meta.get("transcript_path"):
236
+ meta["transcript_path"] = transcript_path
237
+ changed = True
238
+ if changed:
239
+ meta_file.write_text(json.dumps(meta, indent=2))
240
+ except (json.JSONDecodeError, OSError):
241
+ pass
230
242
 
231
243
  # Relativize path for readability
232
244
  display_path = file_path
@@ -314,6 +326,52 @@ def _check_overlap(current_session: str, project: str, file_path: str) -> None:
314
326
  # Stop handler
315
327
  # ---------------------------------------------------------------------------
316
328
 
329
+ def _compute_token_usage(transcript_path: str) -> dict | None:
330
+ """Read a Claude Code transcript JSONL and sum token usage."""
331
+ try:
332
+ tp = Path(transcript_path)
333
+ if not tp.exists():
334
+ return None
335
+
336
+ total_input = 0
337
+ total_output = 0
338
+ total_cache_read = 0
339
+ total_cache_write = 0
340
+ model = None
341
+
342
+ for line in tp.open():
343
+ try:
344
+ d = json.loads(line)
345
+ msg = d.get("message", {})
346
+ usage = msg.get("usage")
347
+ if not usage:
348
+ continue
349
+
350
+ if model is None:
351
+ model = msg.get("model")
352
+
353
+ total_input += usage.get("input_tokens", 0)
354
+ total_output += usage.get("output_tokens", 0)
355
+ total_cache_read += usage.get("cache_read_input_tokens", 0)
356
+ total_cache_write += usage.get("cache_creation_input_tokens", 0)
357
+ except json.JSONDecodeError:
358
+ continue
359
+
360
+ if total_input == 0 and total_output == 0:
361
+ return None
362
+
363
+ return {
364
+ "model": model,
365
+ "input_tokens": total_input,
366
+ "output_tokens": total_output,
367
+ "cache_read_tokens": total_cache_read,
368
+ "cache_write_tokens": total_cache_write,
369
+ "total_tokens": total_input + total_output,
370
+ }
371
+ except (OSError, Exception):
372
+ return None
373
+
374
+
317
375
  def handle_stop(data: dict) -> None:
318
376
  """Finalize the current session with a summary."""
319
377
  session_id = active_session()
@@ -343,13 +401,18 @@ def handle_stop(data: dict) -> None:
343
401
  except json.JSONDecodeError:
344
402
  pass
345
403
 
346
- # Read start time from meta
404
+ # Read start time and transcript path from meta
347
405
  meta_file = sdir / "meta.json"
348
406
  start_time = None
407
+ transcript_path = None
349
408
  if meta_file.exists():
350
409
  try:
351
410
  meta = json.loads(meta_file.read_text())
352
411
  start_time = meta.get("start_time")
412
+ transcript_path = meta.get("transcript_path")
413
+ # Also check the Stop hook data for transcript_path
414
+ if not transcript_path:
415
+ transcript_path = data.get("transcript_path")
353
416
  meta["status"] = "completed"
354
417
  meta_file.write_text(json.dumps(meta, indent=2))
355
418
  except (json.JSONDecodeError, OSError):
@@ -369,7 +432,7 @@ def handle_stop(data: dict) -> None:
369
432
  prompts_file = sdir / "prompts.jsonl"
370
433
  prompt_count = _count_lines(prompts_file)
371
434
 
372
- summary = {
435
+ summary: dict = {
373
436
  "end_time": end_time,
374
437
  "duration_seconds": duration,
375
438
  "prompts": prompt_count,
@@ -381,6 +444,12 @@ def handle_stop(data: dict) -> None:
381
444
  "total_lines_changed": total_lines,
382
445
  }
383
446
 
447
+ # Compute token usage from transcript
448
+ if transcript_path:
449
+ token_usage = _compute_token_usage(transcript_path)
450
+ if token_usage:
451
+ summary["tokens"] = token_usage
452
+
384
453
  (sdir / "summary.json").write_text(json.dumps(summary, indent=2))
385
454
 
386
455
  # Clear active session
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inscript"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "Universal agent activity ledger. Records what AI agents do."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes