@claudemini/shit-cli 1.2.0 → 1.3.0

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/bin/shit.js CHANGED
@@ -4,22 +4,24 @@ const args = process.argv.slice(2);
4
4
  const command = args[0];
5
5
 
6
6
  const commands = {
7
- enable: 'Enable shit-cli in repository',
8
- disable: 'Remove shit-cli hooks',
9
- status: 'Show current session status',
10
- init: 'Initialize hooks in .claude/settings.json',
11
- log: 'Log a hook event (called by hooks)',
12
- commit: 'Create checkpoint on git commit (hook)',
13
- list: 'List all sessions',
14
- checkpoints:'List all checkpoints',
15
- view: 'View session details',
16
- query: 'Query session memory (cross-session)',
17
- rewind: 'Rollback to previous checkpoint',
18
- resume: 'Resume session from checkpoint',
19
- doctor: 'Fix or clean stuck sessions',
20
- shadow: 'List shadow branches',
21
- clean: 'Clean old sessions',
22
- help: 'Show help',
7
+ enable: 'Enable shit-cli in repository',
8
+ disable: 'Remove shit-cli hooks',
9
+ status: 'Show current session status',
10
+ init: 'Initialize hooks in .claude/settings.json',
11
+ log: 'Log a hook event (called by hooks)',
12
+ commit: 'Create checkpoint on git commit (hook)',
13
+ list: 'List all sessions',
14
+ checkpoints: 'List all checkpoints',
15
+ view: 'View session details',
16
+ query: 'Query session memory (cross-session)',
17
+ explain: 'Explain a session or commit',
18
+ rewind: 'Rollback to previous checkpoint',
19
+ resume: 'Resume session from checkpoint',
20
+ reset: 'Delete checkpoint for current HEAD',
21
+ doctor: 'Fix or clean stuck sessions',
22
+ shadow: 'List shadow branches',
23
+ clean: 'Clean old sessions',
24
+ help: 'Show help',
23
25
  };
24
26
 
25
27
  function showHelp() {
package/lib/enable.js CHANGED
@@ -169,6 +169,10 @@ export default async function enable(args) {
169
169
  // Parse arguments
170
170
  const agents = [];
171
171
  let addCheckpointHook = false;
172
+ let useLocal = false;
173
+ let force = false;
174
+ let pushSessions = true;
175
+ let telemetry = true;
172
176
 
173
177
  for (const arg of args) {
174
178
  if (arg === '--all') {
@@ -176,6 +180,18 @@ export default async function enable(args) {
176
180
  agents.push(...Object.keys(AGENT_HOOKS));
177
181
  } else if (arg === '--checkpoint' || arg === '-c') {
178
182
  addCheckpointHook = true;
183
+ } else if (arg === '--local') {
184
+ useLocal = true;
185
+ } else if (arg === '--project') {
186
+ useLocal = false; // Force project-level settings
187
+ } else if (arg === '--force' || arg === '-f') {
188
+ force = true;
189
+ } else if (arg === '--skip-push-sessions') {
190
+ pushSessions = false;
191
+ } else if (arg.startsWith('--telemetry=')) {
192
+ telemetry = arg.split('=')[1] !== 'false';
193
+ } else if (arg === '--telemetry') {
194
+ telemetry = true;
179
195
  } else if (!arg.startsWith('-')) {
180
196
  // Assume it's an agent name
181
197
  if (AGENT_HOOKS[arg]) {
@@ -193,6 +209,26 @@ export default async function enable(args) {
193
209
 
194
210
  console.log('šŸ”§ Enabling shit-cli in repository...\n');
195
211
 
212
+ // Write configuration
213
+ const configData = {
214
+ enabled: true,
215
+ push_sessions: pushSessions,
216
+ telemetry: telemetry,
217
+ log_level: 'info'
218
+ };
219
+
220
+ const settingsFileName = useLocal ? 'settings.local.json' : 'settings.json';
221
+
222
+ // Write to .claude/ directory (project or local)
223
+ const configDir = join(projectRoot, '.claude');
224
+ if (!existsSync(configDir)) {
225
+ mkdirSync(configDir, { recursive: true });
226
+ }
227
+
228
+ const configFile = join(configDir, settingsFileName);
229
+ writeFileSync(configFile, JSON.stringify(configData, null, 2));
230
+ console.log(`āœ… Wrote configuration: ${configFile}`);
231
+
196
232
  // Setup hooks for each agent
197
233
  for (const agent of agents) {
198
234
  const settingsFile = setupAgentHooks(projectRoot, agent);
package/lib/explain.js ADDED
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Explain a session or checkpoint
5
+ * Provides human-readable explanation of what happened in a session
6
+ */
7
+
8
+ import { existsSync, readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { execSync } from 'child_process';
11
+
12
+ function findProjectRoot() {
13
+ let dir = process.cwd();
14
+ while (dir !== '/') {
15
+ if (existsSync(join(dir, '.git'))) {
16
+ return dir;
17
+ }
18
+ dir = join(dir, '..');
19
+ }
20
+ throw new Error('Not in a git repository');
21
+ }
22
+
23
+ function getSessionData(sessionId, projectRoot) {
24
+ const sessionDir = join(projectRoot, '.shit-logs', sessionId);
25
+
26
+ if (!existsSync(sessionDir)) {
27
+ return null;
28
+ }
29
+
30
+ const files = {};
31
+ const fileNames = ['summary.json', 'summary.txt', 'context.md', 'metadata.json'];
32
+
33
+ for (const file of fileNames) {
34
+ const filePath = join(sessionDir, file);
35
+ if (existsSync(filePath)) {
36
+ files[file] = readFileSync(filePath, 'utf-8');
37
+ }
38
+ }
39
+
40
+ return { dir: sessionDir, files };
41
+ }
42
+
43
+ function explainFromSummary(summaryText) {
44
+ const lines = summaryText.split('\n');
45
+ const explanation = [];
46
+
47
+ // Extract key information
48
+ const typeMatch = summaryText.match(/Type:\s*(\w+)/i);
49
+ const riskMatch = summaryText.match(/Risk:\s*(\w+)/i);
50
+ const intentMatch = summaryText.match(/Goal:\s*(.+)/i);
51
+ const durationMatch = summaryText.match(/Duration:\s*(\d+)min/i);
52
+
53
+ if (typeMatch) {
54
+ explanation.push(`šŸ“‹ Session type: **${typeMatch[1]}**`);
55
+ }
56
+
57
+ if (riskMatch) {
58
+ const risk = riskMatch[1].toLowerCase();
59
+ const emoji = risk === 'high' ? 'šŸ”“' : risk === 'medium' ? '🟔' : '🟢';
60
+ explanation.push(`${emoji} Risk level: **${risk}**`);
61
+ }
62
+
63
+ if (intentMatch) {
64
+ explanation.push(`šŸŽÆ Intent: ${intentMatch[1].slice(0, 100)}`);
65
+ }
66
+
67
+ if (durationMatch) {
68
+ explanation.push(`ā±ļø Duration: ${durationMatch[1]} minutes`);
69
+ }
70
+
71
+ // Extract files changed
72
+ const changesMatch = summaryText.match(/## Changes\n([\s\S]+?)##/);
73
+ if (changesMatch) {
74
+ const fileLines = changesMatch[1].split('\n').filter(l => l.trim().startsWith('['));
75
+ if (fileLines.length > 0) {
76
+ explanation.push(`\nšŸ“ Files changed (${fileLines.length}):`);
77
+ fileLines.slice(0, 5).forEach(line => {
78
+ explanation.push(` ${line.trim()}`);
79
+ });
80
+ if (fileLines.length > 5) {
81
+ explanation.push(` ... and ${fileLines.length - 5} more`);
82
+ }
83
+ }
84
+ }
85
+
86
+ // Extract tools used
87
+ const toolsMatch = summaryText.match(/## Tools\n([\s\S]+?)##/);
88
+ if (toolsMatch) {
89
+ const toolLines = toolsMatch[1].split('\n').filter(l => l.includes(':'));
90
+ if (toolLines.length > 0) {
91
+ explanation.push(`\nšŸ”§ Tools used:`);
92
+ toolLines.slice(0, 5).forEach(line => {
93
+ explanation.push(` ${line.trim()}`);
94
+ });
95
+ }
96
+ }
97
+
98
+ // Extract errors
99
+ if (summaryText.includes('## Errors')) {
100
+ const errorsMatch = summaryText.match(/## Errors\n([\s\S]+)/);
101
+ if (errorsMatch && errorsMatch[1].trim()) {
102
+ explanation.push(`\nāš ļø Errors encountered:`);
103
+ const errorLines = errorsMatch[1].split('\n').filter(l => l.trim());
104
+ errorLines.slice(0, 3).forEach(line => {
105
+ explanation.push(` ${line.trim()}`);
106
+ });
107
+ }
108
+ }
109
+
110
+ return explanation.join('\n');
111
+ }
112
+
113
+ function explainFromCommit(commitSha, projectRoot) {
114
+ try {
115
+ // Try to find associated checkpoint
116
+ const checkpoints = execSync('git branch --list "shit/checkpoints/v1/*"', {
117
+ cwd: projectRoot,
118
+ encoding: 'utf-8'
119
+ }).split('\n').filter(Boolean);
120
+
121
+ for (const branch of checkpoints) {
122
+ try {
123
+ const log = execSync(`git log ${branch} --oneline -1`, {
124
+ cwd: projectRoot,
125
+ encoding: 'utf-8'
126
+ }).trim();
127
+
128
+ if (log.includes(commitSha.slice(0, 12))) {
129
+ const message = execSync(`git log ${branch} --format=%B -1`, {
130
+ cwd: projectRoot,
131
+ encoding: 'utf-8'
132
+ }).trim();
133
+
134
+ return `šŸ“ø This commit has an associated checkpoint:\n\nBranch: ${branch}\n\n${message}`;
135
+ }
136
+ } catch {
137
+ // Skip this branch
138
+ }
139
+ }
140
+
141
+ return `No checkpoint found for commit ${commitSha}`;
142
+ } catch {
143
+ return 'No checkpoint information available';
144
+ }
145
+ }
146
+
147
+ export default async function explain(args) {
148
+ try {
149
+ const projectRoot = findProjectRoot();
150
+ const target = args[0];
151
+
152
+ if (!target) {
153
+ console.log('Usage: shit explain <session-id | commit-sha>');
154
+ console.log('\nExamples:');
155
+ console.log(' shit explain 2026-02-28-abc12345 # Explain a session');
156
+ console.log(' shit explain abc1234 # Explain a commit');
157
+ process.exit(1);
158
+ }
159
+
160
+ console.log(`šŸ” Explaining: ${target}\n`);
161
+
162
+ // Try to find session first
163
+ const sessionData = getSessionData(target, projectRoot);
164
+
165
+ if (sessionData && sessionData.files['summary.txt']) {
166
+ console.log('šŸ“‹ Session Explanation\n');
167
+ console.log(explainFromSummary(sessionData.files['summary.txt']));
168
+ console.log('\n---\nšŸ’” Use "shit view ' + target + '" for full details');
169
+ return;
170
+ }
171
+
172
+ // Try as commit
173
+ try {
174
+ execSync('git rev-parse --verify ' + target + '^{commit}', {
175
+ cwd: projectRoot,
176
+ stdio: 'ignore'
177
+ });
178
+
179
+ console.log('šŸ“ø Commit Explanation\n');
180
+ console.log(explainFromCommit(target, projectRoot));
181
+ return;
182
+ } catch {
183
+ // Not a commit
184
+ }
185
+
186
+ // Try partial match
187
+ const sessions = execSync('ls .shit-logs', { cwd: projectRoot, encoding: 'utf-8' })
188
+ .split('\n')
189
+ .filter(s => s.includes(target));
190
+
191
+ if (sessions.length > 0) {
192
+ const matchedSession = sessions[0];
193
+ const data = getSessionData(matchedSession, projectRoot);
194
+ if (data && data.files['summary.txt']) {
195
+ console.log('šŸ“‹ Found session: ' + matchedSession + '\n');
196
+ console.log(explainFromSummary(data.files['summary.txt']));
197
+ return;
198
+ }
199
+ }
200
+
201
+ console.log(`āŒ Could not find session or commit: ${target}`);
202
+
203
+ } catch (error) {
204
+ console.error('āŒ Failed to explain:', error.message);
205
+ process.exit(1);
206
+ }
207
+ }
package/lib/reset.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Reset - delete checkpoint for current HEAD commit
5
+ * Similar to 'entire reset' - removes shadow branch and session state
6
+ */
7
+
8
+ import { existsSync, readFileSync, rmSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { execSync } from 'child_process';
11
+
12
+ function findProjectRoot() {
13
+ let dir = process.cwd();
14
+ while (dir !== '/') {
15
+ if (existsSync(join(dir, '.git'))) {
16
+ return dir;
17
+ }
18
+ dir = join(dir, '..');
19
+ }
20
+ throw new Error('Not in a git repository');
21
+ }
22
+
23
+ function git(cmd, cwd) {
24
+ return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
25
+ }
26
+
27
+ function findCheckpointForCommit(projectRoot, commitSha) {
28
+ try {
29
+ const branches = git('branch --list "shit/checkpoints/v1/*"', projectRoot)
30
+ .split('\n')
31
+ .map(b => b.trim().replace(/^\*?\s*/, ''))
32
+ .filter(Boolean);
33
+
34
+ for (const branch of branches) {
35
+ try {
36
+ const log = git(`log ${branch} --oneline -1`, projectRoot);
37
+ if (log.includes(commitSha.slice(0, 12))) {
38
+ return branch;
39
+ }
40
+ } catch {
41
+ // Skip
42
+ }
43
+ }
44
+ return null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function findActiveSession(projectRoot) {
51
+ const shitLogsDir = join(projectRoot, '.shit-logs');
52
+ if (!existsSync(shitLogsDir)) {
53
+ return null;
54
+ }
55
+
56
+ const { readdirSync, statSync } = require('fs');
57
+ const sessions = readdirSync(shitLogsDir)
58
+ .filter(name => name.match(/^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/))
59
+ .map(name => ({
60
+ name,
61
+ path: join(shitLogsDir, name),
62
+ mtime: statSync(join(shitLogsDir, name)).mtime
63
+ }))
64
+ .sort((a, b) => b.mtime - a.mtime);
65
+
66
+ return sessions.length > 0 ? sessions[0] : null;
67
+ }
68
+
69
+ export default async function reset(args) {
70
+ try {
71
+ const projectRoot = findProjectRoot();
72
+ const force = args.includes('--force') || args.includes('-f');
73
+
74
+ // Get current commit
75
+ const commitSha = git('rev-parse HEAD', projectRoot);
76
+ const commitShort = commitSha.slice(0, 12);
77
+
78
+ console.log(`šŸ”„ Resetting checkpoint for commit: ${commitShort}\n`);
79
+
80
+ // Find checkpoint for this commit
81
+ const branch = findCheckpointForCommit(projectRoot, commitSha);
82
+
83
+ if (!branch) {
84
+ console.log('ā„¹ļø No checkpoint found for current commit.');
85
+ console.log(' Checkpoints are created on git commit, not automatically on session start.');
86
+ process.exit(0);
87
+ }
88
+
89
+ console.log(`šŸ“ø Found checkpoint branch: ${branch}`);
90
+
91
+ if (!force) {
92
+ console.log('\nāš ļø This will delete the checkpoint and its branch.');
93
+ console.log(' Use --force to proceed without confirmation.');
94
+ process.exit(1);
95
+ }
96
+
97
+ // Delete the checkpoint branch
98
+ try {
99
+ git(`branch -D ${branch}`, projectRoot);
100
+ console.log(`āœ… Deleted checkpoint branch: ${branch}`);
101
+ } catch {
102
+ console.log('ā„¹ļø Branch already deleted or not found');
103
+ }
104
+
105
+ // Optionally clean active session if it's linked to this commit
106
+ const activeSession = findActiveSession(projectRoot);
107
+ if (activeSession) {
108
+ const stateFile = join(activeSession.path, 'state.json');
109
+ if (existsSync(stateFile)) {
110
+ try {
111
+ const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
112
+ if (state.checkpoints && state.checkpoints.some(cp => cp.linked_commit === commitSha)) {
113
+ // Remove checkpoint references
114
+ state.checkpoints = state.checkpoints.filter(cp => cp.linked_commit !== commitSha);
115
+ require('fs').writeFileSync(stateFile, JSON.stringify(state, null, 2));
116
+ console.log('āœ… Updated session state');
117
+ }
118
+ } catch {
119
+ // Best effort
120
+ }
121
+ }
122
+ }
123
+
124
+ console.log('\nāœ… Reset complete!');
125
+
126
+ } catch (error) {
127
+ console.error('āŒ Failed to reset:', error.message);
128
+ process.exit(1);
129
+ }
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudemini/shit-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Session-based Hook Intelligence Tracker - CLI tool for logging Claude Code hooks",
5
5
  "type": "module",
6
6
  "bin": {