xtrm-cli 0.5.0 → 0.5.27

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.
Files changed (50) hide show
  1. package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.combined.log +17 -0
  2. package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.stderr.log +0 -0
  3. package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.stdout.log +17 -0
  4. package/dist/index.cjs +969 -1059
  5. package/dist/index.cjs.map +1 -1
  6. package/package.json +1 -1
  7. package/src/commands/clean.ts +7 -6
  8. package/src/commands/debug.ts +255 -0
  9. package/src/commands/docs.ts +180 -0
  10. package/src/commands/help.ts +92 -171
  11. package/src/commands/init.ts +9 -32
  12. package/src/commands/install-pi.ts +9 -16
  13. package/src/commands/install.ts +150 -2
  14. package/src/commands/pi-install.ts +10 -44
  15. package/src/core/context.ts +4 -52
  16. package/src/core/diff.ts +3 -16
  17. package/src/core/preflight.ts +0 -1
  18. package/src/index.ts +7 -4
  19. package/src/types/config.ts +0 -2
  20. package/src/utils/config-injector.ts +3 -3
  21. package/src/utils/pi-extensions.ts +41 -0
  22. package/src/utils/worktree-session.ts +86 -50
  23. package/test/extensions/beads-claim-lifecycle.test.ts +93 -0
  24. package/test/extensions/beads-parity.test.ts +94 -0
  25. package/test/extensions/extension-harness.ts +5 -5
  26. package/test/extensions/quality-gates-parity.test.ts +89 -0
  27. package/test/extensions/session-flow.test.ts +91 -0
  28. package/test/extensions/xtrm-loader.test.ts +38 -20
  29. package/test/install-pi.test.ts +22 -11
  30. package/test/pi-extensions.test.ts +50 -0
  31. package/test/session-launcher.test.ts +28 -38
  32. package/extensions/beads.ts +0 -109
  33. package/extensions/core/adapter.ts +0 -45
  34. package/extensions/core/lib.ts +0 -3
  35. package/extensions/core/logger.ts +0 -45
  36. package/extensions/core/runner.ts +0 -71
  37. package/extensions/custom-footer.ts +0 -160
  38. package/extensions/main-guard-post-push.ts +0 -44
  39. package/extensions/main-guard.ts +0 -126
  40. package/extensions/minimal-mode.ts +0 -201
  41. package/extensions/quality-gates.ts +0 -67
  42. package/extensions/service-skills.ts +0 -150
  43. package/extensions/xtrm-loader.ts +0 -89
  44. package/hooks/gitnexus-impact-reminder.py +0 -13
  45. package/src/commands/finish.ts +0 -25
  46. package/src/core/session-state.ts +0 -139
  47. package/src/core/xtrm-finish.ts +0 -267
  48. package/src/tests/session-flow-parity.test.ts +0 -118
  49. package/src/tests/session-state.test.ts +0 -124
  50. package/src/tests/xtrm-finish.test.ts +0 -148
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.27",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -8,10 +8,7 @@ import { findRepoRoot } from '../utils/repo-root.js';
8
8
 
9
9
  // Canonical hooks (files in ~/.claude/hooks/)
10
10
  const CANONICAL_HOOKS = new Set([
11
- 'agent_context.py',
12
- 'serena-workflow-reminder.py',
13
- 'main-guard.mjs',
14
- 'main-guard-post-push.mjs',
11
+ 'using-xtrm-reminder.mjs',
15
12
  'beads-gate-core.mjs',
16
13
  'beads-gate-utils.mjs',
17
14
  'beads-gate-messages.mjs',
@@ -22,11 +19,15 @@ const CANONICAL_HOOKS = new Set([
22
19
  'beads-claim-sync.mjs',
23
20
  'beads-compact-save.mjs',
24
21
  'beads-compact-restore.mjs',
25
- 'branch-state.mjs',
22
+ 'worktree-boundary.mjs',
23
+ 'statusline.mjs',
26
24
  'quality-check.cjs',
25
+ 'quality-check-env.mjs',
27
26
  'quality-check.py',
27
+ 'xtrm-logger.mjs',
28
+ 'xtrm-tool-logger.mjs',
29
+ 'xtrm-session-logger.mjs',
28
30
  'gitnexus', // directory
29
- 'statusline-starship.sh',
30
31
  'README.md',
31
32
  ]);
32
33
 
@@ -0,0 +1,255 @@
1
+ import { Command } from 'commander';
2
+ import kleur from 'kleur';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import { join, basename } from 'node:path';
6
+
7
+ // ── Types ─────────────────────────────────────────────────────────────────────
8
+
9
+ interface XtrmEvent {
10
+ id: number;
11
+ ts: number;
12
+ session_id: string;
13
+ runtime: string;
14
+ worktree: string | null;
15
+ kind: string;
16
+ tool_name: string | null;
17
+ outcome: string | null;
18
+ issue_id: string | null;
19
+ duration_ms: number | null;
20
+ data: string | null;
21
+ }
22
+
23
+ interface DebugOptions {
24
+ all: boolean;
25
+ follow: boolean;
26
+ session: string | undefined;
27
+ type: string | undefined;
28
+ json: boolean;
29
+ }
30
+
31
+ // ── Kind labels ───────────────────────────────────────────────────────────────
32
+
33
+ type ColorFn = (s: string) => string;
34
+
35
+ // Gate and lifecycle events: fixed 5-char label + fixed color
36
+ const KIND_LABELS: Record<string, { label: string; color: ColorFn }> = {
37
+ 'session.start': { label: 'SESS+', color: kleur.green },
38
+ 'session.end': { label: 'SESS-', color: kleur.white },
39
+ 'gate.edit.allow': { label: 'EDIT+', color: kleur.green },
40
+ 'gate.edit.block': { label: 'EDIT-', color: kleur.red },
41
+ 'gate.commit.allow': { label: 'CMIT+', color: kleur.green },
42
+ 'gate.commit.block': { label: 'CMIT-', color: kleur.red },
43
+ 'gate.stop.block': { label: 'STOP-', color: kleur.red },
44
+ 'gate.memory.triggered': { label: 'MEMO-', color: kleur.yellow },
45
+ 'gate.memory.acked': { label: 'MEMO+', color: kleur.green },
46
+ 'gate.worktree.block': { label: 'WTRE-', color: kleur.red },
47
+ 'bd.claimed': { label: 'CLMD ', color: kleur.cyan },
48
+ 'bd.closed': { label: 'CLSD ', color: kleur.green },
49
+ 'bd.committed': { label: outcome => outcome === 'error' ? 'ACMT-' : 'ACMT+',
50
+ color: outcome => outcome === 'error' ? kleur.red : kleur.cyan } as any,
51
+ };
52
+
53
+ // Tool call events: derive 5-char abbrev from tool_name
54
+ const TOOL_ABBREVS: Record<string, string> = {
55
+ Bash: 'BASH', bash: 'BASH', execute_shell_command: 'BASH',
56
+ Read: 'READ', Write: 'WRIT', Edit: 'EDIT', MultiEdit: 'EDIT', NotebookEdit: 'NTED',
57
+ Glob: 'GLOB', Grep: 'GREP',
58
+ WebFetch: 'WBFT', WebSearch: 'WSRC',
59
+ Agent: 'AGNT', Task: 'TASK',
60
+ LSP: 'LSP ',
61
+ };
62
+
63
+ function toolAbbrev(toolName: string): string {
64
+ if (TOOL_ABBREVS[toolName]) return TOOL_ABBREVS[toolName];
65
+ if (toolName.startsWith('mcp__serena__')) return 'SRNA';
66
+ if (toolName.startsWith('mcp__gitnexus__')) return 'GTNX';
67
+ if (toolName.startsWith('mcp__deepwiki__')) return 'WIKI';
68
+ if (toolName.startsWith('mcp__')) return 'MCP ';
69
+ return toolName.slice(0, 4).toUpperCase();
70
+ }
71
+
72
+ function getLabel(event: XtrmEvent): string {
73
+ if (event.kind === 'tool.call') {
74
+ const abbrev = toolAbbrev(event.tool_name ?? '').padEnd(5);
75
+ return event.outcome === 'error' ? kleur.red(abbrev) : kleur.dim(abbrev);
76
+ }
77
+ const def = KIND_LABELS[event.kind];
78
+ if (!def) {
79
+ // Unknown kind: derive from last segment + outcome marker
80
+ const seg = (event.kind.split('.').pop() ?? 'UNKN').slice(0, 4).toUpperCase();
81
+ const label = `${seg}${event.outcome === 'block' ? '-' : '+'}`.padEnd(5);
82
+ return event.outcome === 'block' ? kleur.red(label) : kleur.dim(label);
83
+ }
84
+ // bd.committed has dynamic label/color
85
+ if (event.kind === 'bd.committed') {
86
+ const label = event.outcome === 'error' ? 'ACMT-' : 'ACMT+';
87
+ return event.outcome === 'error' ? kleur.red(label) : kleur.cyan(label);
88
+ }
89
+ return (def as { label: string; color: ColorFn }).color(
90
+ (def as { label: string; color: ColorFn }).label
91
+ );
92
+ }
93
+
94
+ // ── Session color map ─────────────────────────────────────────────────────────
95
+
96
+ const SESSION_COLORS: ColorFn[] = [
97
+ kleur.blue, kleur.green, kleur.yellow, kleur.cyan, kleur.magenta,
98
+ ];
99
+
100
+ function buildColorMap(events: XtrmEvent[]): Map<string, ColorFn> {
101
+ const map = new Map<string, ColorFn>();
102
+ for (const ev of events) {
103
+ if (!map.has(ev.session_id)) {
104
+ map.set(ev.session_id, SESSION_COLORS[map.size % SESSION_COLORS.length]);
105
+ }
106
+ }
107
+ return map;
108
+ }
109
+
110
+ function extendColorMap(map: Map<string, ColorFn>, events: XtrmEvent[]): void {
111
+ for (const ev of events) {
112
+ if (!map.has(ev.session_id)) {
113
+ map.set(ev.session_id, SESSION_COLORS[map.size % SESSION_COLORS.length]);
114
+ }
115
+ }
116
+ }
117
+
118
+ // ── Formatting ────────────────────────────────────────────────────────────────
119
+
120
+ function fmtTime(ts: number): string {
121
+ return new Date(ts).toLocaleTimeString('en-GB', { hour12: false });
122
+ }
123
+
124
+ function buildDetail(event: XtrmEvent): string {
125
+ const parts: string[] = [];
126
+
127
+ // Parse data JSON if present
128
+ let d: Record<string, string> | null = null;
129
+ if (event.data) {
130
+ try { d = JSON.parse(event.data); } catch { /* ignore */ }
131
+ }
132
+
133
+ if (event.kind === 'tool.call') {
134
+ if (d?.cmd) parts.push(kleur.dim(d.cmd.slice(0, 72)));
135
+ if (d?.file) parts.push(kleur.dim(basename(d.file)));
136
+ if (d?.pattern) parts.push(kleur.dim(`/${d.pattern}/`));
137
+ if (d?.url) parts.push(kleur.dim(d.url.slice(0, 72)));
138
+ if (d?.query) parts.push(kleur.dim(d.query.slice(0, 72)));
139
+ if (d?.prompt) parts.push(kleur.dim(d.prompt.slice(0, 72)));
140
+ } else {
141
+ if (event.issue_id) parts.push(kleur.yellow(event.issue_id));
142
+ if (d?.file) parts.push(kleur.dim(basename(d.file)));
143
+ if (d?.reason_code) parts.push(kleur.dim(`[${d.reason_code}]`));
144
+ if (event.worktree) parts.push(kleur.dim(`wt:${event.worktree}`));
145
+ }
146
+
147
+ return parts.join(' ') || kleur.dim('—');
148
+ }
149
+
150
+ function formatLine(event: XtrmEvent, colorMap: Map<string, ColorFn>): string {
151
+ const time = kleur.dim(fmtTime(event.ts));
152
+ const colorFn = colorMap.get(event.session_id) ?? kleur.white;
153
+ const session = colorFn(event.session_id.slice(0, 8));
154
+ const label = getLabel(event);
155
+ const detail = buildDetail(event);
156
+ return `${time} ${label} ${session} ${detail}`;
157
+ }
158
+
159
+ // ── SQLite queries ────────────────────────────────────────────────────────────
160
+
161
+ function findDbPath(cwd: string): string | null {
162
+ let dir = cwd;
163
+ for (let i = 0; i < 10; i++) {
164
+ if (existsSync(join(dir, '.beads'))) return join(dir, '.xtrm', 'debug.db');
165
+ const parent = join(dir, '..');
166
+ if (parent === dir) break;
167
+ dir = parent;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ function buildWhere(opts: DebugOptions, base: string): string {
173
+ const clauses: string[] = [];
174
+ if (base) clauses.push(base);
175
+ if (opts.session) {
176
+ const s = opts.session.replace(/'/g, "''");
177
+ clauses.push(`session_id LIKE '${s}%'`);
178
+ }
179
+ if (opts.type) {
180
+ const t = opts.type.replace(/'/g, "''");
181
+ clauses.push(`kind LIKE '${t}.%' OR kind = '${t}'`);
182
+ }
183
+ return clauses.length ? clauses.join(' AND ') : '';
184
+ }
185
+
186
+ function queryEvents(dbPath: string, where: string, limit: number): XtrmEvent[] {
187
+ const sql = `SELECT id,ts,session_id,runtime,worktree,kind,tool_name,outcome,issue_id,duration_ms,data FROM events${where ? ` WHERE ${where}` : ''} ORDER BY id ASC LIMIT ${limit}`;
188
+
189
+ const result = spawnSync('sqlite3', [dbPath, '-json', sql], {
190
+ stdio: ['pipe', 'pipe', 'pipe'],
191
+ encoding: 'utf8',
192
+ timeout: 5000,
193
+ });
194
+
195
+ if (result.status !== 0 || !result.stdout.trim()) return [];
196
+ try { return JSON.parse(result.stdout); } catch { return []; }
197
+ }
198
+
199
+ // ── Follow mode ───────────────────────────────────────────────────────────────
200
+
201
+ function follow(dbPath: string, opts: DebugOptions): void {
202
+ const sinceTs = Date.now() - 5 * 60 * 1000;
203
+ const initial = queryEvents(dbPath, buildWhere(opts, `ts >= ${sinceTs}`), 200);
204
+
205
+ const colorMap = buildColorMap(initial);
206
+ let lastId = 0;
207
+
208
+ for (const ev of initial) {
209
+ if (ev.id > lastId) lastId = ev.id;
210
+ opts.json ? console.log(JSON.stringify(ev)) : console.log(formatLine(ev, colorMap));
211
+ }
212
+
213
+ // Poll every 2s — clean integer comparison, no datetime overlap needed
214
+ const interval = setInterval(() => {
215
+ const events = queryEvents(dbPath, buildWhere(opts, `id > ${lastId}`), 50);
216
+ if (events.length > 0) {
217
+ extendColorMap(colorMap, events);
218
+ for (const ev of events) {
219
+ if (ev.id > lastId) lastId = ev.id;
220
+ opts.json ? console.log(JSON.stringify(ev)) : console.log(formatLine(ev, colorMap));
221
+ }
222
+ }
223
+ }, 2000);
224
+
225
+ process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
226
+ }
227
+
228
+ // ── Command ───────────────────────────────────────────────────────────────────
229
+
230
+ export function createDebugCommand(): Command {
231
+ return new Command('debug')
232
+ .description('Watch xtrm events: tool calls, gate decisions, bd lifecycle')
233
+ .option('-f, --follow', 'Follow new events (default)', false)
234
+ .option('--all', 'Show full history and exit', false)
235
+ .option('--session <id>', 'Filter by session ID (prefix match)')
236
+ .option('--type <domain>', 'Filter by domain: tool | gate | bd | session')
237
+ .option('--json', 'Output raw JSON lines', false)
238
+ .action((opts: DebugOptions) => {
239
+ const cwd = process.cwd();
240
+ const dbPath = findDbPath(cwd);
241
+
242
+ if (!dbPath || !existsSync(dbPath)) return;
243
+
244
+ if (opts.all) {
245
+ const events = queryEvents(dbPath, buildWhere(opts, ''), 1000);
246
+ const colorMap = buildColorMap(events);
247
+ for (const ev of events) {
248
+ opts.json ? console.log(JSON.stringify(ev)) : console.log(formatLine(ev, colorMap));
249
+ }
250
+ return;
251
+ }
252
+
253
+ follow(dbPath, opts);
254
+ });
255
+ }
@@ -0,0 +1,180 @@
1
+ import { Command } from 'commander';
2
+ import kleur from 'kleur';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { findRepoRoot } from '../utils/repo-root.js';
6
+ import { t, sym } from '../utils/theme.js';
7
+
8
+ interface Frontmatter {
9
+ [key: string]: string | undefined;
10
+ }
11
+
12
+ interface DocEntry {
13
+ filePath: string;
14
+ relativePath: string;
15
+ frontmatter: Frontmatter | null;
16
+ sizeBytes: number;
17
+ lastModified: Date;
18
+ parseError?: string;
19
+ }
20
+
21
+ const REQUIRED_FIELDS = new Set(['title', 'type', 'status', 'updated_at', 'version']);
22
+
23
+ /** Parse YAML frontmatter from a markdown file (--- delimited block). */
24
+ function parseFrontmatter(content: string): Frontmatter | null {
25
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
26
+ if (!match) return null;
27
+
28
+ const fm: Frontmatter = {};
29
+ for (const line of match[1].split('\n')) {
30
+ const colon = line.indexOf(':');
31
+ if (colon === -1) continue;
32
+ const key = line.slice(0, colon).trim();
33
+ const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
34
+ if (key) fm[key] = value;
35
+ }
36
+ return fm;
37
+ }
38
+
39
+ /** Collect all target doc files in a repo. */
40
+ async function collectDocFiles(repoRoot: string, filterPattern?: string): Promise<DocEntry[]> {
41
+ const candidates: string[] = [];
42
+
43
+ // Fixed candidates
44
+ for (const name of ['README.md', 'CHANGELOG.md']) {
45
+ const p = path.join(repoRoot, name);
46
+ if (await fs.pathExists(p)) candidates.push(p);
47
+ }
48
+
49
+ // docs/ directory
50
+ const docsDir = path.join(repoRoot, 'docs');
51
+ if (await fs.pathExists(docsDir)) {
52
+ const entries = await fs.readdir(docsDir);
53
+ for (const entry of entries) {
54
+ if (entry.endsWith('.md')) candidates.push(path.join(docsDir, entry));
55
+ }
56
+ }
57
+
58
+ const results: DocEntry[] = [];
59
+ for (const filePath of candidates) {
60
+ const rel = path.relative(repoRoot, filePath);
61
+
62
+ // Apply filter if provided
63
+ if (filterPattern && !rel.includes(filterPattern) && !path.basename(filePath).includes(filterPattern)) {
64
+ continue;
65
+ }
66
+
67
+ let entry: DocEntry;
68
+ try {
69
+ const stat = await fs.stat(filePath);
70
+ const content = await fs.readFile(filePath, 'utf8');
71
+ const frontmatter = parseFrontmatter(content);
72
+ entry = {
73
+ filePath,
74
+ relativePath: rel,
75
+ frontmatter,
76
+ sizeBytes: stat.size,
77
+ lastModified: stat.mtime,
78
+ };
79
+ } catch (err: any) {
80
+ entry = {
81
+ filePath,
82
+ relativePath: rel,
83
+ frontmatter: null,
84
+ sizeBytes: 0,
85
+ lastModified: new Date(0),
86
+ parseError: err.message,
87
+ };
88
+ }
89
+ results.push(entry);
90
+ }
91
+
92
+ return results;
93
+ }
94
+
95
+ function formatSize(bytes: number): string {
96
+ if (bytes < 1024) return `${bytes}B`;
97
+ return `${(bytes / 1024).toFixed(1)}KB`;
98
+ }
99
+
100
+ function formatDate(d: Date): string {
101
+ return d.toISOString().slice(0, 10);
102
+ }
103
+
104
+ function printEntry(entry: DocEntry, raw: boolean): void {
105
+ const header = kleur.bold().white(entry.relativePath);
106
+ const meta = kleur.gray(` ${formatSize(entry.sizeBytes)} modified ${formatDate(entry.lastModified)}`);
107
+ console.log(`\n${header}${meta}`);
108
+
109
+ if (entry.parseError) {
110
+ console.log(kleur.red(` ✗ Error reading file: ${entry.parseError}`));
111
+ return;
112
+ }
113
+
114
+ if (!entry.frontmatter || Object.keys(entry.frontmatter).length === 0) {
115
+ console.log(kleur.gray(' (no frontmatter)'));
116
+ return;
117
+ }
118
+
119
+ if (raw) {
120
+ console.log(kleur.gray(' ---'));
121
+ for (const [k, v] of Object.entries(entry.frontmatter)) {
122
+ console.log(` ${k}: ${v}`);
123
+ }
124
+ console.log(kleur.gray(' ---'));
125
+ return;
126
+ }
127
+
128
+ for (const [k, v] of Object.entries(entry.frontmatter)) {
129
+ const keyStr = REQUIRED_FIELDS.has(k)
130
+ ? kleur.cyan(k.padEnd(14))
131
+ : kleur.gray(k.padEnd(14));
132
+ const valStr = v ?? '';
133
+ console.log(` ${keyStr} ${valStr}`);
134
+ }
135
+ }
136
+
137
+ export function createDocsCommand(): Command {
138
+ const docs = new Command('docs')
139
+ .description('Documentation management commands');
140
+
141
+ docs
142
+ .command('show [filter]')
143
+ .description('Display frontmatters for README, CHANGELOG, and docs/ files')
144
+ .option('--raw', 'Output raw YAML frontmatter', false)
145
+ .option('--json', 'Output JSON', false)
146
+ .action(async (filter: string | undefined, opts: { raw: boolean; json: boolean }) => {
147
+ const repoRoot = await findRepoRoot();
148
+ const entries = await collectDocFiles(repoRoot, filter);
149
+
150
+ if (entries.length === 0) {
151
+ console.log(kleur.yellow('\n No documentation files found.\n'));
152
+ return;
153
+ }
154
+
155
+ if (opts.json) {
156
+ const output = entries.map(e => ({
157
+ path: e.relativePath,
158
+ sizeBytes: e.sizeBytes,
159
+ lastModified: e.lastModified.toISOString(),
160
+ frontmatter: e.frontmatter,
161
+ parseError: e.parseError ?? null,
162
+ }));
163
+ console.log(JSON.stringify(output, null, 2));
164
+ return;
165
+ }
166
+
167
+ for (const entry of entries) {
168
+ printEntry(entry, opts.raw);
169
+ }
170
+
171
+ const without = entries.filter(e => !e.frontmatter || Object.keys(e.frontmatter).length === 0).length;
172
+ console.log(
173
+ `\n ${sym.ok} ${entries.length} file${entries.length !== 1 ? 's' : ''}` +
174
+ (without > 0 ? kleur.gray(` (${without} without frontmatter)`) : '') +
175
+ '\n'
176
+ );
177
+ });
178
+
179
+ return docs;
180
+ }