xtrm-tools 2.1.16 → 2.1.17

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/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
package/config/hooks.json CHANGED
@@ -1,16 +1,10 @@
1
1
  {
2
2
  "hooks": {
3
- "UserPromptSubmit": [
3
+ "SessionStart": [
4
4
  {
5
- "script": "skill-suggestion.py",
5
+ "script": "beads-compact-restore.mjs",
6
6
  "timeout": 5000
7
7
  },
8
- {
9
- "script": "gitnexus-impact-reminder.py",
10
- "timeout": 5000
11
- }
12
- ],
13
- "SessionStart": [
14
8
  {
15
9
  "script": "serena-workflow-reminder.py"
16
10
  }
@@ -30,15 +24,6 @@
30
24
  "matcher": "Read|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
31
25
  "script": "serena-workflow-reminder.py"
32
26
  },
33
- {
34
- "matcher": "Bash",
35
- "script": "type-safety-enforcement.py",
36
- "timeout": 30000
37
- },
38
- {
39
- "matcher": "Edit|Write|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
40
- "script": "type-safety-enforcement.py"
41
- },
42
27
  {
43
28
  "matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
44
29
  "script": "beads-edit-gate.mjs",
@@ -71,11 +56,14 @@
71
56
  "script": "beads-memory-gate.mjs",
72
57
  "timeout": 8000
73
58
  }
59
+ ],
60
+ "PreCompact": [
61
+ {
62
+ "script": "beads-compact-save.mjs",
63
+ "timeout": 5000
64
+ }
74
65
  ]
75
66
  },
76
- "skillSuggestions": {
77
- "enabled": true
78
- },
79
67
  "statusLine": {
80
68
  "script": "statusline-starship.sh"
81
69
  }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code SessionStart hook — restore in_progress beads issues after context compaction.
3
+ // Reads .beads/.last_active (written by beads-compact-save.mjs), reinstates statuses,
4
+ // deletes the marker, and injects a brief agent context message.
5
+ // Exit 0 in all paths (informational only).
6
+ //
7
+ // Installed by: xtrm install
8
+
9
+ import { execSync } from 'node:child_process';
10
+ import { readFileSync, existsSync, unlinkSync } from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ let input;
14
+ try {
15
+ input = JSON.parse(readFileSync(0, 'utf8'));
16
+ } catch {
17
+ process.exit(0);
18
+ }
19
+
20
+ const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
21
+ const lastActivePath = path.join(cwd, '.beads', '.last_active');
22
+
23
+ if (!existsSync(lastActivePath)) process.exit(0);
24
+
25
+ const ids = readFileSync(lastActivePath, 'utf8').trim().split('\n').filter(Boolean);
26
+
27
+ // Clean up regardless of whether restore succeeds
28
+ unlinkSync(lastActivePath);
29
+
30
+ if (ids.length === 0) process.exit(0);
31
+
32
+ let restored = 0;
33
+ for (const id of ids) {
34
+ try {
35
+ execSync(`bd update ${id} --status in_progress`, {
36
+ encoding: 'utf8',
37
+ cwd,
38
+ stdio: ['pipe', 'pipe', 'pipe'],
39
+ timeout: 5000,
40
+ });
41
+ restored++;
42
+ } catch {
43
+ // ignore — issue may no longer exist
44
+ }
45
+ }
46
+
47
+ if (restored > 0) {
48
+ process.stdout.write(
49
+ JSON.stringify({
50
+ hookSpecificOutput: {
51
+ hookEventName: 'SessionStart',
52
+ additionalSystemPrompt:
53
+ `Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction. Check \`bd list\` for details.`,
54
+ },
55
+ }) + '\n',
56
+ );
57
+ }
58
+
59
+ process.exit(0);
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code PreCompact hook — save in_progress beads issues before context is compacted.
3
+ // Writes issue IDs to .beads/.last_active so beads-compact-restore.mjs can reinstate them.
4
+ // Exit 0 in all paths (informational only).
5
+ //
6
+ // Installed by: xtrm install
7
+
8
+ import { execSync } from 'node:child_process';
9
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ let input;
13
+ try {
14
+ input = JSON.parse(readFileSync(0, 'utf8'));
15
+ } catch {
16
+ process.exit(0);
17
+ }
18
+
19
+ const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
20
+ const beadsDir = path.join(cwd, '.beads');
21
+
22
+ if (!existsSync(beadsDir)) process.exit(0);
23
+
24
+ let output = '';
25
+ try {
26
+ output = execSync('bd list --status=in_progress', {
27
+ encoding: 'utf8',
28
+ cwd,
29
+ stdio: ['pipe', 'pipe', 'pipe'],
30
+ timeout: 8000,
31
+ }).trim();
32
+ } catch {
33
+ process.exit(0);
34
+ }
35
+
36
+ // Parse issue IDs — lines like "◐ proj-abc123 ● P1 Title"
37
+ const ids = [];
38
+ for (const line of output.split('\n')) {
39
+ const match = line.trim().match(/^[○◐●✓❄]\s+([\w-]+)\s/u);
40
+ if (match) ids.push(match[1]);
41
+ }
42
+
43
+ if (ids.length === 0) process.exit(0);
44
+
45
+ writeFileSync(path.join(beadsDir, '.last_active'), ids.join('\n') + '\n', 'utf8');
46
+ process.exit(0);
@@ -59,13 +59,11 @@ const explicitlyProtectedTarget = protectedBranches
59
59
  .some((b) => lastToken === b || lastToken.endsWith(`:${b}`));
60
60
  if (explicitlyProtectedTarget) process.exit(0);
61
61
 
62
- process.stdout.write(JSON.stringify({
63
- systemMessage:
64
- `✅ Pushed '${branch}'. Next workflow steps:\n\n` +
65
- ' 1. gh pr create --fill\n' +
66
- ' 2. gh pr merge --squash\n' +
67
- ' 3. git checkout main && git reset --hard origin/main\n\n' +
68
- 'Before/after merge, ensure beads state is updated (e.g. bd close <id>).',
69
- }));
70
- process.stdout.write('\n');
62
+ process.stdout.write(
63
+ `✅ Pushed '${branch}'. Next workflow steps:\n\n` +
64
+ ' 1. gh pr create --fill\n' +
65
+ ' 2. gh pr merge --squash\n' +
66
+ ' 3. git checkout main && git reset --hard origin/main\n\n' +
67
+ 'Before/after merge, ensure beads state is updated (e.g. bd close <id>).\n',
68
+ );
71
69
  process.exit(0);
@@ -120,6 +120,7 @@ if (tool === 'Bash') {
120
120
  ...protectedBranches.map(b => new RegExp(`^git\\s+reset\\s+--hard\\s+origin/${b}\\b`)),
121
121
  /^gh\s+/,
122
122
  /^bd\s+/,
123
+ /^touch\s+\.beads\//,
123
124
  ];
124
125
 
125
126
  if (SAFE_BASH_PATTERNS.some(p => p.test(cmd))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -140,7 +140,17 @@ def check_mypy(file_path: str, project_root: str) -> tuple[bool, list[str]]:
140
140
 
141
141
  log_info('Running Mypy type checking...')
142
142
 
143
- cmd = ['mypy', '--pretty', file_path]
143
+ # Build mypy command with strictness flags
144
+ # Default: --disallow-untyped-defs catches untyped function parameters
145
+ # Opt-in: CLAUDE_HOOKS_MYPY_STRICT=true enables full --strict mode
146
+ mypy_strict = os.environ.get('CLAUDE_HOOKS_MYPY_STRICT', 'false').lower() == 'true'
147
+
148
+ if mypy_strict:
149
+ cmd = ['mypy', '--strict', '--pretty', file_path]
150
+ log_debug('Running mypy with --strict (full strictness)')
151
+ else:
152
+ cmd = ['mypy', '--disallow-untyped-defs', '--pretty', file_path]
153
+ log_debug('Running mypy with --disallow-untyped-defs (baseline strictness)')
144
154
  try:
145
155
  result = subprocess.run(cmd, capture_output=True, text=True, cwd=project_root)
146
156
 
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import sys
4
- import os
5
- import re
6
-
7
- # Add script directory to path to allow importing shared modules
8
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
9
- from agent_context import AgentContext
10
-
11
- def get_first_sentence(text):
12
- if not text:
13
- return ""
14
- # Remove newlines and extra spaces
15
- text = re.sub(r'\s+', ' ', text).strip()
16
- # Find the first period followed by a space or end of string
17
- match = re.search(r'^(.*?)[.!?](\s|$)', text)
18
- if match:
19
- return match.group(0).strip()
20
- return text
21
-
22
- def parse_skill_md(file_path):
23
- try:
24
- with open(file_path, 'r') as f:
25
- content = f.read()
26
-
27
- # Extract YAML frontmatter
28
- match = re.search(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
29
- if match:
30
- frontmatter_raw = match.group(1)
31
- # Basic YAML-like parsing using regex to avoid external dependency (PyYAML)
32
- name_match = re.search(r'^name:\s*(.*)$', frontmatter_raw, re.MULTILINE)
33
- desc_match = re.search(r'^description:\s*(?:>-\s*)?\n?\s*(.*)$', frontmatter_raw, re.MULTILINE | re.DOTALL)
34
-
35
- name = name_match.group(1).strip() if name_match else os.path.basename(os.path.dirname(file_path))
36
- description = ""
37
-
38
- if desc_match:
39
- desc_raw = desc_match.group(1).strip()
40
- # If it was a folded scalar, it might have multiple lines
41
- if '\n' in desc_raw:
42
- # Capture until next YAML key or end
43
- description = desc_raw.split('\n')[0].strip()
44
- else:
45
- description = desc_raw
46
-
47
- return name, get_first_sentence(description)
48
- except Exception as e:
49
- print(f"Error parsing {file_path}: {e}", file=sys.stderr)
50
- return None
51
-
52
- def main():
53
- try:
54
- ctx = AgentContext()
55
- project_dir = os.environ.get('GEMINI_PROJECT_DIR', os.getcwd())
56
- skills_root = os.path.join(project_dir, 'skills')
57
-
58
- if not os.path.exists(skills_root):
59
- ctx.fail_open()
60
-
61
- available_skills = []
62
- for skill_dir in os.listdir(skills_root):
63
- skill_path = os.path.join(skills_root, skill_dir)
64
- if os.path.isdir(skill_path):
65
- skill_md = os.path.join(skill_path, 'SKILL.md')
66
- if os.path.exists(skill_md):
67
- result = parse_skill_md(skill_md)
68
- if result:
69
- name, desc = result
70
- available_skills.append(f"- {name}: {desc}")
71
-
72
- if not available_skills:
73
- ctx.fail_open()
74
-
75
- context_msg = "## Available Local Agent Skills\n"
76
- context_msg += "The following specialized skills are available in this repository. Use them when appropriate:\n"
77
- context_msg += "\n".join(sorted(available_skills))
78
- context_msg += "\n\nYou can activate a skill using `activate_skill(name='skill-name')`."
79
-
80
- ctx.allow(
81
- system_message="🚀 Loaded available local skills into context.",
82
- additional_context=context_msg
83
- )
84
-
85
- except Exception as e:
86
- print(f"Hook failed: {e}", file=sys.stderr)
87
- sys.exit(0)
88
-
89
- if __name__ == "__main__":
90
- main()
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env python3
2
- import sys
3
- import os
4
- import re
5
-
6
- # Add script directory to path to allow importing shared modules
7
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
8
- from agent_context import AgentContext
9
-
10
- # Configuration
11
- ORCHESTRATION_PATTERNS = [
12
- r"review.*(code|security|quality)|code.*(review|audit)",
13
- r"security.*(audit|review|scan)",
14
- r"implement.*(feature|endpoint|api)|build.*feature",
15
- r"(debug|investigate|root.*cause|crash|fix.*unknown)",
16
- r"(refactor.*sprint|major.*refactor|migration|technical.*debt)",
17
- r"validate.*(commit|staged)|pre.*commit",
18
- ]
19
-
20
- CCS_PATTERNS = [
21
- r"(fix|correggi|risolvi).*typo",
22
- r"(fix|correggi).*spelling",
23
- r"(add|aggiungi|crea|create).*test",
24
- r"(genera|generate).*(test|unit|case)",
25
- r"(estrai|extract).*(function|method|funzione|metodo)",
26
- r"rename.*variable|rinomina.*variabile",
27
- r"(add|aggiungi).*(doc|docstring|comment)",
28
- r"(aggiorna|update).*comment",
29
- r"(format|formatta|lint|indenta|indent)",
30
- r"(add|aggiungi).*(type|typing|hint)",
31
- r"(rimuovi|remove|elimina|delete).*(import|unused)",
32
- r"(modifica|modify|cambia|change).*(name|nome)"
33
- ]
34
-
35
- P_PATTERNS = [
36
- r"analiz|analyz|esamina|studia|review|rivedi",
37
- r"implementa|implement|create|crea",
38
- r"spiega|explain|descri|describe",
39
- r"^(come|how|what|cosa|perch|why)"
40
- ]
41
-
42
- EXCLUDE_PATTERNS = [
43
- r"archit|design|progett",
44
- r"(add|implement|fix|patch).*(security|auth|oauth)|security.*(vuln|fix|patch)",
45
- r"bug|debug|investig|indaga",
46
- r"performance|ottimizz|optim",
47
- r"migra|breaking.*change",
48
- r"complex|compless"
49
- ]
50
-
51
- CONVERSATIONAL_PATTERNS = [
52
- r"^(ciao|hi|hello|hey|buongiorno|buonasera|salve)([!.]|$)",
53
- r"^(good morning|good afternoon|good evening)([!.]|$)",
54
- r"^(grazie|thanks|thank you|merci|thx)([!.]|$)",
55
- r"^(grazie mille|thanks a lot|many thanks)([!.]|$)",
56
- r"^(ok|okay|va bene|perfetto|perfect|fine|d'accordo|agreed?)([!.]|$)",
57
- r"^(si|sì|yes|no|nope|yeah|yep)([!.]|$)",
58
- r"^(arrivederci|addio|ciao|bye|goodbye|see you|ci vediamo)([!.]|$)",
59
- r"^come stai\?$|^how are you\?$|^come va\?$",
60
- r"^tutto bene\?$|^all good\?$|^everything ok\?$"
61
- ]
62
-
63
- def matches(text, patterns):
64
- for pattern in patterns:
65
- if re.search(pattern, text, re.IGNORECASE):
66
- return True
67
- return False
68
-
69
- try:
70
- ctx = AgentContext()
71
- prompt = ctx.prompt
72
-
73
- if not prompt:
74
- ctx.fail_open()
75
-
76
- ccs_available = not bool(os.environ.get('CLAUDECODE'))
77
- ccs_hint = "CCS backend" if ccs_available else "Gemini or Qwen directly (CCS unavailable inside Claude Code)"
78
-
79
- # 1. Check Exclusions
80
- if matches(prompt, EXCLUDE_PATTERNS) or matches(prompt, CONVERSATIONAL_PATTERNS):
81
- ctx.fail_open()
82
-
83
- agent_name = ctx.agent_type.capitalize()
84
-
85
- # 2. Check Explicit Delegation
86
- if re.search(r'delegate', prompt, re.IGNORECASE):
87
- ctx.allow(system_message=f"💡 {agent_name} Internal Reminder: User mentioned 'delegate'. Consider using the /delegating skill to offload this task.")
88
-
89
- # 3. Check CCS Delegation (Simple Tasks)
90
- if matches(prompt, CCS_PATTERNS):
91
- ctx.allow(system_message=f"💡 {agent_name} Internal Reminder: This appears to be a simple, deterministic task (typo/test/format/doc). Consider using the /delegating skill ({ccs_hint}) for cost-optimized execution.")
92
-
93
- # 4. Check Orchestration (Complex Tasks)
94
- elif matches(prompt, ORCHESTRATION_PATTERNS) and not matches(prompt, EXCLUDE_PATTERNS):
95
- ctx.allow(system_message=f"💡 {agent_name} Internal Reminder: This looks like a multi-agent task (review/implement/debug). Consider using the /delegating skill (Gemini+Qwen orchestration) instead of handling in main session.")
96
-
97
- # 5. Check Prompt Improving (/p)
98
- word_count = len(prompt.split())
99
- is_vague = matches(prompt, P_PATTERNS)
100
-
101
- # Heuristic for very short command-like prompts
102
- if word_count < 6 and not is_vague:
103
- if matches(prompt, [r"(creare|create|fare|do|aggiungere|add|modificare|modify|controllare|check|verificare|verify|testare|test)"]):
104
- is_vague = True
105
-
106
- if is_vague:
107
- ctx.allow(system_message=f"💡 {agent_name} Internal Reminder: This prompt appears vague or could benefit from structure. Consider using the /prompt-improving skill to add XML structure, examples, and thinking space before proceeding.")
108
-
109
- ctx.fail_open()
110
-
111
- except Exception:
112
- sys.exit(0)
@@ -1,107 +0,0 @@
1
- #!/usr/bin/env python3
2
- import sys
3
- import os
4
- import subprocess
5
-
6
- # Add script directory to path to allow importing shared modules
7
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
8
- from agent_context import AgentContext
9
-
10
- # Configuration
11
- STRICT_DIRS = ["mcp_server"]
12
- WARN_DIRS = ["scripts"]
13
- PROJECT_ROOT = os.environ.get('GEMINI_PROJECT_DIR', os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd()))
14
- VENV_PATH = os.path.join(PROJECT_ROOT, ".venv")
15
-
16
- # Colors
17
- RED = '\033[0;31m'
18
- YELLOW = '\033[1;33m'
19
- GREEN = '\033[0;32m'
20
- CYAN = '\033[0;36m'
21
- NC = '\033[0m'
22
-
23
- def is_strict_path(file_path):
24
- rel_path = os.path.relpath(file_path, PROJECT_ROOT)
25
- for d in STRICT_DIRS:
26
- if rel_path.startswith(d):
27
- return True
28
- return False
29
-
30
- def run_mypy(target, is_strict):
31
- if not os.path.exists(os.path.join(VENV_PATH, "bin", "activate")):
32
- print(f"{YELLOW}⚠️ Venv not found at {VENV_PATH}, skipping check{NC}")
33
- return True
34
-
35
- cmd = f"source {VENV_PATH}/bin/activate && python -m mypy {target} --explicit-package-bases"
36
-
37
- try:
38
- result = subprocess.run(cmd, shell=True, executable="/bin/bash", capture_output=True, text=True)
39
-
40
- if result.returncode != 0:
41
- if is_strict:
42
- print(f"{RED}❌ MYPY FAILED (STRICT MODE){NC}", file=sys.stderr)
43
- print(result.stdout, file=sys.stderr)
44
- print(f"\n{RED}🚫 COMMIT BLOCKED: Fix type errors in {target}{NC}", file=sys.stderr)
45
- print(f"{CYAN}💡 Run: source .venv/bin/activate && python -m mypy {target}{NC}", file=sys.stderr)
46
- return False
47
- else:
48
- print(f"{YELLOW}⚠️ MYPY WARNING (LENIENT MODE){NC}", file=sys.stderr)
49
- print("\n".join(result.stdout.splitlines()[:20]), file=sys.stderr)
50
- print(f"\n{YELLOW}⚡ Type errors exist in {target} (commit allowed){NC}", file=sys.stderr)
51
- return True
52
- else:
53
- print(f"{GREEN}✅ MYPY PASSED: {target}{NC}", file=sys.stderr)
54
- return True
55
-
56
- except Exception as e:
57
- print(f"Error running mypy: {e}", file=sys.stderr)
58
- return True # Fail open
59
-
60
- try:
61
- ctx = AgentContext()
62
-
63
- # 1. Check Git Commits (Shell tools)
64
- if ctx.is_shell_tool():
65
- command = ctx.get_command()
66
- if 'git commit' in command:
67
- print(f"{CYAN}🔍 TYPE SAFETY CHECK: Validating staged Python files...{NC}", file=sys.stderr)
68
-
69
- # Get staged files
70
- try:
71
- staged = subprocess.check_output(
72
- "git diff --cached --name-only --diff-filter=ACM | grep '\.py$'",
73
- shell=True, cwd=PROJECT_ROOT
74
- ).decode().strip().splitlines()
75
- except subprocess.CalledProcessError:
76
- staged = []
77
-
78
- if not staged:
79
- print(f"{GREEN}✅ No Python files staged{NC}", file=sys.stderr)
80
- ctx.allow()
81
-
82
- failed = False
83
-
84
- # Check individual files
85
- for f in staged:
86
- full_path = os.path.join(PROJECT_ROOT, f)
87
- if is_strict_path(full_path):
88
- if not run_mypy(full_path, True):
89
- failed = True
90
-
91
- # If failed, block the tool
92
- if failed:
93
- ctx.block(reason="Type safety violations in strict directory.")
94
-
95
- ctx.allow()
96
-
97
- # 2. Check Edits (Write/Edit tools)
98
- elif ctx.is_write_tool() or ctx.is_edit_tool():
99
- file_path = ctx.get_file_path()
100
- if file_path.endswith('.py') and is_strict_path(file_path):
101
- ctx.allow(system_message=f"""{YELLOW}⚠️ EDITING STRICT TYPE-SAFE FILE{NC}
102
- This file is in a STRICT zone ({', '.join(STRICT_DIRS)}).
103
- Any type errors will BLOCK commits.
104
- """)
105
-
106
- except Exception:
107
- sys.exit(0)