xtrm-tools 0.5.21 → 0.5.23

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.
@@ -26,20 +26,20 @@ class Colors:
26
26
  RESET = '\x1b[0m'
27
27
 
28
28
  def log_info(msg: str):
29
- print(f"{Colors.BLUE}[INFO]{Colors.RESET} {msg}", file=sys.stderr)
29
+ print(f"{Colors.BLUE}[INFO]{Colors.RESET} {msg}")
30
30
 
31
31
  def log_error(msg: str):
32
- print(f"{Colors.RED}[ERROR]{Colors.RESET} {msg}", file=sys.stderr)
32
+ print(f"{Colors.RED}[ERROR]{Colors.RESET} {msg}")
33
33
 
34
34
  def log_success(msg: str):
35
- print(f"{Colors.GREEN}[OK]{Colors.RESET} {msg}", file=sys.stderr)
35
+ print(f"{Colors.GREEN}[OK]{Colors.RESET} {msg}")
36
36
 
37
37
  def log_warning(msg: str):
38
- print(f"{Colors.YELLOW}[WARN]{Colors.RESET} {msg}", file=sys.stderr)
38
+ print(f"{Colors.YELLOW}[WARN]{Colors.RESET} {msg}")
39
39
 
40
40
  def log_debug(msg: str):
41
41
  if os.environ.get('CLAUDE_HOOKS_DEBUG', 'false').lower() == 'true':
42
- print(f"{Colors.CYAN}[DEBUG]{Colors.RESET} {msg}", file=sys.stderr)
42
+ print(f"{Colors.CYAN}[DEBUG]{Colors.RESET} {msg}")
43
43
 
44
44
  def find_project_root(file_path: str) -> str:
45
45
  """Find project root by looking for pyproject.toml, setup.py, or .git"""
@@ -203,20 +203,20 @@ def check_pytest_suggestions(file_path: str, project_root: str):
203
203
  def print_summary(errors: list[str], autofixes: list[str]):
204
204
  """Print summary of errors and autofixes"""
205
205
  if autofixes:
206
- print(f'\n{Colors.BLUE}═══ Auto-fixes Applied ═══{Colors.RESET}', file=sys.stderr)
206
+ print(f'\n{Colors.BLUE}═══ Auto-fixes Applied ═══{Colors.RESET}')
207
207
  for fix in autofixes:
208
- print(f'{Colors.GREEN}✨{Colors.RESET} {fix}', file=sys.stderr)
209
- print(f'{Colors.GREEN}Automatically fixed {len(autofixes)} issue(s)!{Colors.RESET}', file=sys.stderr)
208
+ print(f'{Colors.GREEN}✨{Colors.RESET} {fix}')
209
+ print(f'{Colors.GREEN}Automatically fixed {len(autofixes)} issue(s)!{Colors.RESET}')
210
210
 
211
211
  if errors:
212
- print(f'\n{Colors.BLUE}═══ Quality Check Summary ═══{Colors.RESET}', file=sys.stderr)
212
+ print(f'\n{Colors.BLUE}═══ Quality Check Summary ═══{Colors.RESET}')
213
213
  for error in errors:
214
- print(f'{Colors.RED}❌{Colors.RESET} {error}', file=sys.stderr)
215
- print(f'\n{Colors.RED}Found {len(errors)} issue(s) that MUST be fixed!{Colors.RESET}', file=sys.stderr)
216
- print(f'{Colors.RED}══════════════════════════════════════{Colors.RESET}', file=sys.stderr)
217
- print(f'{Colors.RED}❌ ALL ISSUES ARE BLOCKING ❌{Colors.RESET}', file=sys.stderr)
218
- print(f'{Colors.RED}══════════════════════════════════════{Colors.RESET}', file=sys.stderr)
219
- print(f'{Colors.RED}Fix EVERYTHING above until all checks are ✅ GREEN{Colors.RESET}', file=sys.stderr)
214
+ print(f'{Colors.RED}❌{Colors.RESET} {error}')
215
+ print(f'\n{Colors.RED}Found {len(errors)} issue(s) that MUST be fixed!{Colors.RESET}')
216
+ print(f'{Colors.RED}══════════════════════════════════════{Colors.RESET}')
217
+ print(f'{Colors.RED}❌ ALL ISSUES ARE BLOCKING ❌{Colors.RESET}')
218
+ print(f'{Colors.RED}══════════════════════════════════════{Colors.RESET}')
219
+ print(f'{Colors.RED}Fix EVERYTHING above until all checks are ✅ GREEN{Colors.RESET}')
220
220
 
221
221
  def parse_json_input() -> dict:
222
222
  """Parse JSON input from stdin"""
@@ -224,7 +224,7 @@ def parse_json_input() -> dict:
224
224
 
225
225
  if not input_data:
226
226
  log_warning('No JSON input provided.')
227
- print(f'\n{Colors.YELLOW}👉 Hook executed but no input to process.{Colors.RESET}', file=sys.stderr)
227
+ print(f'\n{Colors.YELLOW}👉 Hook executed but no input to process.{Colors.RESET}')
228
228
  sys.exit(0)
229
229
 
230
230
  try:
@@ -253,9 +253,9 @@ def extract_file_path(input_data: dict) -> str | None:
253
253
 
254
254
  def main():
255
255
  """Main entry point"""
256
- print('', file=sys.stderr)
257
- print(f'📦 Python Quality Check - Starting...', file=sys.stderr)
258
- print('─────────────────────────────────────', file=sys.stderr)
256
+ print('')
257
+ print(f'📦 Python Quality Check - Starting...')
258
+ print('─────────────────────────────────────')
259
259
 
260
260
  # Parse input
261
261
  input_data = parse_json_input()
@@ -263,25 +263,25 @@ def main():
263
263
 
264
264
  if not file_path:
265
265
  log_warning('No file path found in JSON input.')
266
- print(f'\n{Colors.YELLOW}👉 No file to check - tool may not be file-related.{Colors.RESET}', file=sys.stderr)
266
+ print(f'\n{Colors.YELLOW}👉 No file to check - tool may not be file-related.{Colors.RESET}')
267
267
  sys.exit(0)
268
268
 
269
269
  # Check if file exists
270
270
  if not Path(file_path).exists():
271
271
  log_info(f'File does not exist: {file_path}')
272
- print(f'\n{Colors.YELLOW}👉 File skipped - doesn\'t exist.{Colors.RESET}', file=sys.stderr)
272
+ print(f'\n{Colors.YELLOW}👉 File skipped - doesn\'t exist.{Colors.RESET}')
273
273
  sys.exit(0)
274
274
 
275
275
  # Skip non-Python files
276
276
  if not is_python_file(file_path):
277
277
  log_info(f'Skipping non-Python file: {file_path}')
278
- print(f'\n{Colors.GREEN}✅ No checks needed for {os.path.basename(file_path)}{Colors.RESET}', file=sys.stderr)
278
+ print(f'\n{Colors.GREEN}✅ No checks needed for {os.path.basename(file_path)}{Colors.RESET}')
279
279
  sys.exit(0)
280
280
 
281
281
  # Update header
282
- print('', file=sys.stderr)
283
- print(f'🔍 Validating: {os.path.basename(file_path)}', file=sys.stderr)
284
- print('─────────────────────────────────────', file=sys.stderr)
282
+ print('')
283
+ print(f'🔍 Validating: {os.path.basename(file_path)}')
284
+ print('─────────────────────────────────────')
285
285
  log_info(f'Checking: {file_path}')
286
286
 
287
287
  # Find project root
@@ -290,7 +290,7 @@ def main():
290
290
 
291
291
  if not has_python_project_config(project_root):
292
292
  log_info('No pyproject.toml or .python-version found - skipping Python quality checks')
293
- print(f'\n{Colors.GREEN}✅ No Python project config detected; skipping checks for {os.path.basename(file_path)}{Colors.RESET}', file=sys.stderr)
293
+ print(f'\n{Colors.GREEN}✅ No Python project config detected; skipping checks for {os.path.basename(file_path)}{Colors.RESET}')
294
294
  sys.exit(0)
295
295
 
296
296
  # Get config from environment
@@ -317,20 +317,20 @@ def main():
317
317
 
318
318
  # Exit with appropriate code
319
319
  if all_errors:
320
- print(f'\n{Colors.RED}🛑 FAILED - Fix issues in your edited file! 🛑{Colors.RESET}', file=sys.stderr)
321
- print(f'{Colors.CYAN}💡 CLAUDE.md CHECK:{Colors.RESET}', file=sys.stderr)
322
- print(f'{Colors.CYAN} → What CLAUDE.md pattern would have prevented this?{Colors.RESET}', file=sys.stderr)
323
- print(f'{Colors.YELLOW}📋 NEXT STEPS:{Colors.RESET}', file=sys.stderr)
324
- print(f'{Colors.YELLOW} 1. Fix the issues listed above{Colors.RESET}', file=sys.stderr)
325
- print(f'{Colors.YELLOW} 2. The hook will run again automatically{Colors.RESET}', file=sys.stderr)
326
- print(f'{Colors.YELLOW} 3. Continue once all checks pass{Colors.RESET}', file=sys.stderr)
320
+ print(f'\n{Colors.RED}🛑 FAILED - Fix issues in your edited file! 🛑{Colors.RESET}')
321
+ print(f'{Colors.CYAN}💡 CLAUDE.md CHECK:{Colors.RESET}')
322
+ print(f'{Colors.CYAN} → What CLAUDE.md pattern would have prevented this?{Colors.RESET}')
323
+ print(f'{Colors.YELLOW}📋 NEXT STEPS:{Colors.RESET}')
324
+ print(f'{Colors.YELLOW} 1. Fix the issues listed above{Colors.RESET}')
325
+ print(f'{Colors.YELLOW} 2. The hook will run again automatically{Colors.RESET}')
326
+ print(f'{Colors.YELLOW} 3. Continue once all checks pass{Colors.RESET}')
327
327
  sys.exit(2)
328
328
  else:
329
- print(f'\n{Colors.GREEN}✅ Quality check passed for {os.path.basename(file_path)}{Colors.RESET}', file=sys.stderr)
329
+ print(f'\n{Colors.GREEN}✅ Quality check passed for {os.path.basename(file_path)}{Colors.RESET}')
330
330
  if all_autofixes:
331
- print(f'\n{Colors.YELLOW}👉 File quality verified. Auto-fixes applied. Continue with your task.{Colors.RESET}', file=sys.stderr)
331
+ print(f'\n{Colors.YELLOW}👉 File quality verified. Auto-fixes applied. Continue with your task.{Colors.RESET}')
332
332
  else:
333
- print(f'\n{Colors.YELLOW}👉 File quality verified. Continue with your task.{Colors.RESET}', file=sys.stderr)
333
+ print(f'\n{Colors.YELLOW}👉 File quality verified. Continue with your task.{Colors.RESET}')
334
334
 
335
335
  # Suggest tests
336
336
  check_pytest_suggestions(file_path, project_root)
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ // xtrm-logger.mjs — shared event logger for xtrm hook and bd lifecycle events
3
+ //
4
+ // Writes to the xtrm_events table in the project's beads Dolt DB.
5
+ // Self-initializing: creates the table on first write if it doesn't exist.
6
+ // Fails completely silently — logging NEVER affects hook behavior.
7
+ //
8
+ // Usage (from any hook):
9
+ // import { logEvent } from './xtrm-logger.mjs';
10
+ // logEvent({ cwd, runtime: 'claude', sessionId, layer: 'gate', kind: 'hook.edit_gate.block',
11
+ // outcome: 'block', toolName, issueId, message, extra });
12
+
13
+ import { spawnSync } from 'node:child_process';
14
+ import { randomUUID } from 'node:crypto';
15
+
16
+ // ── Schema ────────────────────────────────────────────────────────────────────
17
+
18
+ const CREATE_SQL = `CREATE TABLE IF NOT EXISTS xtrm_events (
19
+ id VARCHAR(36) NOT NULL,
20
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
21
+ runtime VARCHAR(16) NOT NULL,
22
+ session_id VARCHAR(255) NOT NULL,
23
+ worktree VARCHAR(255) DEFAULT NULL,
24
+ layer VARCHAR(16) NOT NULL,
25
+ kind VARCHAR(64) NOT NULL,
26
+ outcome VARCHAR(8) NOT NULL,
27
+ tool_name VARCHAR(255) DEFAULT NULL,
28
+ issue_id VARCHAR(255) DEFAULT NULL,
29
+ message TEXT DEFAULT NULL,
30
+ extra JSON DEFAULT NULL,
31
+ PRIMARY KEY (id),
32
+ INDEX idx_session (session_id(64)),
33
+ INDEX idx_kind (kind),
34
+ INDEX idx_created (created_at)
35
+ )`;
36
+
37
+ // ── SQL helpers ───────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Escape a value for use in a MySQL single-quoted string literal.
41
+ * Returns the quoted string, or NULL for null/undefined.
42
+ */
43
+ function sqlEscape(val) {
44
+ if (val === null || val === undefined) return 'NULL';
45
+ const str = String(val)
46
+ .replace(/\\/g, '\\\\') // backslash first
47
+ .replace(/'/g, "''") // single-quote → doubled
48
+ .replace(/\0/g, ''); // strip null bytes (invalid in utf8 strings)
49
+ return `'${str}'`;
50
+ }
51
+
52
+ function bdSql(sql, cwd) {
53
+ return spawnSync('bd', ['sql', sql], {
54
+ cwd,
55
+ stdio: ['pipe', 'pipe', 'pipe'],
56
+ encoding: 'utf8',
57
+ timeout: 5000,
58
+ });
59
+ }
60
+
61
+ // ── Public API ────────────────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Log an xtrm event to the beads xtrm_events table.
65
+ *
66
+ * @param {object} params
67
+ * @param {string} params.cwd Project working directory (required)
68
+ * @param {string} params.runtime 'claude' | 'pi'
69
+ * @param {string} params.sessionId Claude session UUID or Pi PID string
70
+ * @param {string} params.layer 'gate' | 'bd'
71
+ * @param {string} params.kind e.g. 'hook.edit_gate.block', 'bd.claimed'
72
+ * @param {string} params.outcome 'allow' | 'block'
73
+ * @param {string} [params.toolName] Tool intercepted (gates)
74
+ * @param {string} [params.issueId] Linked beads issue ID
75
+ * @param {string} [params.message] Full message sent to agent (blocks)
76
+ * @param {object} [params.extra] Additional structured data {file, cwd, reason_code, ...}
77
+ *
78
+ * @returns {string|null} The event UUID, or null if logging failed
79
+ */
80
+ export function logEvent(params) {
81
+ try {
82
+ const { cwd, runtime, sessionId, layer, kind, outcome } = params;
83
+ if (!cwd || !runtime || !sessionId || !layer || !kind || !outcome) return null;
84
+
85
+ const { toolName, issueId, message, extra } = params;
86
+
87
+ // Derive worktree name if cwd is inside .xtrm/worktrees/<name>
88
+ const worktreeMatch = cwd.match(/\.xtrm\/worktrees\/([^/]+)/);
89
+ const worktree = worktreeMatch ? worktreeMatch[1] : null;
90
+
91
+ const id = randomUUID();
92
+ const extraJson = extra ? JSON.stringify(extra) : null;
93
+
94
+ const cols = 'id, runtime, session_id, worktree, layer, kind, outcome, tool_name, issue_id, message, extra';
95
+ const vals = [
96
+ sqlEscape(id),
97
+ sqlEscape(runtime),
98
+ sqlEscape(sessionId),
99
+ sqlEscape(worktree),
100
+ sqlEscape(layer),
101
+ sqlEscape(kind),
102
+ sqlEscape(outcome),
103
+ sqlEscape(toolName ?? null),
104
+ sqlEscape(issueId ?? null),
105
+ sqlEscape(message ?? null),
106
+ sqlEscape(extraJson),
107
+ ].join(', ');
108
+
109
+ const insertSql = `INSERT INTO xtrm_events (${cols}) VALUES (${vals})`;
110
+
111
+ let result = bdSql(insertSql, cwd);
112
+ if (result.status !== 0) {
113
+ // Table may not exist yet — create it and retry once (self-initializing)
114
+ bdSql(CREATE_SQL, cwd);
115
+ result = bdSql(insertSql, cwd);
116
+ }
117
+
118
+ return result.status === 0 ? id : null;
119
+ } catch {
120
+ // Silently swallow all errors — logging never affects hook behavior
121
+ return null;
122
+ }
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "0.5.21",
3
+ "version": "0.5.23",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",