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/CHANGELOG.md +4 -1
- package/cli/dist/index.cjs +2 -1
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/hooks/beads-gate-messages.mjs +4 -4
- package/hooks/gitnexus/gitnexus-hook.cjs +1 -1
- package/hooks/hooks.json +5 -11
- package/hooks/quality-check-env.mjs +79 -0
- package/hooks/quality-check.cjs +6 -6
- package/hooks/statusline.mjs +115 -0
- package/package.json +1 -1
- package/hooks/branch-state.mjs +0 -39
package/cli/package.json
CHANGED
|
@@ -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
|
|
60
|
+
const claimLine = claimId ? `claim \`${claimId}\` was closed.\n` : '';
|
|
61
61
|
return (
|
|
62
|
-
|
|
63
|
-
'
|
|
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
|
|
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
|
|
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);
|
package/hooks/quality-check.cjs
CHANGED
|
@@ -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
|
|
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 (
|
|
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
package/hooks/branch-state.mjs
DELETED
|
@@ -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
|
-
}
|