claudia-mentor 0.5.3 → 0.5.4
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.
- package/hooks/hooks.json +2 -2
- package/hooks/scripts/check-accessibility.py +1 -1
- package/hooks/scripts/check-deps.py +1 -1
- package/hooks/scripts/check-dockerfile.py +1 -1
- package/hooks/scripts/check-git-hygiene.py +1 -1
- package/hooks/scripts/check-license.py +1 -1
- package/hooks/scripts/check-practices.py +1 -1
- package/hooks/scripts/check-secrets.sh +7 -5
- package/hooks/scripts/claudia-compact-tip.py +1 -1
- package/hooks/scripts/claudia-milestones.py +27 -6
- package/hooks/scripts/claudia-next-steps.py +23 -1
- package/hooks/scripts/claudia-prompt-coach.py +1 -1
- package/hooks/scripts/claudia-run-suggest.py +25 -2
- package/hooks/scripts/claudia-session-tips.py +2 -1
- package/hooks/scripts/claudia-teach.py +27 -6
- package/package.json +1 -1
package/hooks/hooks.json
CHANGED
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"hooks": [
|
|
79
79
|
{
|
|
80
80
|
"type": "command",
|
|
81
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/claudia-
|
|
81
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/claudia-milestones.py",
|
|
82
82
|
"timeout": 10
|
|
83
83
|
}
|
|
84
84
|
]
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"hooks": [
|
|
106
106
|
{
|
|
107
107
|
"type": "command",
|
|
108
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/claudia-
|
|
108
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/claudia-teach.py",
|
|
109
109
|
"timeout": 10
|
|
110
110
|
}
|
|
111
111
|
]
|
|
@@ -159,7 +159,7 @@ def main():
|
|
|
159
159
|
if warnings:
|
|
160
160
|
save_state(session_id, shown)
|
|
161
161
|
message = "Claudia noticed some accessibility concerns:\n" + "\n".join(warnings)
|
|
162
|
-
output = json.dumps({"systemMessage": message})
|
|
162
|
+
output = json.dumps({"systemMessage": f"\033[38;5;209m{message}\033[0m"})
|
|
163
163
|
print(output)
|
|
164
164
|
|
|
165
165
|
sys.exit(0)
|
|
@@ -109,7 +109,7 @@ def main():
|
|
|
109
109
|
if warnings:
|
|
110
110
|
save_state(session_id, shown)
|
|
111
111
|
message = "Claudia noticed some dependency concerns:\n" + "\n".join(warnings)
|
|
112
|
-
output = json.dumps({"systemMessage": message})
|
|
112
|
+
output = json.dumps({"systemMessage": f"\033[38;5;209m{message}\033[0m"})
|
|
113
113
|
print(output)
|
|
114
114
|
|
|
115
115
|
sys.exit(0)
|
|
@@ -166,7 +166,7 @@ def main():
|
|
|
166
166
|
if warnings:
|
|
167
167
|
save_state(session_id, shown)
|
|
168
168
|
message = "Claudia noticed some Dockerfile patterns worth reviewing:\n" + "\n".join(warnings)
|
|
169
|
-
output = json.dumps({"systemMessage": message})
|
|
169
|
+
output = json.dumps({"systemMessage": f"\033[38;5;209m{message}\033[0m"})
|
|
170
170
|
print(output)
|
|
171
171
|
|
|
172
172
|
sys.exit(0)
|
|
@@ -134,7 +134,7 @@ def main():
|
|
|
134
134
|
if warnings:
|
|
135
135
|
save_state(session_id, shown)
|
|
136
136
|
message = "Claudia noticed a git hygiene concern:\n" + "\n".join(warnings)
|
|
137
|
-
output = json.dumps({"systemMessage": message})
|
|
137
|
+
output = json.dumps({"systemMessage": f"\033[38;5;209m{message}\033[0m"})
|
|
138
138
|
print(output)
|
|
139
139
|
|
|
140
140
|
sys.exit(0)
|
|
@@ -156,7 +156,7 @@ def main():
|
|
|
156
156
|
if warnings:
|
|
157
157
|
save_state(session_id, shown)
|
|
158
158
|
message = "Claudia noticed some license concerns:\n" + "\n".join(warnings)
|
|
159
|
-
output = json.dumps({"systemMessage": message})
|
|
159
|
+
output = json.dumps({"systemMessage": f"\033[38;5;209m{message}\033[0m"})
|
|
160
160
|
print(output)
|
|
161
161
|
|
|
162
162
|
sys.exit(0)
|
|
@@ -159,7 +159,7 @@ def main():
|
|
|
159
159
|
save_state(session_id, shown)
|
|
160
160
|
# Advisory output via JSON on stdout (systemMessage)
|
|
161
161
|
message = "Claudia noticed some patterns worth reviewing:\n" + "\n".join(warnings)
|
|
162
|
-
output = json.dumps({"systemMessage": message})
|
|
162
|
+
output = json.dumps({"systemMessage": f"\033[38;5;209m{message}\033[0m"})
|
|
163
163
|
print(output)
|
|
164
164
|
|
|
165
165
|
sys.exit(0)
|
|
@@ -99,15 +99,17 @@ with open('$STATE_FILE', 'w') as f:
|
|
|
99
99
|
echo "[\"${WARNING_KEY}\"]" > "$STATE_FILE"
|
|
100
100
|
fi
|
|
101
101
|
|
|
102
|
-
# Block with explanation
|
|
103
|
-
|
|
102
|
+
# Block with explanation (vermillion colored)
|
|
103
|
+
C="\033[38;5;209m"
|
|
104
|
+
R="\033[0m"
|
|
105
|
+
echo -e "${C}Claudia: ${description} in ${FILE_PATH}.${R}" >&2
|
|
104
106
|
echo "" >&2
|
|
105
|
-
echo "Secrets should never be hardcoded in source files. Use:" >&2
|
|
107
|
+
echo -e "${C}Secrets should never be hardcoded in source files. Use:" >&2
|
|
106
108
|
echo " - Environment variables (.env file, gitignored)" >&2
|
|
107
109
|
echo " - A secret manager (AWS SSM, Vault, Doppler)" >&2
|
|
108
|
-
echo " - CI/CD secrets for pipelines" >&2
|
|
110
|
+
echo -e " - CI/CD secrets for pipelines${R}" >&2
|
|
109
111
|
echo "" >&2
|
|
110
|
-
echo "If this is intentionally a test fixture or example, rename the file to include 'test', 'example', or 'fixture'
|
|
112
|
+
echo -e "${C}If this is intentionally a test fixture or example, rename the file to include 'test', 'example', or 'fixture'.${R}" >&2
|
|
111
113
|
exit 2
|
|
112
114
|
fi
|
|
113
115
|
done
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
12
|
import sys
|
|
13
|
+
import time
|
|
13
14
|
|
|
14
15
|
# Milestone definitions: key, detection pattern, celebration message
|
|
15
16
|
MILESTONES = {
|
|
@@ -55,6 +56,24 @@ MILESTONES = {
|
|
|
55
56
|
STATE_FILE = os.path.expanduser("~/.claude/claudia-milestones.json")
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
def stop_lock_acquire(session_id):
|
|
60
|
+
"""Try to acquire the per-turn Stop hook lock. Returns True if acquired."""
|
|
61
|
+
lock_file = os.path.expanduser(f"~/.claude/claudia_stop_lock_{session_id}.tmp")
|
|
62
|
+
now = time.time()
|
|
63
|
+
try:
|
|
64
|
+
if os.path.exists(lock_file):
|
|
65
|
+
with open(lock_file) as f:
|
|
66
|
+
ts = float(f.read().strip())
|
|
67
|
+
if now - ts < 2.0:
|
|
68
|
+
return False # Another hook already claimed this turn
|
|
69
|
+
os.makedirs(os.path.dirname(lock_file), exist_ok=True)
|
|
70
|
+
with open(lock_file, "w") as f:
|
|
71
|
+
f.write(str(now))
|
|
72
|
+
return True
|
|
73
|
+
except (IOError, ValueError):
|
|
74
|
+
return True # On error, let it through
|
|
75
|
+
|
|
76
|
+
|
|
58
77
|
def load_state():
|
|
59
78
|
if os.path.exists(STATE_FILE):
|
|
60
79
|
try:
|
|
@@ -147,12 +166,14 @@ def main():
|
|
|
147
166
|
if celebration:
|
|
148
167
|
state["achieved"] = list(achieved)
|
|
149
168
|
save_state(state)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
session_id = input_data.get("session_id", "default")
|
|
170
|
+
if stop_lock_acquire(session_id):
|
|
171
|
+
msg = f"Claudia: {celebration}"
|
|
172
|
+
output = json.dumps({
|
|
173
|
+
"additionalContext": msg,
|
|
174
|
+
"systemMessage": f"\033[38;5;209m{msg}\033[0m",
|
|
175
|
+
})
|
|
176
|
+
print(output)
|
|
156
177
|
|
|
157
178
|
# Save state even without celebration (for file_count tracking)
|
|
158
179
|
elif new_files > 0:
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
12
|
import sys
|
|
13
|
+
import time
|
|
13
14
|
|
|
14
15
|
# Completion signals
|
|
15
16
|
COMPLETION_PATTERNS = [
|
|
@@ -74,6 +75,24 @@ NEXT_STEPS = {
|
|
|
74
75
|
],
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
def stop_lock_acquire(session_id):
|
|
79
|
+
"""Try to acquire the per-turn Stop hook lock. Returns True if acquired."""
|
|
80
|
+
lock_file = os.path.expanduser(f"~/.claude/claudia_stop_lock_{session_id}.tmp")
|
|
81
|
+
now = time.time()
|
|
82
|
+
try:
|
|
83
|
+
if os.path.exists(lock_file):
|
|
84
|
+
with open(lock_file) as f:
|
|
85
|
+
ts = float(f.read().strip())
|
|
86
|
+
if now - ts < 2.0:
|
|
87
|
+
return False
|
|
88
|
+
os.makedirs(os.path.dirname(lock_file), exist_ok=True)
|
|
89
|
+
with open(lock_file, "w") as f:
|
|
90
|
+
f.write(str(now))
|
|
91
|
+
return True
|
|
92
|
+
except (IOError, ValueError):
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
77
96
|
# Pattern to extract filenames from the message
|
|
78
97
|
FILENAME_PATTERN = r"[`'\"]?(\S+\.(\w{1,4}))[`'\"]?"
|
|
79
98
|
|
|
@@ -174,6 +193,9 @@ def main():
|
|
|
174
193
|
else:
|
|
175
194
|
formatted.append(step.replace(" {filename}", "").replace("{filename}", "the file"))
|
|
176
195
|
|
|
196
|
+
if not stop_lock_acquire(session_id):
|
|
197
|
+
sys.exit(0)
|
|
198
|
+
|
|
177
199
|
state["count"] += 1
|
|
178
200
|
save_state(session_id, state)
|
|
179
201
|
|
|
@@ -182,7 +204,7 @@ def main():
|
|
|
182
204
|
)
|
|
183
205
|
output = json.dumps({
|
|
184
206
|
"additionalContext": suggestion_text,
|
|
185
|
-
"systemMessage": suggestion_text,
|
|
207
|
+
"systemMessage": f"\033[38;5;209m{suggestion_text}\033[0m",
|
|
186
208
|
})
|
|
187
209
|
print(output)
|
|
188
210
|
sys.exit(0)
|
|
@@ -176,7 +176,7 @@ def main():
|
|
|
176
176
|
save_state(session_id, state)
|
|
177
177
|
result = {"additionalContext": coaching_note}
|
|
178
178
|
if user_msg:
|
|
179
|
-
result["systemMessage"] = user_msg
|
|
179
|
+
result["systemMessage"] = f"\033[38;5;209m{user_msg}\033[0m"
|
|
180
180
|
print(json.dumps(result))
|
|
181
181
|
|
|
182
182
|
sys.exit(0)
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
12
|
import sys
|
|
13
|
+
import time
|
|
13
14
|
|
|
14
15
|
# File type -> run suggestion
|
|
15
16
|
RUN_SUGGESTIONS = {
|
|
@@ -28,6 +29,24 @@ FILE_PATTERNS = [
|
|
|
28
29
|
r"(?:new file|writing to|saved to)\s+[`'\"]?(\S+\.(\w+))[`'\"]?",
|
|
29
30
|
]
|
|
30
31
|
|
|
32
|
+
def stop_lock_acquire(session_id):
|
|
33
|
+
"""Try to acquire the per-turn Stop hook lock. Returns True if acquired."""
|
|
34
|
+
lock_file = os.path.expanduser(f"~/.claude/claudia_stop_lock_{session_id}.tmp")
|
|
35
|
+
now = time.time()
|
|
36
|
+
try:
|
|
37
|
+
if os.path.exists(lock_file):
|
|
38
|
+
with open(lock_file) as f:
|
|
39
|
+
ts = float(f.read().strip())
|
|
40
|
+
if now - ts < 2.0:
|
|
41
|
+
return False
|
|
42
|
+
os.makedirs(os.path.dirname(lock_file), exist_ok=True)
|
|
43
|
+
with open(lock_file, "w") as f:
|
|
44
|
+
f.write(str(now))
|
|
45
|
+
return True
|
|
46
|
+
except (IOError, ValueError):
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
31
50
|
PACKAGE_JSON_PATTERN = r"(?:created|wrote|updated|modified)\s+[`'\"]?package\.json[`'\"]?"
|
|
32
51
|
|
|
33
52
|
|
|
@@ -105,12 +124,14 @@ def main():
|
|
|
105
124
|
|
|
106
125
|
# Check for package.json mentions
|
|
107
126
|
if "package.json" not in shown_types and re.search(PACKAGE_JSON_PATTERN, message, re.IGNORECASE):
|
|
127
|
+
if not stop_lock_acquire(session_id):
|
|
128
|
+
sys.exit(0)
|
|
108
129
|
shown_types.add("package.json")
|
|
109
130
|
state["shown_types"] = list(shown_types)
|
|
110
131
|
save_state(session_id, state)
|
|
111
132
|
suggestion = RUN_SUGGESTIONS["package.json"][1]
|
|
112
133
|
msg = f"Claudia: {suggestion}"
|
|
113
|
-
output = json.dumps({"additionalContext": msg, "systemMessage": msg})
|
|
134
|
+
output = json.dumps({"additionalContext": msg, "systemMessage": f"\033[38;5;209m{msg}\033[0m"})
|
|
114
135
|
print(output)
|
|
115
136
|
sys.exit(0)
|
|
116
137
|
|
|
@@ -121,12 +142,14 @@ def main():
|
|
|
121
142
|
filename = match.group(1)
|
|
122
143
|
ext = match.group(2).lower()
|
|
123
144
|
if ext in RUN_SUGGESTIONS and ext not in shown_types:
|
|
145
|
+
if not stop_lock_acquire(session_id):
|
|
146
|
+
sys.exit(0)
|
|
124
147
|
shown_types.add(ext)
|
|
125
148
|
state["shown_types"] = list(shown_types)
|
|
126
149
|
save_state(session_id, state)
|
|
127
150
|
suggestion = RUN_SUGGESTIONS[ext][1].format(filename=filename)
|
|
128
151
|
msg = f"Claudia: {suggestion}"
|
|
129
|
-
output = json.dumps({"additionalContext": msg, "systemMessage": msg})
|
|
152
|
+
output = json.dumps({"additionalContext": msg, "systemMessage": f"\033[38;5;209m{msg}\033[0m"})
|
|
130
153
|
print(output)
|
|
131
154
|
sys.exit(0)
|
|
132
155
|
|
|
@@ -188,7 +188,8 @@ def main():
|
|
|
188
188
|
if tip:
|
|
189
189
|
visible_parts.append(tip)
|
|
190
190
|
if visible_parts:
|
|
191
|
-
|
|
191
|
+
msg = " | ".join(visible_parts) if greeting and tip else visible_parts[0]
|
|
192
|
+
result["systemMessage"] = f"\033[38;5;209m{msg}\033[0m"
|
|
192
193
|
print(json.dumps(result))
|
|
193
194
|
|
|
194
195
|
sys.exit(0)
|
|
@@ -12,6 +12,7 @@ import json
|
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
14
|
import sys
|
|
15
|
+
import time
|
|
15
16
|
|
|
16
17
|
# Technology keywords by category
|
|
17
18
|
KEYWORDS = {
|
|
@@ -77,6 +78,24 @@ ERROR_PATTERNS = [
|
|
|
77
78
|
(r'\bReferenceError\b', "a reference error — usually a typo or missing variable"),
|
|
78
79
|
]
|
|
79
80
|
|
|
81
|
+
def stop_lock_acquire(session_id):
|
|
82
|
+
"""Try to acquire the per-turn Stop hook lock. Returns True if acquired."""
|
|
83
|
+
lock_file = os.path.expanduser(f"~/.claude/claudia_stop_lock_{session_id}.tmp")
|
|
84
|
+
now = time.time()
|
|
85
|
+
try:
|
|
86
|
+
if os.path.exists(lock_file):
|
|
87
|
+
with open(lock_file) as f:
|
|
88
|
+
ts = float(f.read().strip())
|
|
89
|
+
if now - ts < 2.0:
|
|
90
|
+
return False
|
|
91
|
+
os.makedirs(os.path.dirname(lock_file), exist_ok=True)
|
|
92
|
+
with open(lock_file, "w") as f:
|
|
93
|
+
f.write(str(now))
|
|
94
|
+
return True
|
|
95
|
+
except (IOError, ValueError):
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
80
99
|
# Contextual command reveals for beginners
|
|
81
100
|
COMMAND_REVEALS = {
|
|
82
101
|
"file_written": {
|
|
@@ -276,12 +295,14 @@ def main():
|
|
|
276
295
|
state["shown_keywords"] = list(shown_keywords)
|
|
277
296
|
state["revealed_commands"] = list(revealed_commands)
|
|
278
297
|
save_state(session_id, state)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
298
|
+
if stop_lock_acquire(session_id):
|
|
299
|
+
tip_text = "\n".join(f"Claudia: {tip}" for tip in tips)
|
|
300
|
+
colored = "\n".join(f"\033[38;5;209m{line}\033[0m" for line in tip_text.split("\n"))
|
|
301
|
+
output = json.dumps({
|
|
302
|
+
"additionalContext": tip_text,
|
|
303
|
+
"systemMessage": colored,
|
|
304
|
+
})
|
|
305
|
+
print(output)
|
|
285
306
|
elif shown_keywords != set(state["shown_keywords"]) or revealed_commands != set(state["revealed_commands"]):
|
|
286
307
|
state["shown_keywords"] = list(shown_keywords)
|
|
287
308
|
state["revealed_commands"] = list(revealed_commands)
|
package/package.json
CHANGED