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/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "0.5.24",
3
+ "version": "0.5.25",
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
@@ -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": [
@@ -200,7 +200,7 @@ function main() {
200
200
  runtime: 'claude',
201
201
  sessionId,
202
202
  layer: 'bd',
203
- kind: 'bd.auto_committed',
203
+ kind: 'bd.committed',
204
204
  outcome: commit.ok ? 'allow' : 'block',
205
205
  issueId: closedIssueId ?? null,
206
206
  message: commit.message,
@@ -42,7 +42,7 @@ withSafeBdContext(() => {
42
42
  runtime: 'claude',
43
43
  sessionId: ctx.sessionId,
44
44
  layer: 'gate',
45
- kind: 'hook.commit_gate.allow',
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: 'hook.commit_gate.block',
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: 'hook.worktree_boundary.block',
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: 'hook.edit_gate.allow',
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: 'hook.edit_gate.block',
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: 'hook.memory_gate.acked',
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: 'hook.memory_gate.triggered',
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: 'hook.stop_gate.block',
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: 'hook.stop_gate.allow',
49
+ kind: 'session.end',
50
50
  outcome: 'allow',
51
51
  });
52
52
  process.exit(0);
@@ -1,130 +1,123 @@
1
1
  #!/usr/bin/env node
2
- // xtrm-logger.mjs — shared event logger for xtrm hook and bd lifecycle events
2
+ // xtrm-logger.mjs — shared event logger for xtrm hooks
3
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.
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, runtime: 'claude', sessionId, layer: 'gate', kind: 'hook.edit_gate.block',
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 { randomUUID } from 'node:crypto';
13
+ import { existsSync, mkdirSync } from 'node:fs';
14
+ import { join, dirname } from 'node:path';
15
15
 
16
16
  // ── Schema ────────────────────────────────────────────────────────────────────
17
17
 
18
- const CREATE_SQL = `CREATE TABLE IF NOT EXISTS xtrm_events (
19
- seq INT NOT NULL AUTO_INCREMENT,
20
- id VARCHAR(36) NOT NULL,
21
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
22
- runtime VARCHAR(16) NOT NULL,
23
- session_id VARCHAR(255) NOT NULL,
24
- worktree VARCHAR(255) DEFAULT NULL,
25
- layer VARCHAR(16) NOT NULL,
26
- kind VARCHAR(64) NOT NULL,
27
- outcome VARCHAR(8) NOT NULL,
28
- tool_name VARCHAR(255) DEFAULT NULL,
29
- issue_id VARCHAR(255) DEFAULT NULL,
30
- message TEXT DEFAULT NULL,
31
- extra JSON DEFAULT NULL,
32
- PRIMARY KEY (id),
33
- UNIQUE KEY uk_seq (seq),
34
- INDEX idx_session (session_id(64)),
35
- INDEX idx_kind (kind),
36
- INDEX idx_created (created_at)
37
- )`;
38
-
39
- // Migration: add seq column to existing tables that predate this schema version
40
- const ADD_SEQ_SQL = `ALTER TABLE xtrm_events ADD COLUMN seq INT NOT NULL AUTO_INCREMENT, ADD UNIQUE KEY uk_seq (seq)`;
41
-
42
- // ── SQL helpers ───────────────────────────────────────────────────────────────
43
-
44
- /**
45
- * Escape a value for use in a MySQL single-quoted string literal.
46
- * Returns the quoted string, or NULL for null/undefined.
47
- */
48
- function sqlEscape(val) {
49
- if (val === null || val === undefined) return 'NULL';
50
- const str = String(val)
51
- .replace(/\\/g, '\\\\') // backslash first
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 bdSql(sql, cwd) {
58
- return spawnSync('bd', ['sql', sql], {
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: 5000,
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 the beads xtrm_events table.
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.runtime 'claude' | 'pi'
74
- * @param {string} params.sessionId Claude session UUID or Pi PID string
75
- * @param {string} params.layer 'gate' | 'bd'
76
- * @param {string} params.kind e.g. 'hook.edit_gate.block', 'bd.claimed'
77
- * @param {string} params.outcome 'allow' | 'block'
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 {string} [params.message] Full message sent to agent (blocks)
81
- * @param {object} [params.extra] Additional structured data {file, cwd, reason_code, ...}
82
- *
83
- * @returns {string|null} The event UUID, or null if logging failed
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, runtime, sessionId, layer, kind, outcome } = params;
88
- if (!cwd || !runtime || !sessionId || !layer || !kind || !outcome) return null;
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 { toolName, issueId, message, extra } = params;
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
- const id = randomUUID();
97
- const extraJson = extra ? JSON.stringify(extra) : null;
98
-
99
- const cols = 'id, runtime, session_id, worktree, layer, kind, outcome, tool_name, issue_id, message, extra';
100
- const vals = [
101
- sqlEscape(id),
102
- sqlEscape(runtime),
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
- return result.status === 0 ? id : null;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "0.5.24",
3
+ "version": "0.5.25",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",