xtrm-tools 0.5.24 → 0.5.25
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 +9 -3
- package/cli/dist/index.cjs +146 -190
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +8 -0
- package/hooks/beads-claim-sync.mjs +1 -1
- package/hooks/beads-commit-gate.mjs +2 -2
- package/hooks/beads-edit-gate.mjs +3 -3
- package/hooks/beads-memory-gate.mjs +2 -2
- package/hooks/beads-stop-gate.mjs +2 -2
- package/hooks/xtrm-logger.mjs +84 -91
- package/hooks/xtrm-session-logger.mjs +27 -0
- package/hooks/xtrm-tool-logger.mjs +53 -0
- package/package.json +1 -1
package/cli/package.json
CHANGED
package/config/hooks.json
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
{
|
|
5
5
|
"script": "beads-compact-restore.mjs",
|
|
6
6
|
"timeout": 5000
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"script": "xtrm-session-logger.mjs",
|
|
10
|
+
"timeout": 3000
|
|
7
11
|
}
|
|
8
12
|
],
|
|
9
13
|
"UserPromptSubmit": [
|
|
@@ -49,6 +53,10 @@
|
|
|
49
53
|
"matcher": "Bash|mcp__serena__find_symbol|mcp__serena__get_symbols_overview|mcp__serena__search_for_pattern|mcp__serena__find_referencing_symbols|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|mcp__serena__rename_symbol",
|
|
50
54
|
"script": "gitnexus/gitnexus-hook.cjs",
|
|
51
55
|
"timeout": 10000
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"script": "xtrm-tool-logger.mjs",
|
|
59
|
+
"timeout": 3000
|
|
52
60
|
}
|
|
53
61
|
],
|
|
54
62
|
"Stop": [
|
|
@@ -42,7 +42,7 @@ withSafeBdContext(() => {
|
|
|
42
42
|
runtime: 'claude',
|
|
43
43
|
sessionId: ctx.sessionId,
|
|
44
44
|
layer: 'gate',
|
|
45
|
-
kind: '
|
|
45
|
+
kind: 'gate.commit.allow',
|
|
46
46
|
outcome: 'allow',
|
|
47
47
|
toolName: 'Bash',
|
|
48
48
|
issueId: state?.claimId ?? null,
|
|
@@ -57,7 +57,7 @@ withSafeBdContext(() => {
|
|
|
57
57
|
runtime: 'claude',
|
|
58
58
|
sessionId: ctx.sessionId,
|
|
59
59
|
layer: 'gate',
|
|
60
|
-
kind: '
|
|
60
|
+
kind: 'gate.commit.block',
|
|
61
61
|
outcome: 'block',
|
|
62
62
|
toolName: 'Bash',
|
|
63
63
|
issueId: decision.claimed ?? null,
|
|
@@ -31,7 +31,7 @@ if (!_boundary.allow) {
|
|
|
31
31
|
runtime: 'claude',
|
|
32
32
|
sessionId: resolveSessionId(input),
|
|
33
33
|
layer: 'gate',
|
|
34
|
-
kind: '
|
|
34
|
+
kind: 'gate.worktree.block',
|
|
35
35
|
outcome: 'block',
|
|
36
36
|
toolName: input.tool_name,
|
|
37
37
|
message: _wbReason,
|
|
@@ -55,7 +55,7 @@ withSafeBdContext(() => {
|
|
|
55
55
|
runtime: 'claude',
|
|
56
56
|
sessionId: ctx.sessionId,
|
|
57
57
|
layer: 'gate',
|
|
58
|
-
kind: '
|
|
58
|
+
kind: 'gate.edit.allow',
|
|
59
59
|
outcome: 'allow',
|
|
60
60
|
toolName: input.tool_name,
|
|
61
61
|
issueId: state?.claimId ?? null,
|
|
@@ -73,7 +73,7 @@ withSafeBdContext(() => {
|
|
|
73
73
|
runtime: 'claude',
|
|
74
74
|
sessionId: ctx.sessionId,
|
|
75
75
|
layer: 'gate',
|
|
76
|
-
kind: '
|
|
76
|
+
kind: 'gate.edit.block',
|
|
77
77
|
outcome: 'block',
|
|
78
78
|
toolName: input.tool_name,
|
|
79
79
|
message: reason,
|
|
@@ -41,7 +41,7 @@ if (existsSync(marker)) {
|
|
|
41
41
|
runtime: 'claude',
|
|
42
42
|
sessionId,
|
|
43
43
|
layer: 'gate',
|
|
44
|
-
kind: '
|
|
44
|
+
kind: 'gate.memory.acked',
|
|
45
45
|
outcome: 'allow',
|
|
46
46
|
});
|
|
47
47
|
process.exit(0);
|
|
@@ -72,7 +72,7 @@ logEvent({
|
|
|
72
72
|
runtime: 'claude',
|
|
73
73
|
sessionId,
|
|
74
74
|
layer: 'gate',
|
|
75
|
-
kind: '
|
|
75
|
+
kind: 'gate.memory.triggered',
|
|
76
76
|
outcome: 'block',
|
|
77
77
|
issueId: closedIssueId,
|
|
78
78
|
message: memoryMessage,
|
|
@@ -31,7 +31,7 @@ withSafeBdContext(() => {
|
|
|
31
31
|
runtime: 'claude',
|
|
32
32
|
sessionId: ctx.sessionId,
|
|
33
33
|
layer: 'gate',
|
|
34
|
-
kind: '
|
|
34
|
+
kind: 'gate.stop.block',
|
|
35
35
|
outcome: 'block',
|
|
36
36
|
issueId: decision.claimed ?? null,
|
|
37
37
|
message,
|
|
@@ -46,7 +46,7 @@ withSafeBdContext(() => {
|
|
|
46
46
|
runtime: 'claude',
|
|
47
47
|
sessionId: ctx.sessionId,
|
|
48
48
|
layer: 'gate',
|
|
49
|
-
kind: '
|
|
49
|
+
kind: 'session.end',
|
|
50
50
|
outcome: 'allow',
|
|
51
51
|
});
|
|
52
52
|
process.exit(0);
|
package/hooks/xtrm-logger.mjs
CHANGED
|
@@ -1,130 +1,123 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// xtrm-logger.mjs — shared event logger for xtrm
|
|
2
|
+
// xtrm-logger.mjs — shared event logger for xtrm hooks
|
|
3
3
|
//
|
|
4
|
-
// Writes to
|
|
5
|
-
// Self-initializing: creates the table on first write
|
|
4
|
+
// Writes to .xtrm/debug.db (SQLite WAL) in the project root.
|
|
5
|
+
// Self-initializing: creates the DB and table on first write.
|
|
6
6
|
// Fails completely silently — logging NEVER affects hook behavior.
|
|
7
7
|
//
|
|
8
8
|
// Usage (from any hook):
|
|
9
9
|
// import { logEvent } from './xtrm-logger.mjs';
|
|
10
|
-
// logEvent({ cwd,
|
|
11
|
-
// outcome: 'block', toolName, issueId, message, extra });
|
|
10
|
+
// logEvent({ cwd, sessionId, kind: 'gate.edit.allow', outcome: 'allow', toolName, issueId });
|
|
12
11
|
|
|
13
12
|
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import {
|
|
13
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
15
|
|
|
16
16
|
// ── Schema ────────────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
tool_name
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.replace(/'/g, "''") // single-quote → doubled
|
|
53
|
-
.replace(/\0/g, ''); // strip null bytes (invalid in utf8 strings)
|
|
54
|
-
return `'${str}'`;
|
|
18
|
+
const INIT_SQL = `
|
|
19
|
+
PRAGMA journal_mode=WAL;
|
|
20
|
+
PRAGMA busy_timeout=5000;
|
|
21
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
22
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
ts INTEGER NOT NULL,
|
|
24
|
+
session_id TEXT NOT NULL,
|
|
25
|
+
runtime TEXT NOT NULL,
|
|
26
|
+
worktree TEXT,
|
|
27
|
+
kind TEXT NOT NULL,
|
|
28
|
+
tool_name TEXT,
|
|
29
|
+
outcome TEXT,
|
|
30
|
+
issue_id TEXT,
|
|
31
|
+
duration_ms INTEGER,
|
|
32
|
+
data TEXT
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_ts ON events(ts);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_session ON events(session_id);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_kind ON events(kind);
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function findDbPath(cwd) {
|
|
42
|
+
let dir = cwd;
|
|
43
|
+
for (let i = 0; i < 10; i++) {
|
|
44
|
+
if (existsSync(join(dir, '.beads'))) {
|
|
45
|
+
return join(dir, '.xtrm', 'debug.db');
|
|
46
|
+
}
|
|
47
|
+
const parent = join(dir, '..');
|
|
48
|
+
if (parent === dir) break;
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
return null; // No beads project found — silently skip logging
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
function
|
|
58
|
-
return spawnSync('
|
|
59
|
-
cwd,
|
|
54
|
+
function sqlExec(dbPath, sql) {
|
|
55
|
+
return spawnSync('sqlite3', [dbPath, sql], {
|
|
60
56
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
61
57
|
encoding: 'utf8',
|
|
62
|
-
timeout:
|
|
58
|
+
timeout: 3000,
|
|
63
59
|
});
|
|
64
60
|
}
|
|
65
61
|
|
|
62
|
+
function ensureDb(dbPath) {
|
|
63
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
64
|
+
sqlExec(dbPath, INIT_SQL);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sqlEsc(val) {
|
|
68
|
+
if (val === null || val === undefined) return 'NULL';
|
|
69
|
+
return `'${String(val).replace(/'/g, "''")}'`;
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
67
73
|
|
|
68
74
|
/**
|
|
69
|
-
* Log an xtrm event to
|
|
75
|
+
* Log an xtrm event to .xtrm/debug.db.
|
|
70
76
|
*
|
|
71
77
|
* @param {object} params
|
|
72
78
|
* @param {string} params.cwd Project working directory (required)
|
|
73
|
-
* @param {string} params.
|
|
74
|
-
* @param {string} params.
|
|
75
|
-
* @param {string} params.
|
|
76
|
-
* @param {string} params.
|
|
77
|
-
* @param {string} params.
|
|
78
|
-
* @param {string} [params.toolName] Tool intercepted (gates)
|
|
79
|
+
* @param {string} params.sessionId Session UUID or Pi PID string (required)
|
|
80
|
+
* @param {string} params.kind Dot-separated kind: 'gate.edit.allow', 'tool.call', 'bd.claimed', etc. (required)
|
|
81
|
+
* @param {string} [params.runtime] 'claude' | 'pi' (default: 'claude')
|
|
82
|
+
* @param {string} [params.outcome] 'allow' | 'block' | 'ok' | 'error'
|
|
83
|
+
* @param {string} [params.toolName] Tool name for gate / tool.call events
|
|
79
84
|
* @param {string} [params.issueId] Linked beads issue ID
|
|
80
|
-
* @param {
|
|
81
|
-
* @param {object} [params.
|
|
82
|
-
*
|
|
83
|
-
* @
|
|
85
|
+
* @param {number} [params.durationMs] Tool call duration
|
|
86
|
+
* @param {object} [params.data] Structured context (file, cmd, reason, etc.)
|
|
87
|
+
* @param {string} [params.message] Legacy: message string (merged into data.msg)
|
|
88
|
+
* @param {object} [params.extra] Legacy: extra object (merged into data)
|
|
84
89
|
*/
|
|
85
90
|
export function logEvent(params) {
|
|
86
91
|
try {
|
|
87
|
-
const { cwd,
|
|
88
|
-
if (!cwd || !
|
|
92
|
+
const { cwd, sessionId, kind } = params;
|
|
93
|
+
if (!cwd || !sessionId || !kind) return;
|
|
94
|
+
|
|
95
|
+
const { runtime = 'claude', outcome, toolName, issueId, durationMs, message, extra, data } = params;
|
|
89
96
|
|
|
90
|
-
const
|
|
97
|
+
const dbPath = findDbPath(cwd);
|
|
98
|
+
if (!dbPath) return;
|
|
91
99
|
|
|
92
|
-
// Derive worktree name if cwd is inside .xtrm/worktrees/<name>
|
|
93
100
|
const worktreeMatch = cwd.match(/\.xtrm\/worktrees\/([^/]+)/);
|
|
94
101
|
const worktree = worktreeMatch ? worktreeMatch[1] : null;
|
|
95
102
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
sqlEscape(sessionId),
|
|
104
|
-
sqlEscape(worktree),
|
|
105
|
-
sqlEscape(layer),
|
|
106
|
-
sqlEscape(kind),
|
|
107
|
-
sqlEscape(outcome),
|
|
108
|
-
sqlEscape(toolName ?? null),
|
|
109
|
-
sqlEscape(issueId ?? null),
|
|
110
|
-
sqlEscape(message ?? null),
|
|
111
|
-
sqlEscape(extraJson),
|
|
112
|
-
].join(', ');
|
|
113
|
-
|
|
114
|
-
const insertSql = `INSERT INTO xtrm_events (${cols}) VALUES (${vals})`;
|
|
115
|
-
|
|
116
|
-
let result = bdSql(insertSql, cwd);
|
|
117
|
-
if (result.status !== 0) {
|
|
118
|
-
// Table may not exist yet — create it (includes seq col) and retry once
|
|
119
|
-
bdSql(CREATE_SQL, cwd);
|
|
120
|
-
// Migrate existing table if it predates the seq column (fails silently if col exists)
|
|
121
|
-
bdSql(ADD_SEQ_SQL, cwd);
|
|
122
|
-
result = bdSql(insertSql, cwd);
|
|
103
|
+
// Merge message/extra/data into a single JSON string
|
|
104
|
+
let dataStr = null;
|
|
105
|
+
if (data !== null && data !== undefined) {
|
|
106
|
+
dataStr = typeof data === 'string' ? data : JSON.stringify(data);
|
|
107
|
+
} else if (message || extra) {
|
|
108
|
+
const merged = { ...(message ? { msg: message } : {}), ...(extra || {}) };
|
|
109
|
+
if (Object.keys(merged).length > 0) dataStr = JSON.stringify(merged);
|
|
123
110
|
}
|
|
124
111
|
|
|
125
|
-
|
|
112
|
+
const ts = Date.now();
|
|
113
|
+
const sql = `INSERT INTO events (ts,session_id,runtime,worktree,kind,tool_name,outcome,issue_id,duration_ms,data) VALUES (${ts},${sqlEsc(sessionId)},${sqlEsc(runtime)},${sqlEsc(worktree)},${sqlEsc(kind)},${sqlEsc(toolName ?? null)},${sqlEsc(outcome ?? null)},${sqlEsc(issueId ?? null)},${durationMs ?? 'NULL'},${sqlEsc(dataStr)})`;
|
|
114
|
+
|
|
115
|
+
let result = sqlExec(dbPath, sql);
|
|
116
|
+
if (result.status !== 0) {
|
|
117
|
+
ensureDb(dbPath);
|
|
118
|
+
result = sqlExec(dbPath, sql);
|
|
119
|
+
}
|
|
126
120
|
} catch {
|
|
127
121
|
// Silently swallow all errors — logging never affects hook behavior
|
|
128
|
-
return null;
|
|
129
122
|
}
|
|
130
123
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// xtrm-session-logger.mjs — SessionStart hook
|
|
3
|
+
// Logs session.start to .xtrm/debug.db so every session has a clear entry point.
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { logEvent } from './xtrm-logger.mjs';
|
|
7
|
+
import { resolveCwd, resolveSessionId } from './beads-gate-utils.mjs';
|
|
8
|
+
|
|
9
|
+
function readInput() {
|
|
10
|
+
try { return JSON.parse(readFileSync(0, 'utf-8')); } catch { return null; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const input = readInput();
|
|
14
|
+
if (!input) process.exit(0);
|
|
15
|
+
|
|
16
|
+
const cwd = resolveCwd(input) || process.cwd();
|
|
17
|
+
const sessionId = resolveSessionId(input);
|
|
18
|
+
|
|
19
|
+
logEvent({
|
|
20
|
+
cwd,
|
|
21
|
+
runtime: 'claude',
|
|
22
|
+
sessionId,
|
|
23
|
+
kind: 'session.start',
|
|
24
|
+
outcome: 'ok',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
process.exit(0);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// xtrm-tool-logger.mjs — PostToolUse hook
|
|
3
|
+
// Logs every tool call to .xtrm/debug.db with kind=tool.call.
|
|
4
|
+
// Captures tool-specific context: cmd for Bash, file path for edits, etc.
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { logEvent } from './xtrm-logger.mjs';
|
|
8
|
+
import { resolveCwd, resolveSessionId } from './beads-gate-utils.mjs';
|
|
9
|
+
|
|
10
|
+
function readInput() {
|
|
11
|
+
try { return JSON.parse(readFileSync(0, 'utf-8')); } catch { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildData(toolName, toolInput) {
|
|
15
|
+
if (!toolInput) return null;
|
|
16
|
+
if (toolName === 'Bash' || toolName === 'bash' || toolName === 'execute_shell_command') {
|
|
17
|
+
return { cmd: (toolInput.command || '').slice(0, 120) };
|
|
18
|
+
}
|
|
19
|
+
if (['Read', 'Write', 'Edit', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
|
|
20
|
+
return toolInput.file_path ? { file: toolInput.file_path } : null;
|
|
21
|
+
}
|
|
22
|
+
if (toolName === 'Glob') return { pattern: toolInput.pattern, path: toolInput.path };
|
|
23
|
+
if (toolName === 'Grep') return { pattern: toolInput.pattern, path: toolInput.path };
|
|
24
|
+
if (toolName === 'WebFetch') return { url: (toolInput.url || '').slice(0, 100) };
|
|
25
|
+
if (toolName === 'WebSearch') return { query: (toolInput.query || '').slice(0, 100) };
|
|
26
|
+
if (toolName === 'Agent') return { prompt: (toolInput.prompt || '').slice(0, 80) };
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const input = readInput();
|
|
31
|
+
if (!input || input.hook_event_name !== 'PostToolUse') process.exit(0);
|
|
32
|
+
|
|
33
|
+
const toolName = input.tool_name;
|
|
34
|
+
|
|
35
|
+
// Skip tools that would create noise or cause recursion
|
|
36
|
+
const SKIP = new Set(['TodoRead', 'TodoWrite', 'Task', 'TaskCreate', 'TaskUpdate', 'TaskGet']);
|
|
37
|
+
if (SKIP.has(toolName)) process.exit(0);
|
|
38
|
+
|
|
39
|
+
const cwd = resolveCwd(input) || process.cwd();
|
|
40
|
+
const sessionId = resolveSessionId(input);
|
|
41
|
+
const isError = input.tool_response?.is_error === true;
|
|
42
|
+
|
|
43
|
+
logEvent({
|
|
44
|
+
cwd,
|
|
45
|
+
runtime: 'claude',
|
|
46
|
+
sessionId,
|
|
47
|
+
kind: 'tool.call',
|
|
48
|
+
outcome: isError ? 'error' : 'ok',
|
|
49
|
+
toolName,
|
|
50
|
+
data: buildData(toolName, input.tool_input),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
process.exit(0);
|