xtrm-tools 0.5.11 → 0.5.13

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": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -57,12 +57,12 @@ export function stopBlockMessage(summary, claimed) {
57
57
  // ── Memory gate messages ─────────────────────────────────────────
58
58
 
59
59
  export function memoryPromptMessage(claimId) {
60
- const claimLine = claimId ? `claim \`${claimId}\` was closed this session.\n` : '';
60
+ const claimLine = claimId ? `claim \`${claimId}\` was closed.\n` : '';
61
61
  return (
62
- `🧠 Memory gate: ${claimLine}` +
63
- 'For each closed issue, worth persisting?\n' +
62
+ `\u25cf Memory gate: ${claimLine}` +
63
+ 'Ask: "Would this be useful in 14 days on a fresh session?"\n' +
64
64
  ' YES → `bd remember "<insight>"`\n' +
65
65
  ' NO → note "nothing to persist"\n' +
66
- ' Then acknowledge: `touch .beads/.memory-gate-done`\n'
66
+ ' Then: `touch .beads/.memory-gate-done`\n'
67
67
  );
68
68
  }
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * GitNexus Claude Code Hook — PostToolUse enrichment
4
4
  *
5
- * Fires AFTER Read/Grep/Glob/Bash/Serena tools complete.
5
+ * Fires AFTER Bash/Grep/Read/Glob/Serena tools complete.
6
6
  * Extracts patterns from both tool input AND output, runs
7
7
  * `gitnexus augment <pattern>`, and injects a [GitNexus: ...]
8
8
  * block into Claude's context — mirroring pi-gitnexus behavior.
package/hooks/hooks.json CHANGED
@@ -12,6 +12,11 @@
12
12
  "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/beads-compact-restore.mjs",
13
13
  "timeout": 5000
14
14
  },
15
+ {
16
+ "type": "command",
17
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/quality-check-env.mjs",
18
+ "timeout": 5000
19
+ },
15
20
  {
16
21
  "type": "command",
17
22
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/serena-workflow-reminder.py"
@@ -104,17 +109,6 @@
104
109
  }
105
110
  ]
106
111
  }
107
- ],
108
- "UserPromptSubmit": [
109
- {
110
- "hooks": [
111
- {
112
- "type": "command",
113
- "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/branch-state.mjs",
114
- "timeout": 3000
115
- }
116
- ]
117
- }
118
112
  ]
119
113
  }
120
114
  }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook — verify quality gate environment is intact.
3
+ // Checks for tsc, eslint, ruff so the agent knows early if enforcement
4
+ // is silently degraded. Exits 0 always (informational only).
5
+
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { execSync } from 'node:child_process';
8
+ import path from 'node:path';
9
+
10
+ let input;
11
+ try {
12
+ input = JSON.parse(readFileSync(0, 'utf8'));
13
+ } catch {
14
+ process.exit(0);
15
+ }
16
+
17
+ const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
18
+
19
+ // Only relevant in projects that have quality gates wired
20
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? '';
21
+ const hookPresent =
22
+ existsSync(path.join(pluginRoot, 'hooks', 'quality-check.cjs')) ||
23
+ existsSync(path.join(cwd, '.claude', 'hooks', 'quality-check.cjs'));
24
+
25
+ if (!hookPresent) process.exit(0);
26
+
27
+ function which(cmd) {
28
+ try {
29
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
30
+ return true;
31
+ } catch {
32
+ // fall through to local node_modules probe
33
+ }
34
+ // Check node_modules/.bin/ walking up from cwd
35
+ let dir = cwd;
36
+ while (true) {
37
+ if (existsSync(path.join(dir, 'node_modules', '.bin', cmd))) return true;
38
+ const parent = path.dirname(dir);
39
+ if (parent === dir) break;
40
+ dir = parent;
41
+ }
42
+ return false;
43
+ }
44
+
45
+ const warnings = [];
46
+
47
+ // CLAUDE_PROJECT_DIR check
48
+ if (!process.env.CLAUDE_PROJECT_DIR) {
49
+ warnings.push('CLAUDE_PROJECT_DIR is not set — quality gate may target wrong directory');
50
+ }
51
+
52
+ // TypeScript project checks
53
+ const hasTsConfig = existsSync(path.join(cwd, 'tsconfig.json')) ||
54
+ existsSync(path.join(cwd, 'cli', 'tsconfig.json'));
55
+
56
+ if (hasTsConfig) {
57
+ if (!which('tsc')) warnings.push('tsc not found — TypeScript compilation check will be skipped');
58
+ const hasEslintConfig = ['eslint.config.js', 'eslint.config.mjs', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']
59
+ .some(f => existsSync(path.join(cwd, f)));
60
+ if (hasEslintConfig && !which('eslint')) warnings.push('eslint not found — ESLint check will be skipped');
61
+ }
62
+
63
+ // Python project checks
64
+ const hasPyFiles = existsSync(path.join(cwd, 'pyproject.toml')) ||
65
+ existsSync(path.join(cwd, 'setup.py')) ||
66
+ existsSync(path.join(cwd, 'requirements.txt'));
67
+
68
+ if (hasPyFiles) {
69
+ if (!which('ruff')) warnings.push('ruff not found — Python lint check will be skipped');
70
+ }
71
+
72
+ if (warnings.length === 0) process.exit(0);
73
+
74
+ const msg = `⚠️ Quality gate environment issue(s) detected:\n${warnings.map(w => ` • ${w}`).join('\n')}\nFix these to ensure quality gates enforce correctly.`;
75
+
76
+ process.stdout.write(JSON.stringify({
77
+ hookSpecificOutput: { additionalSystemPrompt: msg },
78
+ }));
79
+ process.exit(0);
@@ -447,7 +447,7 @@ class QualityChecker {
447
447
  if (/\.(ts|tsx)$/.test(filePath)) {
448
448
  return 'typescript';
449
449
  }
450
- if (/\.(js|jsx)$/.test(filePath)) {
450
+ if (/\.(js|jsx|cjs|mjs)$/.test(filePath)) {
451
451
  return 'javascript';
452
452
  }
453
453
  return 'unknown';
@@ -537,7 +537,7 @@ class QualityChecker {
537
537
  const resolved = path.resolve(dir, importPath);
538
538
 
539
539
  // Try common extensions
540
- const extensions = ['.ts', '.tsx', '.js', '.jsx'];
540
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.cjs', '.mjs'];
541
541
  for (const ext of extensions) {
542
542
  const fullPath = resolved + ext;
543
543
  if (require('fs').existsSync(fullPath)) {
@@ -565,8 +565,8 @@ class QualityChecker {
565
565
  return;
566
566
  }
567
567
 
568
- // Skip TypeScript checking for JavaScript files in hook directories
569
- if (this.filePath.endsWith('.js') && this.filePath.includes('.claude/hooks/')) {
568
+ // Skip TypeScript checking for JavaScript/CJS/MJS files in hook directories
569
+ if (/\.(js|cjs|mjs)$/.test(this.filePath) && this.filePath.includes('.claude/hooks/')) {
570
570
  log.debug('Skipping TypeScript check for JavaScript hook file');
571
571
  return;
572
572
  }
@@ -865,7 +865,7 @@ class QualityChecker {
865
865
  const debuggerRule = config._fileConfig.rules?.debugger || {};
866
866
  if (debuggerRule.enabled !== false) {
867
867
  lines.forEach((line, index) => {
868
- if (/\bdebugger\b/.test(line)) {
868
+ if (/^\s*debugger\s*;/.test(line)) {
869
869
  const severity = debuggerRule.severity || 'error';
870
870
  const message =
871
871
  debuggerRule.message || 'Remove debugger statements before committing';
@@ -1111,7 +1111,7 @@ async function fileExists(filePath) {
1111
1111
  * @returns {boolean} True if source file
1112
1112
  */
1113
1113
  function isSourceFile(filePath) {
1114
- return /\.(ts|tsx|js|jsx)$/.test(filePath);
1114
+ return /\.(ts|tsx|js|jsx|cjs|mjs)$/.test(filePath);
1115
1115
  }
1116
1116
 
1117
1117
  /**
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ // statusline.mjs — Claude Code statusLine command for xt claude worktree sessions
3
+ // Two lines:
4
+ // Line 1 (plain): XTRM ⎇ <branch>
5
+ // Line 2 (colored): ◐ <claim title in italics> OR ○ N open
6
+ // State file: .xtrm/statusline-claim (written by beads-claim-sync.mjs)
7
+ // Results cached 5s in /tmp to avoid hammering bd on every render.
8
+
9
+ import { execSync } from 'node:child_process';
10
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+ import { createHash } from 'node:crypto';
14
+
15
+ const cwd = process.cwd();
16
+ const cacheKey = createHash('md5').update(cwd).digest('hex').slice(0, 8);
17
+ const CACHE_FILE = join(tmpdir(), `xtrm-sl-${cacheKey}.json`);
18
+ const CACHE_TTL = 5000;
19
+
20
+ function run(cmd) {
21
+ try {
22
+ return execSync(cmd, {
23
+ encoding: 'utf8', cwd,
24
+ stdio: ['pipe', 'pipe', 'pipe'],
25
+ timeout: 2000,
26
+ }).trim();
27
+ } catch { return null; }
28
+ }
29
+
30
+ function getCached() {
31
+ try {
32
+ const c = JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
33
+ if (Date.now() - c.ts < CACHE_TTL) return c.data;
34
+ } catch {}
35
+ return null;
36
+ }
37
+
38
+ function setCache(data) {
39
+ try { writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), data })); } catch {}
40
+ }
41
+
42
+ // ANSI
43
+ const R = '\x1b[0m';
44
+ const BOLD = '\x1b[1m';
45
+ const BOLD_OFF = '\x1b[22m';
46
+ const ITALIC = '\x1b[3m';
47
+ const ITALIC_OFF = '\x1b[23m';
48
+ const FG_WHITE = '\x1b[38;5;15m';
49
+ const FG_ACCENT = '\x1b[38;5;75m';
50
+ const FG_MUTED = '\x1b[38;5;245m';
51
+ const BG_CLAIMED = '\x1b[48;5;17m';
52
+ const BG_IDLE = '\x1b[48;5;238m';
53
+
54
+ // Data
55
+ let data = getCached();
56
+ if (!data) {
57
+ const branch = run('git branch --show-current');
58
+ let claimTitle = null;
59
+ let openCount = 0;
60
+
61
+ const hasBeads = existsSync(join(cwd, '.beads'));
62
+ if (hasBeads) {
63
+ const claimFile = join(cwd, '.xtrm', 'statusline-claim');
64
+ let claimId = null;
65
+ if (existsSync(claimFile)) {
66
+ claimId = readFileSync(claimFile, 'utf8').trim() || null;
67
+ }
68
+
69
+ if (claimId) {
70
+ try {
71
+ const raw = run(`bd show ${claimId} --json`);
72
+ if (raw) {
73
+ const parsed = JSON.parse(raw);
74
+ claimTitle = parsed?.[0]?.title ?? null;
75
+ }
76
+ } catch {}
77
+ }
78
+
79
+ if (!claimTitle) {
80
+ const listOut = run('bd list');
81
+ const m = listOut?.match(/\((\d+)\s+open/);
82
+ if (m) openCount = parseInt(m[1], 10);
83
+ }
84
+ }
85
+
86
+ data = { branch, claimTitle, openCount };
87
+ setCache(data);
88
+ }
89
+
90
+ // Render
91
+ const { branch, claimTitle, openCount } = data;
92
+ const cols = process.stdout.columns || 80;
93
+
94
+ const brand = `${BOLD}${FG_ACCENT}XTRM${BOLD_OFF}${R}`;
95
+ const branchStr = branch ? `${FG_MUTED}⎇ ${branch}${R}` : '';
96
+ const line1 = [brand, branchStr].filter(Boolean).join(' ');
97
+
98
+ function padded(text, bg) {
99
+ const visible = text.replace(/\x1b\[[0-9;]*m/g, '');
100
+ const pad = Math.max(0, cols - visible.length);
101
+ return `${bg}${FG_WHITE}${text}${' '.repeat(pad)}${R}`;
102
+ }
103
+
104
+ let line2;
105
+ if (claimTitle) {
106
+ const maxLen = cols - 4;
107
+ const title = claimTitle.length > maxLen ? claimTitle.slice(0, maxLen - 1) + '\u2026' : claimTitle;
108
+ line2 = padded(` \u25d0 ${ITALIC}${title}${ITALIC_OFF}`, BG_CLAIMED);
109
+ } else {
110
+ const idle = openCount > 0 ? `\u25cb ${openCount} open` : '\u25cb no open issues';
111
+ line2 = padded(` ${idle}`, BG_IDLE);
112
+ }
113
+
114
+ process.stdout.write(line1 + '\n' + line2 + '\n');
115
+ process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
- // branch-state.mjs — UserPromptSubmit hook
3
- // Re-injects current git branch and active beads claim at each prompt.
4
- // Keeps the agent oriented after /compact or long sessions.
5
- // Output: { hookSpecificOutput: { additionalSystemPrompt } }
6
-
7
- import { execSync } from 'node:child_process';
8
- import { readFileSync } from 'node:fs';
9
-
10
- function readInput() {
11
- try { return JSON.parse(readFileSync(0, 'utf-8')); } catch { return null; }
12
- }
13
-
14
- function getBranch(cwd) {
15
- try {
16
- return execSync('git branch --show-current', {
17
- encoding: 'utf8', cwd,
18
- stdio: ['pipe', 'pipe', 'pipe'], timeout: 2000,
19
- }).trim() || null;
20
- } catch { return null; }
21
- }
22
-
23
- try {
24
- const input = readInput();
25
- if (!input) process.exit(0);
26
-
27
- const cwd = input.cwd || process.cwd();
28
- const branch = getBranch(cwd);
29
-
30
- if (!branch) process.exit(0);
31
-
32
- process.stdout.write(JSON.stringify({
33
- hookSpecificOutput: { additionalSystemPrompt: `[Context: branch=${branch}]` },
34
- }));
35
- process.stdout.write('\n');
36
- process.exit(0);
37
- } catch {
38
- process.exit(0);
39
- }