clocwork 0.1.0__py3-none-any.whl
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.
- clocwork/__init__.py +15 -0
- clocwork/__main__.py +4 -0
- clocwork/agents.py +113 -0
- clocwork/analyse.py +662 -0
- clocwork/classify.py +124 -0
- clocwork/cli.py +172 -0
- clocwork/cloc.py +346 -0
- clocwork/config.py +83 -0
- clocwork/manpage.py +175 -0
- clocwork/paths.py +171 -0
- clocwork/render.py +172 -0
- clocwork/sources/__init__.py +38 -0
- clocwork/sources/claude_code.py +87 -0
- clocwork/sources/codex.py +266 -0
- clocwork/sources/gemini.py +161 -0
- clocwork/template.html +1974 -0
- clocwork/tokens.py +196 -0
- clocwork-0.1.0.dist-info/METADATA +328 -0
- clocwork-0.1.0.dist-info/RECORD +23 -0
- clocwork-0.1.0.dist-info/WHEEL +5 -0
- clocwork-0.1.0.dist-info/entry_points.txt +2 -0
- clocwork-0.1.0.dist-info/licenses/LICENSE +21 -0
- clocwork-0.1.0.dist-info/top_level.txt +1 -0
clocwork/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""clocwork: lines, agents and tokens over a repository's whole history."""
|
|
2
|
+
|
|
3
|
+
import sys as _sys
|
|
4
|
+
|
|
5
|
+
# Every way in (the console script, the clone's shim, the zipapp, python -m)
|
|
6
|
+
# imports this package before any module that needs 3.11, such as config's
|
|
7
|
+
# tomllib. pip checks requires-python; the others would stop in a traceback.
|
|
8
|
+
# Kept to syntax an old interpreter can parse.
|
|
9
|
+
if _sys.version_info < (3, 11):
|
|
10
|
+
_v = _sys.version_info
|
|
11
|
+
_sys.stderr.write("clocwork: needs Python 3.11 or later; this is Python %d.%d.%d (%s)\n"
|
|
12
|
+
% (_v[0], _v[1], _v[2], _sys.executable))
|
|
13
|
+
raise SystemExit(2)
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
clocwork/__main__.py
ADDED
clocwork/agents.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Which AI agent, if any, a commit credits.
|
|
2
|
+
|
|
3
|
+
Attribution is read from "Co-Authored-By:" trailer lines only, so a human
|
|
4
|
+
commit that merely mentions CLAUDE.md or a claude-* branch name is not
|
|
5
|
+
counted as AI-assisted.
|
|
6
|
+
|
|
7
|
+
Claude model names are parsed generically rather than listed one by one, so
|
|
8
|
+
any Claude model - past, present, or future - is recognised without a code
|
|
9
|
+
change. Two naming schemes are handled:
|
|
10
|
+
|
|
11
|
+
family-first (Claude 4+): "Claude Opus 4.6", "Claude Fable 5.1",
|
|
12
|
+
"Claude Opus 5 (1M context)"
|
|
13
|
+
version-first (Claude 3.x): "Claude 3.5 Sonnet", "Claude 3 Opus"
|
|
14
|
+
|
|
15
|
+
Both normalise to "Claude <Family> <version>", with " (1M)" appended for the
|
|
16
|
+
1M-context variants, so the dashboard sees one consistent naming scheme. The
|
|
17
|
+
version is captured greedily, which is what keeps "Fable 5.1" from being read
|
|
18
|
+
as "Fable 5".
|
|
19
|
+
|
|
20
|
+
Other agents are matched by a substring of the trailer, from a vendor table a
|
|
21
|
+
workspace can extend. An unrecognised trailer stays unmatched rather than
|
|
22
|
+
being guessed at: a wrong attribution is worse than a missing one.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
CLAUDE_FAMILIES = r"(?:Fable|Opus|Sonnet|Haiku|Mythos)"
|
|
28
|
+
CLAUDE_VERSION = r"\d+(?:\.\d+)?"
|
|
29
|
+
CLAUDE_CONTEXT = r"(?:\s*\((\d+[KM]) context\))?"
|
|
30
|
+
|
|
31
|
+
MODEL_FAMILY_FIRST = re.compile(
|
|
32
|
+
rf"Claude\s+({CLAUDE_FAMILIES})\s+({CLAUDE_VERSION}){CLAUDE_CONTEXT}",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
MODEL_VERSION_FIRST = re.compile(
|
|
36
|
+
rf"Claude\s+({CLAUDE_VERSION})\s+({CLAUDE_FAMILIES}){CLAUDE_CONTEXT}",
|
|
37
|
+
re.IGNORECASE,
|
|
38
|
+
)
|
|
39
|
+
COAUTHOR_TRAILER = re.compile(r"^\s*Co-Authored-By:\s*(.+)$", re.IGNORECASE | re.MULTILINE)
|
|
40
|
+
|
|
41
|
+
UNKNOWN_CLAUDE = "Claude (unknown version)"
|
|
42
|
+
|
|
43
|
+
# (substring of the trailer, reported name)
|
|
44
|
+
VENDORS = (
|
|
45
|
+
("Copilot", "Copilot"),
|
|
46
|
+
("Cursor", "Cursor"),
|
|
47
|
+
("Codex", "Codex"),
|
|
48
|
+
("Devin", "Devin"),
|
|
49
|
+
("aider", "aider"),
|
|
50
|
+
# GitHub credits accepted review suggestions to gemini-code-assist[bot]. It
|
|
51
|
+
# comes before the next row, which would otherwise claim it.
|
|
52
|
+
("gemini-code-assist", "Gemini Code Assist"),
|
|
53
|
+
# Gemini CLI adds no trailer of its own; this catches the one a person adds.
|
|
54
|
+
("Gemini", "Gemini"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def normalise_model(family, version, context):
|
|
59
|
+
name = f"Claude {family.capitalize()} {version}"
|
|
60
|
+
if context:
|
|
61
|
+
name += f" ({context.upper()})"
|
|
62
|
+
return name
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_claude_model(text):
|
|
66
|
+
"""Return the normalised Claude model name found in a co-author trailer, or None."""
|
|
67
|
+
m = MODEL_FAMILY_FIRST.search(text)
|
|
68
|
+
if m:
|
|
69
|
+
return normalise_model(m.group(1), m.group(2), m.group(3))
|
|
70
|
+
m = MODEL_VERSION_FIRST.search(text)
|
|
71
|
+
if m:
|
|
72
|
+
return normalise_model(m.group(2), m.group(1), m.group(3))
|
|
73
|
+
if re.search(r"\bClaude\b", text, re.IGNORECASE):
|
|
74
|
+
return UNKNOWN_CLAUDE
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AgentTable:
|
|
79
|
+
"""The vendor table plus a workspace's [agents].extra rows."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, extra=()):
|
|
82
|
+
self.rules = list(VENDORS) + [(e["match"], e["name"]) for e in extra]
|
|
83
|
+
|
|
84
|
+
def match(self, trailer):
|
|
85
|
+
model = parse_claude_model(trailer)
|
|
86
|
+
if model:
|
|
87
|
+
return model
|
|
88
|
+
lowered = trailer.lower()
|
|
89
|
+
for needle, name in self.rules:
|
|
90
|
+
if needle.lower() in lowered:
|
|
91
|
+
return name
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def detect(self, body):
|
|
95
|
+
"""The agent credited in a commit body via its Co-Authored-By trailers.
|
|
96
|
+
|
|
97
|
+
The first recognised trailer wins, matching the previous first-match
|
|
98
|
+
behaviour for commits that credit more than one model.
|
|
99
|
+
"""
|
|
100
|
+
if not body:
|
|
101
|
+
return None
|
|
102
|
+
for trailer in COAUTHOR_TRAILER.findall(body):
|
|
103
|
+
name = self.match(trailer)
|
|
104
|
+
if name:
|
|
105
|
+
return name
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
DEFAULT_AGENTS = AgentTable()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def detect_agent(body, table=DEFAULT_AGENTS):
|
|
113
|
+
return table.detect(body)
|