artificial-intelligence-usage 0.0.2__tar.gz → 0.0.4__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: artificial-intelligence-usage
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Analyze usage of artificial intelligence tool.
5
5
  Author: Kaizten Analytics
6
6
  License-Expression: LicenseRef-Proprietary
@@ -17,7 +17,8 @@ latest session followed by daily session, turn, token, and rate-limit usage.
17
17
  ## Behavior
18
18
 
19
19
  - Reads rollout files below `~/.codex/sessions`.
20
- - Reports when the Codex or Claude CLI is not available on `PATH`.
20
+ - Shows the installed Codex and Claude CLI versions when those commands are
21
+ available.
21
22
  - Shows status details from the most recently modified session.
22
23
  - Reports sessions from the last 14 calendar days by default, including today.
23
24
  - Counts user turns and cumulative tokens for each session.
@@ -6,7 +6,8 @@ latest session followed by daily session, turn, token, and rate-limit usage.
6
6
  ## Behavior
7
7
 
8
8
  - Reads rollout files below `~/.codex/sessions`.
9
- - Reports when the Codex or Claude CLI is not available on `PATH`.
9
+ - Shows the installed Codex and Claude CLI versions when those commands are
10
+ available.
10
11
  - Shows status details from the most recently modified session.
11
12
  - Reports sessions from the last 14 calendar days by default, including today.
12
13
  - Counts user turns and cumulative tokens for each session.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "artificial-intelligence-usage"
7
- version = "0.0.2"
7
+ version = "0.0.4"
8
8
  description = "Analyze usage of artificial intelligence tool."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: artificial-intelligence-usage
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Analyze usage of artificial intelligence tool.
5
5
  Author: Kaizten Analytics
6
6
  License-Expression: LicenseRef-Proprietary
@@ -17,7 +17,8 @@ latest session followed by daily session, turn, token, and rate-limit usage.
17
17
  ## Behavior
18
18
 
19
19
  - Reads rollout files below `~/.codex/sessions`.
20
- - Reports when the Codex or Claude CLI is not available on `PATH`.
20
+ - Shows the installed Codex and Claude CLI versions when those commands are
21
+ available.
21
22
  - Shows status details from the most recently modified session.
22
23
  - Reports sessions from the last 14 calendar days by default, including today.
23
24
  - Counts user turns and cumulative tokens for each session.
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env python3
2
- """Analyze recent Codex sessions and report usage information."""
2
+ """Analyze recent sessions of artificial intelligence tools (Codex, Claude, etc.) and report usage information."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
7
  import json
8
8
  import shutil
9
+ import subprocess
9
10
  from collections import Counter, defaultdict
10
11
  from datetime import date, datetime, timedelta
11
12
  from pathlib import Path
@@ -13,13 +14,7 @@ from typing import Any
13
14
 
14
15
 
15
16
  DEFAULT_DAYS = 14
16
-
17
-
18
- def print_unavailable_tools() -> None:
19
- """Print a message for each supported AI CLI that is not on PATH."""
20
- for command, name in (("codex", "Codex"), ("claude", "Claude")):
21
- if shutil.which(command) is None:
22
- print(f"{name} is not available.")
17
+ SUPPORTED_TOOLS = (("Codex", "codex"), ("Claude", "claude"))
23
18
 
24
19
 
25
20
  def positive_int(value: str) -> int:
@@ -35,6 +30,53 @@ def positive_int(value: str) -> int:
35
30
  return number
36
31
 
37
32
 
33
+ def installed_tool_version(command: str) -> str | None:
34
+ """Return an installed command's version output, or ``None`` if absent."""
35
+ executable = shutil.which(command)
36
+ if executable is None:
37
+ return None
38
+
39
+ try:
40
+ result = subprocess.run(
41
+ [executable, "--version"],
42
+ capture_output=True,
43
+ check=False,
44
+ text=True,
45
+ timeout=5,
46
+ )
47
+ except (OSError, subprocess.TimeoutExpired):
48
+ return "unknown"
49
+
50
+ output = result.stdout.strip() or result.stderr.strip()
51
+ if not output:
52
+ return "unknown"
53
+
54
+ return " ".join(output.splitlines()[0].split())
55
+
56
+
57
+ def detect_installed_tool_versions() -> dict[str, str]:
58
+ """Return versions for the supported AI command-line tools that exist."""
59
+ versions = {}
60
+ for name, command in SUPPORTED_TOOLS:
61
+ version = installed_tool_version(command)
62
+ if version is not None:
63
+ versions[name] = version
64
+ return versions
65
+
66
+
67
+ def print_installed_tool_versions(versions: dict[str, str]) -> None:
68
+ """Print installed AI command-line tool versions when any are present."""
69
+ if not versions:
70
+ return
71
+
72
+ print("Installed AI tools")
73
+ print("-" * 57)
74
+ for name, _command in SUPPORTED_TOOLS:
75
+ if name in versions:
76
+ print(f"{name + ':':<16} {versions[name]}")
77
+ print()
78
+
79
+
38
80
  def format_timestamp(value: str | int | float | None) -> str:
39
81
  """Format an ISO or Unix timestamp in the local timezone."""
40
82
  try:
@@ -268,7 +310,12 @@ def build_parser() -> argparse.ArgumentParser:
268
310
  return parser
269
311
 
270
312
 
271
- def run_report(sessions_dir: Path, days: int, today: date | None = None) -> int:
313
+ def run_report(
314
+ sessions_dir: Path,
315
+ days: int,
316
+ today: date | None = None,
317
+ tool_versions: dict[str, str] | None = None,
318
+ ) -> int:
272
319
  """Analyze sessions and print the status and daily usage report."""
273
320
  current_day = today or date.today()
274
321
  first_day = current_day - timedelta(days=days - 1)
@@ -277,6 +324,10 @@ def run_report(sessions_dir: Path, days: int, today: date | None = None) -> int:
277
324
  token_counts: Counter[str] = Counter()
278
325
  last_session_by_date: dict[str, Path] = {}
279
326
 
327
+ if tool_versions is None:
328
+ tool_versions = detect_installed_tool_versions()
329
+ print_installed_tool_versions(tool_versions)
330
+
280
331
  all_session_files = list(sessions_dir.glob("*/*/*/rollout-*.jsonl"))
281
332
  latest_session = (
282
333
  max(all_session_files, key=lambda path: path.stat().st_mtime)
@@ -360,7 +411,6 @@ def run_report(sessions_dir: Path, days: int, today: date | None = None) -> int:
360
411
  def main(argv: list[str] | None = None) -> int:
361
412
  """Run the artificial-intelligence-usage command."""
362
413
  args = build_parser().parse_args(argv)
363
- print_unavailable_tools()
364
414
  return run_report(Path.home() / ".codex" / "sessions", args.days)
365
415
 
366
416
 
@@ -73,6 +73,63 @@ class ArtificialIntelligenceUsageTests(unittest.TestCase):
73
73
  with self.assertRaisesRegex(Exception, "must be an integer"):
74
74
  artificial_intelligence_usage.positive_int("nope")
75
75
 
76
+ def test_installed_tool_version_returns_none_when_command_is_absent(self):
77
+ with patch.object(
78
+ artificial_intelligence_usage.shutil, "which", return_value=None
79
+ ):
80
+ self.assertIsNone(
81
+ artificial_intelligence_usage.installed_tool_version("codex")
82
+ )
83
+
84
+ def test_installed_tool_version_reads_version_from_command(self):
85
+ completed = artificial_intelligence_usage.subprocess.CompletedProcess(
86
+ args=["/tools/codex", "--version"],
87
+ returncode=0,
88
+ stdout="codex-cli 1.2.3\n",
89
+ stderr="",
90
+ )
91
+ with (
92
+ patch.object(
93
+ artificial_intelligence_usage.shutil,
94
+ "which",
95
+ return_value="/tools/codex",
96
+ ),
97
+ patch.object(
98
+ artificial_intelligence_usage.subprocess,
99
+ "run",
100
+ return_value=completed,
101
+ ) as run,
102
+ ):
103
+ version = artificial_intelligence_usage.installed_tool_version("codex")
104
+
105
+ self.assertEqual(version, "codex-cli 1.2.3")
106
+ run.assert_called_once_with(
107
+ ["/tools/codex", "--version"],
108
+ capture_output=True,
109
+ check=False,
110
+ text=True,
111
+ timeout=5,
112
+ )
113
+
114
+ def test_report_includes_installed_codex_and_claude_versions(self):
115
+ with tempfile.TemporaryDirectory() as directory:
116
+ output = io.StringIO()
117
+ with contextlib.redirect_stdout(output):
118
+ artificial_intelligence_usage.run_report(
119
+ Path(directory),
120
+ days=14,
121
+ today=date(2026, 10, 4),
122
+ tool_versions={
123
+ "Codex": "codex-cli 1.2.3",
124
+ "Claude": "2.0.1 (Claude Code)",
125
+ },
126
+ )
127
+
128
+ report = output.getvalue()
129
+ self.assertIn("Installed AI tools", report)
130
+ self.assertIn("Codex: codex-cli 1.2.3", report)
131
+ self.assertIn("Claude: 2.0.1 (Claude Code)", report)
132
+
76
133
  def test_analyze_session_ignores_malformed_lines_and_uses_maximum_tokens(self):
77
134
  with tempfile.TemporaryDirectory() as directory:
78
135
  session_file = Path(directory) / "rollout-test.jsonl"
@@ -118,7 +175,10 @@ class ArtificialIntelligenceUsageTests(unittest.TestCase):
118
175
  output = io.StringIO()
119
176
  with contextlib.redirect_stdout(output):
120
177
  result = artificial_intelligence_usage.run_report(
121
- Path(directory), days=14, today=date(2026, 10, 4)
178
+ Path(directory),
179
+ days=14,
180
+ today=date(2026, 10, 4),
181
+ tool_versions={},
122
182
  )
123
183
 
124
184
  self.assertEqual(result, 0)
@@ -170,7 +230,10 @@ class ArtificialIntelligenceUsageTests(unittest.TestCase):
170
230
  output = io.StringIO()
171
231
  with contextlib.redirect_stdout(output):
172
232
  result = artificial_intelligence_usage.run_report(
173
- sessions_dir, days=2, today=date(2026, 10, 4)
233
+ sessions_dir,
234
+ days=2,
235
+ today=date(2026, 10, 4),
236
+ tool_versions={},
174
237
  )
175
238
 
176
239
  report = output.getvalue()
@@ -184,40 +247,14 @@ class ArtificialIntelligenceUsageTests(unittest.TestCase):
184
247
  def test_main_can_be_called_repeatedly(self):
185
248
  with patch.object(
186
249
  artificial_intelligence_usage, "run_report", return_value=0
187
- ) as run_report, patch.object(
188
- artificial_intelligence_usage, "print_unavailable_tools"
189
- ) as print_unavailable_tools:
250
+ ) as run_report:
190
251
  self.assertEqual(artificial_intelligence_usage.main(["--days", "3"]), 0)
191
252
  self.assertEqual(artificial_intelligence_usage.main(["--days", "5"]), 0)
192
253
 
193
254
  self.assertEqual(run_report.call_count, 2)
194
- self.assertEqual(print_unavailable_tools.call_count, 2)
195
255
  self.assertEqual(run_report.call_args_list[0].args[1], 3)
196
256
  self.assertEqual(run_report.call_args_list[1].args[1], 5)
197
257
 
198
- def test_unavailable_tools_prints_a_message_for_codex_and_claude(self):
199
- output = io.StringIO()
200
- with patch.object(
201
- artificial_intelligence_usage.shutil, "which", return_value=None
202
- ), contextlib.redirect_stdout(output):
203
- artificial_intelligence_usage.print_unavailable_tools()
204
-
205
- self.assertEqual(
206
- output.getvalue(),
207
- "Codex is not available.\nClaude is not available.\n",
208
- )
209
-
210
- def test_unavailable_tools_does_not_print_for_installed_commands(self):
211
- output = io.StringIO()
212
- with patch.object(
213
- artificial_intelligence_usage.shutil,
214
- "which",
215
- side_effect=lambda command: f"/usr/bin/{command}",
216
- ), contextlib.redirect_stdout(output):
217
- artificial_intelligence_usage.print_unavailable_tools()
218
-
219
- self.assertEqual(output.getvalue(), "")
220
-
221
258
 
222
259
  if __name__ == "__main__":
223
260
  unittest.main()