@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 +18 -16
- package/lib/enable.js +36 -0
- package/lib/explain.js +207 -0
- package/lib/reset.js +130 -0
- package/package.json +1 -1
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:
|
|
8
|
-
disable:
|
|
9
|
-
status:
|
|
10
|
-
init:
|
|
11
|
-
log:
|
|
12
|
-
commit:
|
|
13
|
-
list:
|
|
14
|
-
checkpoints:'List all checkpoints',
|
|
15
|
-
view:
|
|
16
|
-
query:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|