artificial-intelligence-usage 0.0.4__tar.gz → 0.0.5__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 (14) hide show
  1. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/PKG-INFO +17 -2
  2. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/README.md +15 -1
  3. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/pyproject.toml +2 -1
  4. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/PKG-INFO +17 -2
  5. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/SOURCES.txt +1 -0
  6. artificial_intelligence_usage-0.0.5/src/artificial_intelligence_usage.egg-info/requires.txt +1 -0
  7. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.py +307 -9
  8. artificial_intelligence_usage-0.0.5/tests/test_artificial_intelligence_usage.py +608 -0
  9. artificial_intelligence_usage-0.0.4/tests/test_artificial_intelligence_usage.py +0 -260
  10. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/setup.cfg +0 -0
  11. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/dependency_links.txt +0 -0
  12. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/entry_points.txt +0 -0
  13. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/top_level.txt +0 -0
  14. {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/tests/test_publish_pypi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: artificial-intelligence-usage
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Analyze usage of artificial intelligence tool.
5
5
  Author: Kaizten Analytics
6
6
  License-Expression: LicenseRef-Proprietary
@@ -8,11 +8,12 @@ Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3 :: Only
9
9
  Requires-Python: >=3.10
10
10
  Description-Content-Type: text/markdown
11
+ Requires-Dist: langid==1.1.6
11
12
 
12
13
  # Artificial Intelligence Usage
13
14
 
14
15
  `artificial-intelligence-usage` analyzes local Codex session rollouts and prints a summary of the
15
- latest session followed by daily session, turn, token, and rate-limit usage.
16
+ latest session followed by daily and per-session prompt, token, and rate-limit usage.
16
17
 
17
18
  ## Behavior
18
19
 
@@ -22,6 +23,19 @@ latest session followed by daily session, turn, token, and rate-limit usage.
22
23
  - Shows status details from the most recently modified session.
23
24
  - Reports sessions from the last 14 calendar days by default, including today.
24
25
  - Counts user turns and cumulative tokens for each session.
26
+ - Breaks applied prompts down into Normal, Plan, and Goal counts. Goal counts
27
+ include automatic goal continuations, while background tasks such as guardian
28
+ reviews are excluded.
29
+ - Shows a second table with one row per session, including its start time,
30
+ session name (up to 10 characters), first-prompt preview (up to 20
31
+ characters), GitHub repository, usage, and language.
32
+ - Reads session names from `~/.codex/session_index.jsonl`. Missing names and
33
+ prompts display as `Unknown`.
34
+ - For goal-only sessions, reads the goal objective from
35
+ `~/.codex/goals_1.sqlite` for the prompt preview and reports its language as
36
+ `Unknown`.
37
+ - Uses `langid`, restricted to English and Spanish, for prompt language
38
+ detection. Sessions without a detectable user prompt show `Unknown`.
25
39
  - Shows the remaining five-hour and weekly allowance recorded in each day's
26
40
  last session.
27
41
  - Ignores malformed JSON lines and incomplete status events.
@@ -31,6 +45,7 @@ The tool only reads local session files and does not modify them.
31
45
  ## Requirements
32
46
 
33
47
  - Python 3.10 or newer
48
+ - `langid` 1.1.6 (installed automatically with the package)
34
49
 
35
50
  ## Building
36
51
 
@@ -1,7 +1,7 @@
1
1
  # Artificial Intelligence Usage
2
2
 
3
3
  `artificial-intelligence-usage` analyzes local Codex session rollouts and prints a summary of the
4
- latest session followed by daily session, turn, token, and rate-limit usage.
4
+ latest session followed by daily and per-session prompt, token, and rate-limit usage.
5
5
 
6
6
  ## Behavior
7
7
 
@@ -11,6 +11,19 @@ latest session followed by daily session, turn, token, and rate-limit usage.
11
11
  - Shows status details from the most recently modified session.
12
12
  - Reports sessions from the last 14 calendar days by default, including today.
13
13
  - Counts user turns and cumulative tokens for each session.
14
+ - Breaks applied prompts down into Normal, Plan, and Goal counts. Goal counts
15
+ include automatic goal continuations, while background tasks such as guardian
16
+ reviews are excluded.
17
+ - Shows a second table with one row per session, including its start time,
18
+ session name (up to 10 characters), first-prompt preview (up to 20
19
+ characters), GitHub repository, usage, and language.
20
+ - Reads session names from `~/.codex/session_index.jsonl`. Missing names and
21
+ prompts display as `Unknown`.
22
+ - For goal-only sessions, reads the goal objective from
23
+ `~/.codex/goals_1.sqlite` for the prompt preview and reports its language as
24
+ `Unknown`.
25
+ - Uses `langid`, restricted to English and Spanish, for prompt language
26
+ detection. Sessions without a detectable user prompt show `Unknown`.
14
27
  - Shows the remaining five-hour and weekly allowance recorded in each day's
15
28
  last session.
16
29
  - Ignores malformed JSON lines and incomplete status events.
@@ -20,6 +33,7 @@ The tool only reads local session files and does not modify them.
20
33
  ## Requirements
21
34
 
22
35
  - Python 3.10 or newer
36
+ - `langid` 1.1.6 (installed automatically with the package)
23
37
 
24
38
  ## Building
25
39
 
@@ -4,12 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "artificial-intelligence-usage"
7
- version = "0.0.4"
7
+ version = "0.0.5"
8
8
  description = "Analyze usage of artificial intelligence tool."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "LicenseRef-Proprietary"
12
12
  authors = [{ name = "Kaizten Analytics" }]
13
+ dependencies = ["langid==1.1.6"]
13
14
  classifiers = [
14
15
  "Programming Language :: Python :: 3",
15
16
  "Programming Language :: Python :: 3 :: Only",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: artificial-intelligence-usage
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Analyze usage of artificial intelligence tool.
5
5
  Author: Kaizten Analytics
6
6
  License-Expression: LicenseRef-Proprietary
@@ -8,11 +8,12 @@ Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3 :: Only
9
9
  Requires-Python: >=3.10
10
10
  Description-Content-Type: text/markdown
11
+ Requires-Dist: langid==1.1.6
11
12
 
12
13
  # Artificial Intelligence Usage
13
14
 
14
15
  `artificial-intelligence-usage` analyzes local Codex session rollouts and prints a summary of the
15
- latest session followed by daily session, turn, token, and rate-limit usage.
16
+ latest session followed by daily and per-session prompt, token, and rate-limit usage.
16
17
 
17
18
  ## Behavior
18
19
 
@@ -22,6 +23,19 @@ latest session followed by daily session, turn, token, and rate-limit usage.
22
23
  - Shows status details from the most recently modified session.
23
24
  - Reports sessions from the last 14 calendar days by default, including today.
24
25
  - Counts user turns and cumulative tokens for each session.
26
+ - Breaks applied prompts down into Normal, Plan, and Goal counts. Goal counts
27
+ include automatic goal continuations, while background tasks such as guardian
28
+ reviews are excluded.
29
+ - Shows a second table with one row per session, including its start time,
30
+ session name (up to 10 characters), first-prompt preview (up to 20
31
+ characters), GitHub repository, usage, and language.
32
+ - Reads session names from `~/.codex/session_index.jsonl`. Missing names and
33
+ prompts display as `Unknown`.
34
+ - For goal-only sessions, reads the goal objective from
35
+ `~/.codex/goals_1.sqlite` for the prompt preview and reports its language as
36
+ `Unknown`.
37
+ - Uses `langid`, restricted to English and Spanish, for prompt language
38
+ detection. Sessions without a detectable user prompt show `Unknown`.
25
39
  - Shows the remaining five-hour and weekly allowance recorded in each day's
26
40
  last session.
27
41
  - Ignores malformed JSON lines and incomplete status events.
@@ -31,6 +45,7 @@ The tool only reads local session files and does not modify them.
31
45
  ## Requirements
32
46
 
33
47
  - Python 3.10 or newer
48
+ - `langid` 1.1.6 (installed automatically with the package)
34
49
 
35
50
  ## Building
36
51
 
@@ -5,6 +5,7 @@ src/artificial_intelligence_usage.egg-info/PKG-INFO
5
5
  src/artificial_intelligence_usage.egg-info/SOURCES.txt
6
6
  src/artificial_intelligence_usage.egg-info/dependency_links.txt
7
7
  src/artificial_intelligence_usage.egg-info/entry_points.txt
8
+ src/artificial_intelligence_usage.egg-info/requires.txt
8
9
  src/artificial_intelligence_usage.egg-info/top_level.txt
9
10
  tests/test_artificial_intelligence_usage.py
10
11
  tests/test_publish_pypi.py
@@ -5,16 +5,46 @@ from __future__ import annotations
5
5
 
6
6
  import argparse
7
7
  import json
8
+ import re
8
9
  import shutil
10
+ import sqlite3
9
11
  import subprocess
10
12
  from collections import Counter, defaultdict
13
+ from dataclasses import dataclass
11
14
  from datetime import date, datetime, timedelta
12
15
  from pathlib import Path
13
16
  from typing import Any
14
17
 
18
+ import langid
19
+
15
20
 
16
21
  DEFAULT_DAYS = 14
17
22
  SUPPORTED_TOOLS = (("Codex", "codex"), ("Claude", "claude"))
23
+ INTERNAL_USER_TEXT_TAGS = {
24
+ "codex_internal_context",
25
+ "environment_context",
26
+ "goal",
27
+ "goals",
28
+ "guardian_tool_descriptions",
29
+ "turn_aborted",
30
+ }
31
+
32
+ langid.set_languages(["en", "es"])
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class SessionAnalysis:
37
+ """Usage information extracted from one Codex session."""
38
+
39
+ turns: int
40
+ tokens: int
41
+ normal_prompts: int
42
+ plan_prompts: int
43
+ goal_prompts: int
44
+ repository: str
45
+ language: str
46
+ session_id: str | None
47
+ first_prompt: str | None
18
48
 
19
49
 
20
50
  def positive_int(value: str) -> int:
@@ -263,10 +293,170 @@ def print_status(session_file: Path | None) -> None:
263
293
  print()
264
294
 
265
295
 
266
- def analyze_session(session_file: Path) -> tuple[int, int]:
267
- """Return ``(user_turns, total_tokens)`` for one Codex session."""
296
+ def prompt_type(payload: dict[str, Any]) -> str | None:
297
+ """Return the prompt type for a task-started payload, if applicable."""
298
+ attribution = payload.get("turn_attribution")
299
+ if isinstance(attribution, dict):
300
+ trigger = attribution.get("turn_trigger")
301
+ elif isinstance(attribution, str):
302
+ trigger = attribution
303
+ else:
304
+ trigger = None
305
+
306
+ if trigger == "goal":
307
+ return "goal"
308
+
309
+ if trigger not in (None, "user"):
310
+ return None
311
+
312
+ mode = payload.get("collaboration_mode_kind")
313
+ if mode == "plan":
314
+ return "plan"
315
+ if mode in (None, "default"):
316
+ return "normal"
317
+
318
+ return None
319
+
320
+
321
+ def github_repository(repository_url: Any) -> str:
322
+ """Return ``owner/repository`` for a GitHub remote URL."""
323
+ if not isinstance(repository_url, str):
324
+ return "Unknown"
325
+
326
+ match = re.match(
327
+ r"^(?:https?://|ssh://git@|git@)github\.com[/:]"
328
+ r"(?P<owner>[^/]+)/(?P<repository>[^/#]+)/?$",
329
+ repository_url.strip(),
330
+ re.IGNORECASE,
331
+ )
332
+ if match is None:
333
+ return "Unknown"
334
+
335
+ repository = match.group("repository").removesuffix(".git")
336
+ return f"{match.group('owner')}/{repository}"
337
+
338
+
339
+ def is_internal_user_text(text: str) -> bool:
340
+ """Return whether user-role text contains Codex-injected context."""
341
+ match = re.match(r"^\s*<([A-Za-z0-9_-]+)(?:\s|>)", text)
342
+ return match is not None and match.group(1) in INTERNAL_USER_TEXT_TAGS
343
+
344
+
345
+ def user_prompt_text(payload: dict[str, Any]) -> str | None:
346
+ """Return real user-authored text from a user-message payload."""
347
+ content = payload.get("content")
348
+ if not isinstance(content, list):
349
+ return None
350
+
351
+ parts = []
352
+ for item in content:
353
+ if not isinstance(item, dict) or item.get("type") != "input_text":
354
+ continue
355
+
356
+ text = item.get("text")
357
+ if (
358
+ isinstance(text, str)
359
+ and text.strip()
360
+ and not is_internal_user_text(text)
361
+ ):
362
+ parts.append(text.strip())
363
+
364
+ return "\n".join(parts) or None
365
+
366
+
367
+ def detect_language(prompt: str | None) -> str:
368
+ """Classify a prompt as English or Spanish."""
369
+ if prompt is None:
370
+ return "Unknown"
371
+
372
+ language_code, _confidence = langid.classify(prompt)
373
+ return {"en": "English", "es": "Spanish"}.get(language_code, "Unknown")
374
+
375
+
376
+ def compact_text(value: str | None, maximum_length: int) -> str:
377
+ """Normalize whitespace and truncate display text."""
378
+ if not isinstance(value, str):
379
+ return "Unknown"
380
+
381
+ normalized = " ".join(value.split())
382
+ return normalized[:maximum_length] or "Unknown"
383
+
384
+
385
+ def read_session_names(session_index_file: Path) -> dict[str, str]:
386
+ """Return the latest indexed name for each Codex session."""
387
+ names: dict[str, str] = {}
388
+
389
+ try:
390
+ with session_index_file.open("r", encoding="utf-8") as file_handle:
391
+ for line in file_handle:
392
+ try:
393
+ entry = json.loads(line)
394
+ except (json.JSONDecodeError, UnicodeDecodeError):
395
+ continue
396
+
397
+ session_id = entry.get("id")
398
+ thread_name = entry.get("thread_name")
399
+ if (
400
+ isinstance(session_id, str)
401
+ and session_id
402
+ and isinstance(thread_name, str)
403
+ and thread_name.strip()
404
+ ):
405
+ names[session_id] = thread_name
406
+ except (OSError, UnicodeError):
407
+ pass
408
+
409
+ return names
410
+
411
+
412
+ def read_goal_objectives(goals_database: Path) -> dict[str, str]:
413
+ """Return goal objectives keyed by their Codex thread ID."""
414
+ if not goals_database.is_file():
415
+ return {}
416
+
417
+ connection: sqlite3.Connection | None = None
418
+ try:
419
+ database_uri = f"{goals_database.resolve().as_uri()}?mode=ro"
420
+ connection = sqlite3.connect(database_uri, uri=True)
421
+ rows = connection.execute(
422
+ "SELECT thread_id, objective FROM thread_goals"
423
+ ).fetchall()
424
+ except (OSError, sqlite3.Error):
425
+ return {}
426
+ finally:
427
+ if connection is not None:
428
+ connection.close()
429
+
430
+ return {
431
+ thread_id: objective
432
+ for thread_id, objective in rows
433
+ if isinstance(thread_id, str)
434
+ and thread_id
435
+ and isinstance(objective, str)
436
+ and objective.strip()
437
+ }
438
+
439
+
440
+ def session_start_time(session_file: Path) -> str:
441
+ """Return the session start time encoded in a rollout filename."""
442
+ match = re.match(
443
+ r"^rollout-\d{4}-\d{2}-\d{2}T(\d{2})-(\d{2})-(\d{2})-",
444
+ session_file.name,
445
+ )
446
+ if match is None:
447
+ return "Unknown"
448
+
449
+ return ":".join(match.groups())
450
+
451
+
452
+ def analyze_session(session_file: Path) -> SessionAnalysis:
453
+ """Return usage information extracted from one Codex session."""
268
454
  turns = 0
269
455
  total_tokens = 0
456
+ prompt_counts: Counter[str] = Counter()
457
+ repository = "Unknown"
458
+ session_id = None
459
+ first_prompt = None
270
460
 
271
461
  with session_file.open("r", encoding="utf-8") as file_handle:
272
462
  for line in file_handle:
@@ -277,12 +467,29 @@ def analyze_session(session_file: Path) -> tuple[int, int]:
277
467
 
278
468
  payload = event.get("payload") or {}
279
469
 
470
+ if event.get("type") == "session_meta":
471
+ candidate_session_id = payload.get("id") or payload.get("session_id")
472
+ if isinstance(candidate_session_id, str) and candidate_session_id:
473
+ session_id = candidate_session_id
474
+ git = payload.get("git") or {}
475
+ repository = github_repository(git.get("repository_url"))
476
+
477
+ if (
478
+ event.get("type") == "event_msg"
479
+ and payload.get("type") == "task_started"
480
+ ):
481
+ category = prompt_type(payload)
482
+ if category is not None:
483
+ prompt_counts[category] += 1
484
+
280
485
  if (
281
486
  event.get("type") == "response_item"
282
487
  and payload.get("type") == "message"
283
488
  and payload.get("role") == "user"
284
489
  ):
285
490
  turns += 1
491
+ if first_prompt is None:
492
+ first_prompt = user_prompt_text(payload)
286
493
 
287
494
  if (
288
495
  event.get("type") == "event_msg"
@@ -295,7 +502,17 @@ def analyze_session(session_file: Path) -> tuple[int, int]:
295
502
  if isinstance(value, (int, float)):
296
503
  total_tokens = max(total_tokens, int(value))
297
504
 
298
- return turns, total_tokens
505
+ return SessionAnalysis(
506
+ turns=turns,
507
+ tokens=total_tokens,
508
+ normal_prompts=prompt_counts["normal"],
509
+ plan_prompts=prompt_counts["plan"],
510
+ goal_prompts=prompt_counts["goal"],
511
+ repository=repository,
512
+ language=detect_language(first_prompt),
513
+ session_id=session_id,
514
+ first_prompt=first_prompt,
515
+ )
299
516
 
300
517
 
301
518
  def build_parser() -> argparse.ArgumentParser:
@@ -315,6 +532,8 @@ def run_report(
315
532
  days: int,
316
533
  today: date | None = None,
317
534
  tool_versions: dict[str, str] | None = None,
535
+ session_index_file: Path | None = None,
536
+ goals_database: Path | None = None,
318
537
  ) -> int:
319
538
  """Analyze sessions and print the status and daily usage report."""
320
539
  current_day = today or date.today()
@@ -322,12 +541,24 @@ def run_report(
322
541
  session_counts: Counter[str] = Counter()
323
542
  turn_counts: defaultdict[str, list[int]] = defaultdict(list)
324
543
  token_counts: Counter[str] = Counter()
544
+ normal_prompt_counts: Counter[str] = Counter()
545
+ plan_prompt_counts: Counter[str] = Counter()
546
+ goal_prompt_counts: Counter[str] = Counter()
325
547
  last_session_by_date: dict[str, Path] = {}
548
+ session_rows: list[tuple[str, Path, SessionAnalysis]] = []
326
549
 
327
550
  if tool_versions is None:
328
551
  tool_versions = detect_installed_tool_versions()
329
552
  print_installed_tool_versions(tool_versions)
330
553
 
554
+ codex_dir = sessions_dir.parent
555
+ session_names = read_session_names(
556
+ session_index_file or codex_dir / "session_index.jsonl"
557
+ )
558
+ goal_objectives = read_goal_objectives(
559
+ goals_database or codex_dir / "goals_1.sqlite"
560
+ )
561
+
331
562
  all_session_files = list(sessions_dir.glob("*/*/*/rollout-*.jsonl"))
332
563
  latest_session = (
333
564
  max(all_session_files, key=lambda path: path.stat().st_mtime)
@@ -349,10 +580,14 @@ def run_report(
349
580
  if not first_day <= session_date <= current_day:
350
581
  continue
351
582
 
352
- turns, tokens = analyze_session(session_file)
583
+ analysis = analyze_session(session_file)
353
584
  session_counts[date_label] += 1
354
- turn_counts[date_label].append(turns)
355
- token_counts[date_label] += tokens
585
+ turn_counts[date_label].append(analysis.turns)
586
+ token_counts[date_label] += analysis.tokens
587
+ normal_prompt_counts[date_label] += analysis.normal_prompts
588
+ plan_prompt_counts[date_label] += analysis.plan_prompts
589
+ goal_prompt_counts[date_label] += analysis.goal_prompts
590
+ session_rows.append((date_label, session_file, analysis))
356
591
 
357
592
  previous_last = last_session_by_date.get(date_label)
358
593
  if previous_last is None or session_file.name > previous_last.name:
@@ -362,16 +597,20 @@ def run_report(
362
597
  print(f"No Codex sessions found in the last {days} days in {sessions_dir}")
363
598
  return 0
364
599
 
365
- print(
600
+ header = (
366
601
  f"{'Date':<12} "
367
602
  f"{'Sessions':>8} "
368
603
  f"{'Turns':>8} "
369
604
  f"{'Avg turns':>10} "
605
+ f"{'Normal':>8} "
606
+ f"{'Plan':>8} "
607
+ f"{'Goal':>8} "
370
608
  f"{'Tokens':>15} "
371
609
  f"{'5h last session':>17} "
372
610
  f"{'Weekly last session':>20}"
373
611
  )
374
- print("-" * 96)
612
+ print(header)
613
+ print("-" * len(header))
375
614
 
376
615
  for date_label in sorted(session_counts):
377
616
  sessions = session_counts[date_label]
@@ -385,6 +624,9 @@ def run_report(
385
624
  f"{sessions:>8} "
386
625
  f"{total_turns:>8} "
387
626
  f"{avg_turns:>10.1f} "
627
+ f"{normal_prompt_counts[date_label]:>8} "
628
+ f"{plan_prompt_counts[date_label]:>8} "
629
+ f"{goal_prompt_counts[date_label]:>8} "
388
630
  f"{tokens:>15,} "
389
631
  f"{five_hour:>17} "
390
632
  f"{weekly:>20}"
@@ -393,18 +635,74 @@ def run_report(
393
635
  total_sessions = sum(session_counts.values())
394
636
  total_turns = sum(sum(turns) for turns in turn_counts.values())
395
637
  total_tokens = sum(token_counts.values())
638
+ total_normal_prompts = sum(normal_prompt_counts.values())
639
+ total_plan_prompts = sum(plan_prompt_counts.values())
640
+ total_goal_prompts = sum(goal_prompt_counts.values())
396
641
  overall_average = total_turns / total_sessions
397
642
 
398
- print("-" * 96)
643
+ print("-" * len(header))
399
644
  print(
400
645
  f"{'Total':<12} "
401
646
  f"{total_sessions:>8} "
402
647
  f"{total_turns:>8} "
403
648
  f"{overall_average:>10.1f} "
649
+ f"{total_normal_prompts:>8} "
650
+ f"{total_plan_prompts:>8} "
651
+ f"{total_goal_prompts:>8} "
404
652
  f"{total_tokens:>15,} "
405
653
  f"{'-':>17} "
406
654
  f"{'-':>20}"
407
655
  )
656
+
657
+ print()
658
+ print("Sessions")
659
+ repository_width = max(
660
+ len("Repository"),
661
+ *(len(analysis.repository) for _date, _path, analysis in session_rows),
662
+ )
663
+ session_header = (
664
+ f"{'Date':<12} "
665
+ f"{'Start':<8} "
666
+ f"{'Repository':<{repository_width}} "
667
+ f"{'Name':<10} "
668
+ f"{'First prompt':<20} "
669
+ f"{'Turns':>8} "
670
+ f"{'Normal':>8} "
671
+ f"{'Plan':>8} "
672
+ f"{'Goal':>8} "
673
+ f"{'Language':<8} "
674
+ f"{'Tokens':>15} "
675
+ f"{'5h last session':>17} "
676
+ f"{'Weekly last session':>20}"
677
+ )
678
+ print(session_header)
679
+ print("-" * len(session_header))
680
+
681
+ for date_label, session_file, analysis in sorted(
682
+ session_rows, key=lambda row: (row[0], row[1].name)
683
+ ):
684
+ five_hour, weekly = session_rate_limits(session_file)
685
+ session_name = compact_text(session_names.get(analysis.session_id), 10)
686
+ first_prompt = analysis.first_prompt
687
+ if first_prompt is None and analysis.goal_prompts:
688
+ first_prompt = goal_objectives.get(analysis.session_id)
689
+ prompt_preview = compact_text(first_prompt, 20)
690
+ print(
691
+ f"{date_label:<12} "
692
+ f"{session_start_time(session_file):<8} "
693
+ f"{analysis.repository:<{repository_width}} "
694
+ f"{session_name:<10} "
695
+ f"{prompt_preview:<20} "
696
+ f"{analysis.turns:>8} "
697
+ f"{analysis.normal_prompts:>8} "
698
+ f"{analysis.plan_prompts:>8} "
699
+ f"{analysis.goal_prompts:>8} "
700
+ f"{analysis.language:<8} "
701
+ f"{analysis.tokens:>15,} "
702
+ f"{five_hour:>17} "
703
+ f"{weekly:>20}"
704
+ )
705
+
408
706
  return 0
409
707
 
410
708