@claudemini/shit-cli 1.0.3

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/lib/log.js ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * shit log — event ingestion dispatcher.
5
+ * Reads stdin, parses event, delegates to session/extract/report modules.
6
+ */
7
+
8
+ import { appendFileSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { getProjectRoot, getLogDir } from './config.js';
11
+ import { loadState, saveState, processEvent, updateIndex } from './session.js';
12
+ import { extractIntent, extractChanges, classifySession } from './extract.js';
13
+ import { generateReports } from './report.js';
14
+
15
+ export default async function log(args) {
16
+ const hookType = args[0] || 'unknown';
17
+
18
+ // Read payload from stdin
19
+ let payload = '';
20
+ for await (const chunk of process.stdin) {
21
+ payload += chunk;
22
+ }
23
+ if (!payload.trim()) process.exit(0);
24
+
25
+ let event;
26
+ try {
27
+ event = JSON.parse(payload);
28
+ } catch {
29
+ process.exit(1);
30
+ }
31
+
32
+ const projectRoot = getProjectRoot();
33
+ const logDir = getLogDir(projectRoot);
34
+ const sessionId = event.session_id || 'unknown';
35
+ const sessionDir = join(logDir, sessionId);
36
+ mkdirSync(sessionDir, { recursive: true });
37
+
38
+ // 1. Append raw event
39
+ appendFileSync(join(sessionDir, 'events.jsonl'), payload.trim() + '\n');
40
+
41
+ // 2. Load and update state
42
+ const state = loadState(sessionDir);
43
+ if (!state) process.exit(1);
44
+ processEvent(state, event, hookType, projectRoot);
45
+ saveState(sessionDir, state);
46
+
47
+ // 3. Extract semantics and generate reports
48
+ const intent = extractIntent(state.prompts);
49
+ const changes = extractChanges(state);
50
+ const classification = classifySession(intent, changes);
51
+ generateReports(sessionDir, sessionId, state, intent, changes, classification);
52
+
53
+ // 4. On session end: shadow branch + update index
54
+ if (hookType === 'session-end' || hookType === 'stop') {
55
+ try {
56
+ const { commitShadow } = await import('./git-shadow.js');
57
+ await commitShadow(projectRoot, sessionDir, sessionId);
58
+ } catch { /* shadow is best-effort */ }
59
+
60
+ try {
61
+ updateIndex(logDir, sessionId, intent, classification, changes, state);
62
+ } catch { /* index is best-effort */ }
63
+ }
64
+
65
+ process.exit(0);
66
+ }
package/lib/query.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * shit query — cross-session memory queries.
5
+ * Reads index.json to answer questions about past sessions.
6
+ */
7
+
8
+ import { readFileSync, existsSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { getProjectRoot, getLogDir } from './config.js';
11
+
12
+ function loadIndex(logDir) {
13
+ const indexFile = join(logDir, 'index.json');
14
+ if (!existsSync(indexFile)) return null;
15
+ try {
16
+ return JSON.parse(readFileSync(indexFile, 'utf-8'));
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export default async function query(args) {
23
+ const logDir = getLogDir(getProjectRoot());
24
+ const index = loadIndex(logDir);
25
+
26
+ if (!index || index.sessions.length === 0) {
27
+ console.log('No session index found. Sessions are indexed on session-end.');
28
+ console.log('Run "shit list" to see raw sessions.');
29
+ return;
30
+ }
31
+
32
+ const jsonOutput = args.includes('--json');
33
+ const fileArg = args.find(a => a.startsWith('--file='));
34
+ const recentArg = args.find(a => a.startsWith('--recent='));
35
+ const typeArg = args.find(a => a.startsWith('--type='));
36
+ const riskArg = args.find(a => a.startsWith('--risk='));
37
+
38
+ let results = [...index.sessions];
39
+
40
+ // Filter by file
41
+ if (fileArg) {
42
+ const filePath = fileArg.split('=')[1];
43
+ const sessionIds = index.file_history?.[filePath] || [];
44
+ if (sessionIds.length === 0) {
45
+ // Try partial match
46
+ const matchingFiles = Object.keys(index.file_history || {})
47
+ .filter(f => f.includes(filePath));
48
+ const matchedIds = new Set();
49
+ for (const f of matchingFiles) {
50
+ for (const id of index.file_history[f]) matchedIds.add(id);
51
+ }
52
+ results = results.filter(s => matchedIds.has(s.id));
53
+ if (matchingFiles.length > 0 && !jsonOutput) {
54
+ console.log(`Matched files: ${matchingFiles.join(', ')}\n`);
55
+ }
56
+ } else {
57
+ const idSet = new Set(sessionIds);
58
+ results = results.filter(s => idSet.has(s.id));
59
+ }
60
+ }
61
+
62
+ // Filter by type
63
+ if (typeArg) {
64
+ const type = typeArg.split('=')[1];
65
+ results = results.filter(s => s.type === type);
66
+ }
67
+
68
+ // Filter by risk
69
+ if (riskArg) {
70
+ const risk = riskArg.split('=')[1];
71
+ results = results.filter(s => s.risk === risk);
72
+ }
73
+
74
+ // Limit by recent
75
+ if (recentArg) {
76
+ const n = parseInt(recentArg.split('=')[1]) || 5;
77
+ results = results.slice(-n);
78
+ }
79
+
80
+ // Output
81
+ if (jsonOutput) {
82
+ console.log(JSON.stringify(results, null, 2));
83
+ return;
84
+ }
85
+
86
+ if (results.length === 0) {
87
+ console.log('No matching sessions found.');
88
+ return;
89
+ }
90
+
91
+ console.log(`${results.length} session(s):\n`);
92
+ for (const s of results.reverse()) {
93
+ const filesCount = s.files?.length || 0;
94
+ console.log(` ${s.id.slice(0, 8)} ${s.date} [${s.type}] risk:${s.risk} ${s.duration}min ${filesCount} files`);
95
+ if (s.intent) {
96
+ console.log(` ${s.intent.slice(0, 100)}`);
97
+ }
98
+ }
99
+
100
+ // Show file history summary if --file was used
101
+ if (fileArg) {
102
+ const filePath = fileArg.split('=')[1];
103
+ const history = index.file_history?.[filePath];
104
+ if (history) {
105
+ console.log(`\nFile "${filePath}" modified in ${history.length} session(s)`);
106
+ }
107
+ }
108
+
109
+ console.log('\nOptions: --file=<path> --type=<type> --risk=<level> --recent=<n> --json');
110
+ }
package/lib/report.js ADDED
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Report generation module.
5
+ * Produces summary.json (v2, bot-readable), summary.txt (human-readable),
6
+ * prompts.txt, and metadata.json from session state + semantic data.
7
+ */
8
+
9
+ import { writeFileSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ /**
13
+ * Generate all report files for a session.
14
+ */
15
+ export function generateReports(sessionDir, sessionId, state, intent, changes, classification) {
16
+ const durationMs = state.start_time && state.last_time
17
+ ? new Date(state.last_time) - new Date(state.start_time) : 0;
18
+ const durationMin = Math.round(durationMs / 60000);
19
+
20
+ writeSummaryJson(sessionDir, sessionId, state, intent, changes, classification, durationMin);
21
+ writeSummaryTxt(sessionDir, sessionId, state, intent, changes, classification, durationMin);
22
+ writePromptsTxt(sessionDir, state);
23
+ writeMetadataJson(sessionDir, sessionId, state, intent, classification, durationMin);
24
+ }
25
+
26
+ function writeSummaryJson(sessionDir, sessionId, state, intent, changes, classification, durationMin) {
27
+ const summary = {
28
+ version: '2.0',
29
+ session: {
30
+ id: sessionId,
31
+ start: state.start_time,
32
+ end: state.last_time,
33
+ duration_minutes: durationMin,
34
+ type: classification.type,
35
+ intent: intent.goal,
36
+ risk: classification.risk,
37
+ summary: classification.summary,
38
+ },
39
+ changes: {
40
+ files: changes.files
41
+ .filter(f => f.operations.some(op => op !== 'read'))
42
+ .map(f => ({
43
+ path: f.path,
44
+ category: f.category,
45
+ operations: f.operations.filter(op => op !== 'read'),
46
+ editCount: f.editCount,
47
+ editSummary: f.editSummary,
48
+ })),
49
+ summary: changes.summary,
50
+ },
51
+ activity: {
52
+ tools: state.tool_counts,
53
+ commands: changes.commands,
54
+ errors: state.errors,
55
+ },
56
+ review_hints: {
57
+ tests_run: classification.reviewHints.testsRun,
58
+ build_verified: classification.reviewHints.buildVerified,
59
+ files_without_tests: classification.reviewHints.filesWithoutTests,
60
+ large_change: classification.reviewHints.largeChange,
61
+ config_changed: classification.reviewHints.configChanged,
62
+ migration_added: classification.reviewHints.migrationAdded,
63
+ },
64
+ prompts: state.prompts.map(p => typeof p === 'string' ? p : p.text),
65
+ scope: intent.scope,
66
+ };
67
+ writeFileSync(join(sessionDir, 'summary.json'), JSON.stringify(summary, null, 2));
68
+ }
69
+
70
+ function writeSummaryTxt(sessionDir, sessionId, state, intent, changes, classification, durationMin) {
71
+ const lines = [];
72
+
73
+ // Header
74
+ lines.push(`# Session: ${sessionId.slice(0, 8)}...`);
75
+ lines.push(`Type: ${classification.type} | Risk: ${classification.risk} | Duration: ${durationMin}min`);
76
+ if (classification.summary) {
77
+ lines.push(`Summary: ${classification.summary}`);
78
+ }
79
+ lines.push('');
80
+
81
+ // Intent
82
+ if (intent.goal) {
83
+ lines.push('## Intent');
84
+ lines.push(` Goal: ${intent.goal.slice(0, 200)}`);
85
+ if (intent.scope.length > 0) {
86
+ lines.push(` Scope: ${intent.scope.join(', ')}`);
87
+ }
88
+ lines.push('');
89
+ }
90
+
91
+ // Changes
92
+ const modified = changes.files.filter(f => f.operations.some(op => op !== 'read'));
93
+ if (modified.length > 0) {
94
+ lines.push('## Changes');
95
+ for (const f of modified.slice(0, 20)) {
96
+ const ops = f.operations.filter(op => op !== 'read').join(',');
97
+ lines.push(` [${f.category}] ${f.path} (${ops}${f.editCount > 0 ? ` x${f.editCount}` : ''})`);
98
+ }
99
+ if (modified.length > 20) {
100
+ lines.push(` ... and ${modified.length - 20} more files`);
101
+ }
102
+ lines.push('');
103
+ }
104
+
105
+ // Tools
106
+ if (Object.keys(state.tool_counts).length > 0) {
107
+ lines.push('## Tools');
108
+ Object.entries(state.tool_counts)
109
+ .sort((a, b) => b[1] - a[1])
110
+ .forEach(([t, c]) => lines.push(` ${t}: ${c}`));
111
+ lines.push('');
112
+ }
113
+
114
+ // Commands by category
115
+ const cmdEntries = Object.entries(changes.commands).filter(([, cmds]) => cmds.length > 0);
116
+ if (cmdEntries.length > 0) {
117
+ lines.push('## Commands');
118
+ for (const [cat, cmds] of cmdEntries) {
119
+ lines.push(` [${cat}] ${cmds.slice(0, 5).join(' | ')}${cmds.length > 5 ? ` (+${cmds.length - 5})` : ''}`);
120
+ }
121
+ lines.push('');
122
+ }
123
+
124
+ // Review hints
125
+ const hints = classification.reviewHints;
126
+ lines.push('## Review Hints');
127
+ lines.push(` Tests run: ${hints.testsRun ? 'YES' : 'NO'}`);
128
+ lines.push(` Build verified: ${hints.buildVerified ? 'YES' : 'NO'}`);
129
+ if (hints.configChanged) lines.push(' WARNING: Config files changed');
130
+ if (hints.migrationAdded) lines.push(' WARNING: Database migration added');
131
+ if (hints.largeChange) lines.push(' WARNING: Large change (>10 files)');
132
+ if (hints.filesWithoutTests.length > 0) {
133
+ lines.push(` Files without tests: ${hints.filesWithoutTests.slice(0, 5).join(', ')}`);
134
+ }
135
+ lines.push('');
136
+
137
+ // Errors
138
+ if (state.errors.length > 0) {
139
+ lines.push('## Errors');
140
+ state.errors.slice(-5).forEach(e => lines.push(` [${e.tool}] ${(e.message || '').slice(0, 100)}`));
141
+ lines.push('');
142
+ }
143
+
144
+ // Prompts
145
+ if (state.prompts.length > 0) {
146
+ lines.push('## Prompts');
147
+ state.prompts.forEach(p => {
148
+ const text = typeof p === 'string' ? p : p.text;
149
+ lines.push(` > ${text.slice(0, 120)}`);
150
+ });
151
+ }
152
+
153
+ writeFileSync(join(sessionDir, 'summary.txt'), lines.join('\n') + '\n');
154
+ }
155
+
156
+ function writePromptsTxt(sessionDir, state) {
157
+ if (state.prompts.length > 0) {
158
+ const promptLines = state.prompts
159
+ .map(p => {
160
+ const time = typeof p === 'string' ? '' : p.time;
161
+ const text = typeof p === 'string' ? p : p.text;
162
+ return time ? `=== ${time} ===\n${text}\n` : `${text}\n`;
163
+ })
164
+ .join('\n');
165
+ writeFileSync(join(sessionDir, 'prompts.txt'), promptLines);
166
+ }
167
+ }
168
+
169
+ function writeMetadataJson(sessionDir, sessionId, state, intent, classification, durationMin) {
170
+ writeFileSync(join(sessionDir, 'metadata.json'), JSON.stringify({
171
+ session_id: sessionId,
172
+ start_time: state.start_time,
173
+ last_updated: state.last_time,
174
+ duration_minutes: durationMin,
175
+ type: classification.type,
176
+ intent: intent.goal.slice(0, 200),
177
+ risk: classification.risk,
178
+ event_count: state.event_count,
179
+ tool_calls: Object.values(state.tool_counts).reduce((a, b) => a + b, 0),
180
+ files_touched: state.file_ops.write.length + state.file_ops.edit.length,
181
+ errors: state.errors.length,
182
+ scope: intent.scope,
183
+ last_hook_type: state.last_hook_type,
184
+ }, null, 2));
185
+ }
package/lib/session.js ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Session state management and cross-session index.
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { toRelative } from './config.js';
10
+
11
+ // --- State management ---
12
+
13
+ const EMPTY_STATE = {
14
+ start_time: null,
15
+ last_time: null,
16
+ event_count: 0,
17
+ tool_counts: {},
18
+ file_ops: { read: [], write: [], edit: [], glob: [], grep: [] },
19
+ commands: [],
20
+ edits: [],
21
+ errors: [],
22
+ prompts: [],
23
+ last_hook_type: null,
24
+ shadow_branch: null,
25
+ };
26
+
27
+ export function loadState(sessionDir) {
28
+ const stateFile = join(sessionDir, 'state.json');
29
+ if (!existsSync(stateFile)) {
30
+ return {
31
+ ...EMPTY_STATE,
32
+ file_ops: { read: [], write: [], edit: [], glob: [], grep: [] },
33
+ commands: [],
34
+ edits: [],
35
+ errors: [],
36
+ prompts: [],
37
+ };
38
+ }
39
+ try {
40
+ return JSON.parse(readFileSync(stateFile, 'utf-8'));
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ export function saveState(sessionDir, state) {
47
+ writeFileSync(join(sessionDir, 'state.json'), JSON.stringify(state, null, 2));
48
+ }
49
+
50
+ // --- Event processing ---
51
+
52
+ function addUnique(arr, val) {
53
+ if (val && !arr.includes(val)) arr.push(val);
54
+ }
55
+
56
+ export function processEvent(state, event, hookType, projectRoot) {
57
+ const now = new Date().toISOString();
58
+ state.event_count++;
59
+ state.last_hook_type = hookType;
60
+ state.last_time = now;
61
+ if (!state.start_time) state.start_time = now;
62
+
63
+ const toolName = event.tool_name;
64
+ const input = event.tool_input || {};
65
+
66
+ // User prompts
67
+ if (hookType === 'user-prompt-submit') {
68
+ const prompt = event.prompt || event.message || '';
69
+ if (prompt) state.prompts.push({ time: now, text: prompt });
70
+ return;
71
+ }
72
+
73
+ // Tool events
74
+ if (hookType === 'post-tool-use' && toolName) {
75
+ state.tool_counts[toolName] = (state.tool_counts[toolName] || 0) + 1;
76
+
77
+ const rel = (p) => toRelative(projectRoot, p);
78
+
79
+ switch (toolName) {
80
+ case 'Read':
81
+ addUnique(state.file_ops.read, rel(input.file_path));
82
+ break;
83
+ case 'Write':
84
+ addUnique(state.file_ops.write, rel(input.file_path));
85
+ break;
86
+ case 'Edit':
87
+ addUnique(state.file_ops.edit, rel(input.file_path));
88
+ if (input.old_string || input.new_string) {
89
+ state.edits.push({
90
+ file: rel(input.file_path),
91
+ old: (input.old_string || '').slice(0, 200),
92
+ new: (input.new_string || '').slice(0, 200),
93
+ });
94
+ }
95
+ break;
96
+ case 'Glob':
97
+ addUnique(state.file_ops.glob, input.pattern);
98
+ break;
99
+ case 'Grep':
100
+ addUnique(state.file_ops.grep, input.pattern);
101
+ break;
102
+ case 'Bash':
103
+ if (input.command) state.commands.push(input.command.slice(0, 500));
104
+ break;
105
+ }
106
+
107
+ // Track errors
108
+ const result = event.tool_result;
109
+ if (result && typeof result === 'object' && result.isError) {
110
+ state.errors.push({
111
+ tool: toolName,
112
+ time: now,
113
+ message: (typeof result.content === 'string'
114
+ ? result.content : JSON.stringify(result.content)).slice(0, 300),
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+ // --- Cross-session index ---
121
+
122
+ export function updateIndex(logDir, sessionId, intent, classification, changes, state) {
123
+ const indexFile = join(logDir, 'index.json');
124
+ let index = { project: '', sessions: [], file_history: {} };
125
+
126
+ if (existsSync(indexFile)) {
127
+ try {
128
+ index = JSON.parse(readFileSync(indexFile, 'utf-8'));
129
+ } catch { /* start fresh */ }
130
+ }
131
+
132
+ // Detect project name from git or directory
133
+ if (!index.project) {
134
+ const parts = logDir.split('/');
135
+ // logDir is typically <project>/.shit-logs
136
+ index.project = parts[parts.length - 2] || 'unknown';
137
+ }
138
+
139
+ const durationMs = state.start_time && state.last_time
140
+ ? new Date(state.last_time) - new Date(state.start_time) : 0;
141
+ const durationMin = Math.round(durationMs / 60000);
142
+
143
+ // Upsert session entry
144
+ const modifiedFiles = changes.files
145
+ .filter(f => f.operations.some(op => op !== 'read'))
146
+ .map(f => f.path);
147
+
148
+ const existingIdx = index.sessions.findIndex(s => s.id === sessionId);
149
+ const sessionEntry = {
150
+ id: sessionId,
151
+ date: (state.start_time || new Date().toISOString()).slice(0, 10),
152
+ type: classification.type,
153
+ intent: intent.goal.slice(0, 200),
154
+ files: modifiedFiles,
155
+ duration: durationMin,
156
+ risk: classification.risk,
157
+ };
158
+
159
+ if (existingIdx >= 0) {
160
+ index.sessions[existingIdx] = sessionEntry;
161
+ } else {
162
+ index.sessions.push(sessionEntry);
163
+ }
164
+
165
+ // Keep last 100 sessions
166
+ if (index.sessions.length > 100) {
167
+ index.sessions = index.sessions.slice(-100);
168
+ }
169
+
170
+ // Update file history
171
+ if (!index.file_history) index.file_history = {};
172
+ for (const file of modifiedFiles) {
173
+ if (!index.file_history[file]) index.file_history[file] = [];
174
+ if (!index.file_history[file].includes(sessionId)) {
175
+ index.file_history[file].push(sessionId);
176
+ }
177
+ // Keep last 20 sessions per file
178
+ if (index.file_history[file].length > 20) {
179
+ index.file_history[file] = index.file_history[file].slice(-20);
180
+ }
181
+ }
182
+
183
+ writeFileSync(indexFile, JSON.stringify(index, null, 2));
184
+ }
package/lib/shadow.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'child_process';
4
+ import { getProjectRoot } from './config.js';
5
+
6
+ export { commitShadow, listShadowBranches, shadowInfo } from './git-shadow.js';
7
+
8
+ export default async function shadowCmd(args) {
9
+ const projectRoot = getProjectRoot();
10
+ const sub = args[0];
11
+
12
+ if (sub === 'info' && args[1]) {
13
+ const { shadowInfo } = await import('./git-shadow.js');
14
+ const info = shadowInfo(projectRoot, args[1]);
15
+ if (!info) {
16
+ console.error(`Branch not found: ${args[1]}`);
17
+ process.exit(1);
18
+ }
19
+ console.log('Recent commits:');
20
+ console.log(info.log);
21
+ console.log('\nFiles:');
22
+ info.files.forEach(f => console.log(` ${f}`));
23
+ return;
24
+ }
25
+
26
+ // Default: list shadow branches
27
+ const { listShadowBranches } = await import('./git-shadow.js');
28
+ const branches = listShadowBranches(projectRoot);
29
+
30
+ if (branches.length === 0) {
31
+ console.log('No shadow branches found.');
32
+ console.log('Shadow branches are created on session-end.');
33
+ return;
34
+ }
35
+
36
+ console.log(`${branches.length} shadow branch(es):\n`);
37
+ branches.forEach(b => {
38
+ try {
39
+ const log = execSync(`git log ${b} --oneline -1`, {
40
+ cwd: projectRoot, encoding: 'utf-8',
41
+ }).trim();
42
+ console.log(` ${b} ${log}`);
43
+ } catch {
44
+ console.log(` ${b}`);
45
+ }
46
+ });
47
+
48
+ console.log('\nUsage:');
49
+ console.log(' shit shadow info <branch> # Show branch details');
50
+ console.log(' git log <branch> --oneline # View commits');
51
+ }
package/lib/view.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { getProjectRoot, getLogDir } from './config.js';
6
+
7
+ export default async function view(args) {
8
+ const sessionId = args[0];
9
+ if (!sessionId) {
10
+ console.error('Usage: shit view <session-id>');
11
+ process.exit(1);
12
+ }
13
+
14
+ const logDir = getLogDir(getProjectRoot());
15
+ const sessionDir = join(logDir, sessionId);
16
+ if (!existsSync(sessionDir)) {
17
+ console.error(`Session not found: ${sessionId}`);
18
+ process.exit(1);
19
+ }
20
+
21
+ // Show summary.txt (contains semantic report)
22
+ const summaryFile = join(sessionDir, 'summary.txt');
23
+ if (existsSync(summaryFile)) {
24
+ console.log(readFileSync(summaryFile, 'utf-8'));
25
+ }
26
+
27
+ // Show summary.json v2 data if --json flag
28
+ if (args.includes('--json')) {
29
+ const jsonFile = join(sessionDir, 'summary.json');
30
+ if (existsSync(jsonFile)) {
31
+ console.log('\n--- summary.json ---');
32
+ console.log(readFileSync(jsonFile, 'utf-8'));
33
+ }
34
+ }
35
+
36
+ // Show shadow branch info
37
+ const stateFile = join(sessionDir, 'state.json');
38
+ if (existsSync(stateFile)) {
39
+ try {
40
+ const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
41
+ if (state.shadow_branch) {
42
+ console.log(`\nShadow branch: ${state.shadow_branch}`);
43
+ console.log(` git log ${state.shadow_branch} --oneline`);
44
+ console.log(` git show ${state.shadow_branch}:.shit-logs/${sessionId}/summary.json`);
45
+ }
46
+ } catch { /* ignore */ }
47
+ }
48
+
49
+ console.log(`\nFull logs: ${sessionDir}`);
50
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@claudemini/shit-cli",
3
+ "version": "1.0.3",
4
+ "description": "Session-based Hook Intelligence Tracker - CLI tool for logging Claude Code hooks",
5
+ "type": "module",
6
+ "bin": {
7
+ "shit": "./bin/shit.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "claude-code",
14
+ "hooks",
15
+ "logging",
16
+ "session-tracking"
17
+ ],
18
+ "author": "",
19
+ "license": "MIT",
20
+ "dependencies": {},
21
+ "devDependencies": {}
22
+ }