claudia-mentor 0.8.0 → 0.9.1

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A Claude Code plugin that acts as your technology mentor, security advisor, and prompt coach. Claudia fills the gaps between writing code and making good technology decisions.
6
6
 
7
- **10 knowledge domains. 14 hooks. 11 commands. 205+ tests. Beginner-friendly.**
7
+ **10 knowledge domains. 15 hooks. 11 commands. 264 tests. Beginner-friendly.**
8
8
 
9
9
  ## What Claudia Does
10
10
 
@@ -60,7 +60,7 @@ Claudia automatically activates when you:
60
60
 
61
61
  ### Hooks (Always Active)
62
62
 
63
- **7 file-check hooks** (run on every file write):
63
+ **8 file-check hooks** (run on every file write):
64
64
 
65
65
  | Hook | Type | What it catches |
66
66
  |------|------|-----------------|
@@ -71,6 +71,7 @@ Claudia automatically activates when you:
71
71
  | Git hygiene | blocks | .env writes, merge conflict markers. Warns on large binaries |
72
72
  | Accessibility | warns | Missing alt text, unlabeled inputs, icon-only buttons, div click handlers |
73
73
  | License compliance | warns | GPL/AGPL dependencies in permissive-licensed projects |
74
+ | CSS anti-patterns | warns | `!important` overuse, magic numbers, deep nesting, inline styles in templates |
74
75
 
75
76
  **7 proactive hooks** (watch your conversation):
76
77
 
@@ -86,7 +87,7 @@ Claudia automatically activates when you:
86
87
 
87
88
  ## Beginner Mode
88
89
 
89
- Set `"experience": "beginner"` in `~/.claude/claudia-context.json` (or run `/claudia:setup`) and Claudia adapts:
90
+ Set `"experience": "beginner"` in `~/.claude/claudia.json` (or `~/.claude/claudia-context.json`, or run `/claudia:setup`) and Claudia adapts:
90
91
 
91
92
  - **Simplified greeting** -- No command list on startup. Just "Claudia is here. Just build. I'm watching."
92
93
  - **Stuck detection** -- Type "I'm stuck" or "help" and she asks one clarifying question, then suggests one small next step
@@ -165,12 +166,19 @@ Override Claudia's personality and proactivity level:
165
166
  ```json
166
167
  {
167
168
  "proactivity": "high",
169
+ "experience": "beginner",
168
170
  "personality": {
169
171
  "tone": "casual"
170
- }
172
+ },
173
+ "suppress_topics": ["Netlify", "hosting"],
174
+ "suppress_hooks": ["next-steps", "milestones"]
171
175
  }
172
176
  ```
173
177
 
178
+ `suppress_topics` silences teach tips for specific keywords (e.g. `"Netlify"`) or entire categories (e.g. `"hosting"`). Case-insensitive. Each tip includes a dismiss hint showing how to add it.
179
+
180
+ `suppress_hooks` silences entire proactive hooks by name. Valid names: `teach`, `prompt-coach`, `next-steps`, `run-suggest`, `milestones`, `session-tips`, `compact-tip`. Each hook occasionally shows a dismiss hint telling you how.
181
+
174
182
  ### Per-project (`.claudia.json` in project root)
175
183
 
176
184
  ```json
@@ -37,7 +37,7 @@ def save_state(session_id, state):
37
37
 
38
38
 
39
39
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
40
- from claudia_config import load_user_config
40
+ from claudia_config import load_user_config, load_suppress_hooks
41
41
 
42
42
 
43
43
  def load_config():
@@ -56,6 +56,10 @@ def main():
56
56
  proactivity, experience = load_config()
57
57
  is_beginner = experience == "beginner"
58
58
 
59
+ # Bail if hook is suppressed
60
+ if "compact-tip" in load_suppress_hooks():
61
+ sys.exit(0)
62
+
59
63
  # Non-beginners at moderate or low proactivity: skip entirely
60
64
  if not is_beginner:
61
65
  sys.exit(0)
@@ -94,7 +94,7 @@ def save_state(state):
94
94
 
95
95
 
96
96
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
97
- from claudia_config import load_user_config
97
+ from claudia_config import load_user_config, dismiss_hint
98
98
 
99
99
 
100
100
  def load_config():
@@ -156,7 +156,8 @@ def check(input_data, proactivity, experience):
156
156
  state["achieved"] = list(achieved)
157
157
  save_state(state)
158
158
  msg = f"Claudia: {celebration}"
159
- return {"additionalContext": msg, "systemMessage": f"\033[38;5;160m{msg}\033[0m"}
159
+ user_hint, claude_hint = dismiss_hint("milestones")
160
+ return {"additionalContext": msg + "\n" + claude_hint, "systemMessage": f"\033[38;5;160m{msg}\n{user_hint}\033[0m"}
160
161
 
161
162
  # Save state even without celebration (for file_count tracking)
162
163
  if new_files > 0:
@@ -14,25 +14,28 @@ import time
14
14
 
15
15
  # Completion signals
16
16
  COMPLETION_PATTERNS = [
17
- r"I've created",
18
- r"I have created",
19
- r"I've written",
20
- r"I've built",
21
- r"I've set up",
22
- r"I've added",
23
- r"I've updated",
24
- r"I've fixed",
25
- r"I've implemented",
26
- r"Done[.!]",
27
- r"Here's your",
28
- r"Here is your",
29
- r"All set[.!]",
30
- r"That's done",
31
- r"It's ready",
32
- r"The [\w\s]+ is ready",
33
- r"Your [\w\s]+ is ready",
17
+ r"^I've created",
18
+ r"^I have created",
19
+ r"^I've written",
20
+ r"^I've built",
21
+ r"^I've set up",
22
+ r"^I've added",
23
+ r"^I've updated",
24
+ r"^I've fixed",
25
+ r"^I've implemented",
26
+ r"^Done[.!]",
27
+ r"^Here's your",
28
+ r"^Here is your",
29
+ r"^All set[.!]",
30
+ r"^That's done",
31
+ r"^It's ready",
32
+ r"^The [\w\s]+ is ready",
33
+ r"^Your [\w\s]+ is ready",
34
34
  ]
35
35
 
36
+ # Long messages are summaries, not single-action completions
37
+ MAX_COMPLETION_LENGTH = 800
38
+
36
39
  # File type -> contextual next steps
37
40
  NEXT_STEPS = {
38
41
  "html": [
@@ -123,7 +126,7 @@ def save_state(session_id, state):
123
126
 
124
127
 
125
128
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
126
- from claudia_config import load_user_config
129
+ from claudia_config import load_user_config, dismiss_hint
127
130
 
128
131
 
129
132
  def load_config():
@@ -147,8 +150,12 @@ def check(input_data, proactivity, experience):
147
150
  if state["count"] >= 3:
148
151
  return None
149
152
 
153
+ # Long messages are summaries/changelogs, not single-action completions
154
+ if len(message) > MAX_COMPLETION_LENGTH:
155
+ return None
156
+
150
157
  is_completion = any(
151
- re.search(pattern, message, re.IGNORECASE)
158
+ re.search(pattern, message, re.IGNORECASE | re.MULTILINE)
152
159
  for pattern in COMPLETION_PATTERNS
153
160
  )
154
161
 
@@ -184,9 +191,15 @@ def check(input_data, proactivity, experience):
184
191
  suggestion_text = "Claudia: What's next? Here are some ideas:\n" + "\n".join(
185
192
  f" - {step}" for step in formatted
186
193
  )
194
+ system_msg = suggestion_text
195
+ context = suggestion_text
196
+ if state["count"] % 3 == 0:
197
+ user_hint, claude_hint = dismiss_hint("next-steps")
198
+ system_msg += "\n" + user_hint
199
+ context += "\n" + claude_hint
187
200
  return {
188
- "additionalContext": suggestion_text,
189
- "systemMessage": f"\033[38;5;160m{suggestion_text}\033[0m",
201
+ "additionalContext": context,
202
+ "systemMessage": f"\033[38;5;160m{system_msg}\033[0m",
190
203
  }
191
204
 
192
205
 
@@ -64,7 +64,7 @@ def save_state(session_id, state):
64
64
 
65
65
 
66
66
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
67
- from claudia_config import load_user_config
67
+ from claudia_config import load_user_config, load_suppress_hooks, dismiss_hint
68
68
 
69
69
 
70
70
  def load_config():
@@ -85,6 +85,10 @@ def main():
85
85
 
86
86
  proactivity, experience = load_config()
87
87
 
88
+ # Bail if hook is suppressed
89
+ if "prompt-coach" in load_suppress_hooks():
90
+ sys.exit(0)
91
+
88
92
  # Skip slash commands — user is using the system correctly
89
93
  if prompt.startswith("/"):
90
94
  sys.exit(0)
@@ -144,7 +148,7 @@ def main():
144
148
 
145
149
  # Check for very short prompts (< 15 chars) that aren't slash commands
146
150
  if not coaching_note and len(prompt) < 15 and is_beginner:
147
- if not re.match(r'^(yes|no|ok|okay|sure|yeah|yep|nah|nope|thanks|ty|thx)\b', prompt, re.IGNORECASE):
151
+ if not re.match(r'^(yes|no|ok|okay|sure|yeah|yep|nah|nope|thanks|ty|thx|commit this|push it|push this|run tests|run it|do this|ship it|test it|build it|deploy it|lint it|format it|save it|merge it|revert it|undo that)\b', prompt, re.IGNORECASE):
148
152
  coaching_note = (
149
153
  "Claudia note: The user's prompt is quite short. They might benefit from "
150
154
  "a gentle nudge to be more specific. Before responding, consider asking: "
@@ -174,7 +178,12 @@ def main():
174
178
  save_state(session_id, state)
175
179
  result = {"additionalContext": coaching_note}
176
180
  if user_msg:
177
- result["systemMessage"] = f"\033[38;5;160m{user_msg}\033[0m"
181
+ system_msg = user_msg
182
+ if state["count"] % 2 == 0:
183
+ user_hint, claude_hint = dismiss_hint("prompt-coach")
184
+ system_msg += "\n" + user_hint
185
+ result["additionalContext"] += "\n" + claude_hint
186
+ result["systemMessage"] = f"\033[38;5;160m{system_msg}\033[0m"
178
187
  print(json.dumps(result))
179
188
 
180
189
  sys.exit(0)
@@ -76,7 +76,7 @@ def save_state(session_id, state):
76
76
 
77
77
 
78
78
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
79
- from claudia_config import load_user_config
79
+ from claudia_config import load_user_config, dismiss_hint
80
80
 
81
81
 
82
82
  def load_config():
@@ -98,6 +98,10 @@ def check(input_data, proactivity, experience):
98
98
  state = load_state(session_id)
99
99
  shown_types = set(state.get("shown_types", []))
100
100
 
101
+ user_hint, claude_hint = dismiss_hint("run-suggest")
102
+ hint = "\n" + user_hint
103
+ ctx_hint = "\n" + claude_hint
104
+
101
105
  # Check for package.json mentions
102
106
  if "package.json" not in shown_types and re.search(PACKAGE_JSON_PATTERN, message, re.IGNORECASE):
103
107
  shown_types.add("package.json")
@@ -105,7 +109,7 @@ def check(input_data, proactivity, experience):
105
109
  save_state(session_id, state)
106
110
  suggestion = RUN_SUGGESTIONS["package.json"][1]
107
111
  msg = f"Claudia: {suggestion}"
108
- return {"additionalContext": msg, "systemMessage": f"\033[38;5;160m{msg}\033[0m"}
112
+ return {"additionalContext": msg + ctx_hint, "systemMessage": f"\033[38;5;160m{msg}{hint}\033[0m"}
109
113
 
110
114
  # Check for file creation patterns
111
115
  for pattern in FILE_PATTERNS:
@@ -119,7 +123,7 @@ def check(input_data, proactivity, experience):
119
123
  save_state(session_id, state)
120
124
  suggestion = RUN_SUGGESTIONS[ext][1].format(filename=filename)
121
125
  msg = f"Claudia: {suggestion}"
122
- return {"additionalContext": msg, "systemMessage": f"\033[38;5;160m{msg}\033[0m"}
126
+ return {"additionalContext": msg + ctx_hint, "systemMessage": f"\033[38;5;160m{msg}{hint}\033[0m"}
123
127
 
124
128
  return None
125
129
 
@@ -154,7 +154,7 @@ def save_state(session_id, state):
154
154
 
155
155
 
156
156
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
157
- from claudia_config import load_user_config, load_project_context
157
+ from claudia_config import load_user_config, load_project_context, load_suppress_hooks
158
158
 
159
159
 
160
160
  def load_config():
@@ -173,6 +173,10 @@ def main():
173
173
  proactivity, experience = load_config()
174
174
  is_beginner = experience == "beginner"
175
175
 
176
+ # Bail if hook is suppressed
177
+ if "session-tips" in load_suppress_hooks():
178
+ sys.exit(0)
179
+
176
180
  # Low proactivity: skip tips, but still show greeting on startup
177
181
  if proactivity == "low" and source != "startup":
178
182
  sys.exit(0)
@@ -12,7 +12,7 @@ import sys
12
12
  import time
13
13
 
14
14
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
15
- from claudia_config import load_user_config
15
+ from claudia_config import load_user_config, load_suppress_topics, load_suppress_hooks
16
16
 
17
17
  # Import check() from each Stop hook module
18
18
  import importlib
@@ -56,9 +56,22 @@ def main():
56
56
 
57
57
  session_id = input_data.get("session_id", "default")
58
58
  proactivity, experience = load_user_config()
59
+ input_data["suppress_topics"] = load_suppress_topics()
60
+ suppress_hooks = load_suppress_hooks()
61
+
62
+ # Map module names to hook names for suppress check
63
+ MODULE_TO_HOOK = {
64
+ "claudia-milestones": "milestones",
65
+ "claudia-run-suggest": "run-suggest",
66
+ "claudia-next-steps": "next-steps",
67
+ "claudia-teach": "teach",
68
+ }
59
69
 
60
70
  # Try each hook in order; first with output wins
61
71
  for module_name in HOOK_MODULES:
72
+ hook_name = MODULE_TO_HOOK.get(module_name, module_name)
73
+ if hook_name in suppress_hooks:
74
+ continue
62
75
  try:
63
76
  mod = _import_hook(module_name)
64
77
  result = mod.check(input_data, proactivity, experience)
@@ -237,7 +237,7 @@ def save_state(session_id, state):
237
237
 
238
238
 
239
239
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
240
- from claudia_config import load_user_config
240
+ from claudia_config import load_user_config, dismiss_hint
241
241
 
242
242
 
243
243
  def load_config():
@@ -265,16 +265,25 @@ def check(input_data, proactivity, experience):
265
265
  revealed_commands = set(state["revealed_commands"])
266
266
  tips = []
267
267
 
268
+ # Build suppressed topics set (case-insensitive)
269
+ suppress_topics = input_data.get("suppress_topics", [])
270
+ suppressed = {t.lower() for t in suppress_topics if isinstance(t, str)}
271
+
268
272
  # Scan for technology keywords
269
273
  for category, keywords in KEYWORDS.items():
274
+ if category.lower() in suppressed:
275
+ continue
270
276
  for keyword, description in keywords.items():
277
+ if keyword.lower() in suppressed:
278
+ continue
271
279
  pattern = r'\b' + re.escape(keyword) + r'\b'
272
280
  if re.search(pattern, message, re.IGNORECASE):
273
281
  if keyword.lower() not in shown_keywords:
274
282
  shown_keywords.add(keyword.lower())
275
283
  tips.append(
276
284
  f"I noticed we're talking about {keyword} ({description}). "
277
- f"Want me to explain more? Just say `/claudia:explain {keyword.lower()}`"
285
+ f"Want me to explain more? Just say `/claudia:explain {keyword.lower()}`\n"
286
+ f"(Say \"stop tips about {keyword}\" to silence this)"
278
287
  )
279
288
  break
280
289
  if tips:
@@ -312,8 +321,18 @@ def check(input_data, proactivity, experience):
312
321
  state["revealed_commands"] = list(revealed_commands)
313
322
  save_state(session_id, state)
314
323
  tip_text = "\n".join(f"Claudia: {tip}" for tip in tips)
315
- colored = "\n".join(f"\033[38;5;160m{line}\033[0m" for line in tip_text.split("\n"))
316
- return {"additionalContext": tip_text, "systemMessage": colored}
324
+ system_text = tip_text
325
+ context = tip_text + (
326
+ "\nIf the user asks to stop tips about a keyword or category, "
327
+ "add it to the \"suppress_topics\" list in ~/.claude/claudia.json "
328
+ "(create the file/key if missing)."
329
+ )
330
+ if len(shown_keywords) % 3 == 0:
331
+ user_hint, claude_hint = dismiss_hint("teach")
332
+ system_text += "\n" + user_hint
333
+ context += "\n" + claude_hint
334
+ colored = "\n".join(f"\033[38;5;160m{line}\033[0m" for line in system_text.split("\n"))
335
+ return {"additionalContext": context, "systemMessage": colored}
317
336
 
318
337
  if shown_keywords != set(state["shown_keywords"]) or revealed_commands != set(state["revealed_commands"]):
319
338
  state["shown_keywords"] = list(shown_keywords)
@@ -47,8 +47,57 @@ def resolve_project():
47
47
 
48
48
  # --- User Config ---
49
49
 
50
+ def load_suppress_topics():
51
+ """Load suppress_topics list from ~/.claude/claudia.json.
52
+
53
+ Returns:
54
+ list of topic strings to suppress (empty if not set or file missing).
55
+ """
56
+ config_path = os.path.expanduser("~/.claude/claudia.json")
57
+ if os.path.exists(config_path):
58
+ try:
59
+ with open(config_path) as f:
60
+ data = json.load(f)
61
+ topics = data.get("suppress_topics", [])
62
+ if isinstance(topics, list):
63
+ return topics
64
+ except (json.JSONDecodeError, IOError):
65
+ pass
66
+ return []
67
+
68
+
69
+ def load_suppress_hooks():
70
+ """Load suppress_hooks list from ~/.claude/claudia.json.
71
+
72
+ Returns:
73
+ set of lowercase hook name strings to suppress (empty if not set or file missing).
74
+ """
75
+ config_path = os.path.expanduser("~/.claude/claudia.json")
76
+ if os.path.exists(config_path):
77
+ try:
78
+ with open(config_path) as f:
79
+ data = json.load(f)
80
+ hooks = data.get("suppress_hooks", [])
81
+ if isinstance(hooks, list):
82
+ return {h.lower() for h in hooks if isinstance(h, str)}
83
+ except (json.JSONDecodeError, IOError):
84
+ pass
85
+ return set()
86
+
87
+
88
+ def dismiss_hint(hook_name):
89
+ """Return a user-visible dismiss hint and Claude-visible instruction."""
90
+ user_hint = f'(Say "silence {hook_name}" to turn this off)'
91
+ claude_hint = (
92
+ f'If the user asks to silence or stop {hook_name} tips, '
93
+ f'add "{hook_name}" to the "suppress_hooks" list in ~/.claude/claudia.json '
94
+ f'(create the file/key if missing).'
95
+ )
96
+ return user_hint, claude_hint
97
+
98
+
50
99
  def load_user_config():
51
- """Load proactivity from ~/.claude/claudia.json, experience from context.
100
+ """Load proactivity and experience from ~/.claude/claudia.json, fallback to context.
52
101
 
53
102
  Returns:
54
103
  (proactivity, experience) tuple with string values.
@@ -58,17 +107,22 @@ def load_user_config():
58
107
  experience = "intermediate"
59
108
 
60
109
  config_path = os.path.expanduser("~/.claude/claudia.json")
110
+ experience_set = False
61
111
  if os.path.exists(config_path):
62
112
  try:
63
113
  with open(config_path) as f:
64
114
  data = json.load(f)
65
115
  proactivity = data.get("proactivity", proactivity)
116
+ if "experience" in data:
117
+ experience = data["experience"]
118
+ experience_set = True
66
119
  except (json.JSONDecodeError, IOError):
67
120
  pass
68
121
 
69
- # Experience comes from project context (tries project-scoped first, then global)
70
- ctx = load_project_context()
71
- experience = ctx.get("experience", experience)
122
+ # Experience falls back to project context if not set in claudia.json
123
+ if not experience_set:
124
+ ctx = load_project_context()
125
+ experience = ctx.get("experience", experience)
72
126
 
73
127
  return proactivity, experience
74
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudia-mentor",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Proactive technology mentor, security advisor, and prompt coach for Claude Code",
5
5
  "author": "Regan O'Malley <regan@reganomalley.com>",
6
6
  "license": "MIT",