@claudemini/shit-cli 1.4.0 → 1.5.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/README.md +126 -117
- package/lib/checkpoint.js +39 -32
- package/lib/checkpoints.js +3 -15
- package/lib/commit.js +7 -15
- package/lib/config.js +3 -1
- package/lib/disable.js +54 -18
- package/lib/doctor.js +17 -24
- package/lib/enable.js +24 -13
- package/lib/explain.js +43 -38
- package/lib/reset.js +8 -16
- package/lib/resume.js +32 -27
- package/lib/rewind.js +63 -38
- package/lib/session.js +6 -0
- package/lib/status.js +44 -19
- package/lib/summarize.js +2 -13
- package/package.json +21 -4
- package/.claude/settings.json +0 -81
- package/.claude/settings.local.json +0 -20
- package/COMPARISON.md +0 -92
- package/DESIGN_PHILOSOPHY.md +0 -138
- package/QUICKSTART.md +0 -109
package/README.md
CHANGED
|
@@ -2,69 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
**S**ession-based **H**ook **I**ntelligence **T**racker
|
|
4
4
|
|
|
5
|
-
A memory system for human-AI
|
|
5
|
+
A zero-dependency memory system for human-AI coding sessions. Tracks what happened, classifies intent and risk, and provides structured data for code review automation.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Supports **Claude Code**, **Gemini CLI**, **Cursor**, and **OpenCode**.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. **Code Review Bot Data Support** - Structured semantic data for intelligent code review
|
|
11
|
-
|
|
12
|
-
See [DESIGN_PHILOSOPHY.md](./DESIGN_PHILOSOPHY.md) for detailed design rationale.
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
9
|
+
## Quick Start
|
|
15
10
|
|
|
16
11
|
```bash
|
|
17
|
-
|
|
18
|
-
npm link
|
|
19
|
-
```
|
|
12
|
+
npm install -g @claudemini/shit-cli
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
cd /path/to/your/project
|
|
15
|
+
shit enable # Setup hooks + .shit-logs
|
|
16
|
+
# ... use Claude Code normally ...
|
|
17
|
+
shit list # See sessions
|
|
18
|
+
shit status # Check current session
|
|
24
19
|
```
|
|
25
20
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
### Initialize hooks
|
|
21
|
+
## Installation
|
|
29
22
|
|
|
30
23
|
```bash
|
|
31
|
-
|
|
32
|
-
shit init
|
|
24
|
+
npm install -g @claudemini/shit-cli
|
|
33
25
|
```
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
### List sessions
|
|
27
|
+
Or use directly without installing:
|
|
38
28
|
|
|
39
29
|
```bash
|
|
40
|
-
shit
|
|
30
|
+
npx @claudemini/shit-cli <command>
|
|
41
31
|
```
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
3 session(s):
|
|
33
|
+
## Commands
|
|
46
34
|
|
|
47
|
-
|
|
48
|
-
Fix auth timeout by adjusting retry logic
|
|
49
|
-
45min | 42 events | 28 tools | 3 files | 0 errors
|
|
50
|
-
2/27/2026, 3:15:00 PM
|
|
35
|
+
### Setup
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
```bash
|
|
38
|
+
shit enable # Enable for Claude Code (default)
|
|
39
|
+
shit enable gemini-cli # Enable for Gemini CLI
|
|
40
|
+
shit enable --all # Enable for all supported agents
|
|
41
|
+
shit enable --checkpoint # Also create checkpoints on git commit
|
|
42
|
+
shit disable # Remove hooks (keep data)
|
|
43
|
+
shit disable --clean # Remove hooks and all data
|
|
44
|
+
shit init # Low-level: register hooks in .claude/settings.json
|
|
56
45
|
```
|
|
57
46
|
|
|
58
|
-
###
|
|
47
|
+
### Session Tracking
|
|
59
48
|
|
|
60
49
|
```bash
|
|
61
|
-
shit
|
|
62
|
-
shit
|
|
50
|
+
shit status # Show current session + git info
|
|
51
|
+
shit list # List all sessions with type, risk, intent
|
|
52
|
+
shit view <session-id> # View semantic session report
|
|
53
|
+
shit view <session-id> --json # Include raw JSON data
|
|
54
|
+
shit explain <session-id> # Human-friendly explanation of a session
|
|
55
|
+
shit explain <commit-sha> # Explain a commit via its checkpoint
|
|
63
56
|
```
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
### Query session memory
|
|
58
|
+
### Cross-Session Queries
|
|
68
59
|
|
|
69
60
|
```bash
|
|
70
61
|
shit query --recent=5 # Recent 5 sessions
|
|
@@ -74,70 +65,92 @@ shit query --risk=high # High-risk sessions
|
|
|
74
65
|
shit query --type=feature --json # JSON output for bot consumption
|
|
75
66
|
```
|
|
76
67
|
|
|
77
|
-
###
|
|
68
|
+
### Checkpoints & Recovery
|
|
78
69
|
|
|
79
70
|
```bash
|
|
80
|
-
shit
|
|
81
|
-
shit
|
|
71
|
+
shit checkpoints # List all checkpoints
|
|
72
|
+
shit commit # Manually create checkpoint for current HEAD
|
|
73
|
+
shit rewind <checkpoint> # Rollback to a checkpoint (git reset --hard)
|
|
74
|
+
shit rewind --interactive # Choose from available checkpoints
|
|
75
|
+
shit resume <checkpoint> # Restore session data from a checkpoint
|
|
76
|
+
shit reset --force # Delete checkpoint for current HEAD
|
|
82
77
|
```
|
|
83
78
|
|
|
84
|
-
###
|
|
79
|
+
### Maintenance
|
|
85
80
|
|
|
86
81
|
```bash
|
|
87
|
-
shit
|
|
88
|
-
shit
|
|
82
|
+
shit doctor # Diagnose issues (corrupted state, stuck sessions)
|
|
83
|
+
shit doctor --fix # Auto-fix detected issues
|
|
84
|
+
shit shadow # List shadow branches
|
|
85
|
+
shit shadow info <branch> # Show branch details
|
|
86
|
+
shit clean --days=7 --dry-run # Preview cleanup
|
|
87
|
+
shit clean --days=7 # Delete sessions older than 7 days
|
|
88
|
+
shit summarize <session-id> # Generate AI summary (requires API key)
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
##
|
|
91
|
+
## Command Reference
|
|
92
92
|
|
|
93
93
|
| Command | Description |
|
|
94
94
|
|---------|-------------|
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
95
|
+
| `enable` | Enable shit-cli in repository (multi-agent support) |
|
|
96
|
+
| `disable` | Remove hooks, optionally clean data |
|
|
97
|
+
| `status` | Show current session and git info |
|
|
98
|
+
| `init` | Register hooks in .claude/settings.json |
|
|
99
|
+
| `log <type>` | Log a hook event from stdin (called by hooks) |
|
|
97
100
|
| `list` | List all sessions with type, intent, risk |
|
|
98
|
-
| `view <
|
|
99
|
-
| `query
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
| `view <id>` | View semantic session report |
|
|
102
|
+
| `query` | Query session memory across sessions |
|
|
103
|
+
| `explain <id>` | Human-friendly explanation of a session or commit |
|
|
104
|
+
| `commit` | Create checkpoint on git commit |
|
|
105
|
+
| `checkpoints` | List all checkpoints |
|
|
106
|
+
| `rewind <cp>` | Rollback to a checkpoint |
|
|
107
|
+
| `resume <cp>` | Resume session from a checkpoint |
|
|
108
|
+
| `reset` | Delete checkpoint for current HEAD |
|
|
109
|
+
| `summarize <id>` | Generate AI summary for a session |
|
|
110
|
+
| `doctor` | Diagnose and fix issues |
|
|
111
|
+
| `shadow` | List/inspect shadow branches |
|
|
112
|
+
| `clean` | Clean old sessions |
|
|
113
|
+
|
|
114
|
+
## How It Works
|
|
105
115
|
|
|
106
116
|
```
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
│ ├── query.js # shit query (cross-session memory queries)
|
|
119
|
-
│ ├── clean.js # shit clean (session cleanup)
|
|
120
|
-
│ ├── shadow.js # shit shadow (branch listing)
|
|
121
|
-
│ └── git-shadow.js # Git plumbing for shadow branches
|
|
117
|
+
Human <-> AI Agent (Claude Code, Gemini CLI, ...)
|
|
118
|
+
| (hooks)
|
|
119
|
+
Event Ingestion (log.js)
|
|
120
|
+
|
|
|
121
|
+
Semantic Extraction (extract.js)
|
|
122
|
+
|
|
|
123
|
+
Session State (session.js) + Reports (report.js)
|
|
124
|
+
|
|
|
125
|
+
Memory System (.shit-logs/ + index.json)
|
|
126
|
+
|
|
|
127
|
+
Code Review Bot / Human Queries
|
|
122
128
|
```
|
|
123
129
|
|
|
130
|
+
1. **Ingestion** - Hooks fire on every agent event (tool use, prompts, session start/end). Events are appended to `events.jsonl`.
|
|
131
|
+
2. **Extraction** - Each event updates incremental state. Intent, change categories, and risk are computed using rule-based pattern matching (zero latency, zero cost, fully offline).
|
|
132
|
+
3. **Reports** - `summary.json` (bot-readable), `summary.txt` (human-readable), `context.md`, `metadata.json`, and `prompts.txt` are regenerated on every event.
|
|
133
|
+
4. **Checkpoints** - On session end or git commit, session data is committed to an orphan git branch using plumbing commands (no working tree impact).
|
|
134
|
+
|
|
124
135
|
## Data Model
|
|
125
136
|
|
|
126
137
|
### Session Directory
|
|
127
138
|
|
|
128
139
|
```
|
|
129
140
|
.shit-logs/
|
|
130
|
-
├── index.json # Cross-session index
|
|
141
|
+
├── index.json # Cross-session index
|
|
131
142
|
└── <session-id>/
|
|
132
143
|
├── events.jsonl # Raw hook events
|
|
133
144
|
├── state.json # Incremental processing state
|
|
134
|
-
├── summary.json # Bot data interface (v2
|
|
135
|
-
├── summary.txt # Human-readable
|
|
145
|
+
├── summary.json # Bot data interface (v2)
|
|
146
|
+
├── summary.txt # Human-readable report
|
|
147
|
+
├── context.md # Session context (Entire-style)
|
|
136
148
|
├── prompts.txt # User prompts with timestamps
|
|
137
|
-
|
|
149
|
+
├── metadata.json # Lightweight session metadata
|
|
150
|
+
└── ai-summary.md # AI-generated summary (optional)
|
|
138
151
|
```
|
|
139
152
|
|
|
140
|
-
### summary.json v2
|
|
153
|
+
### summary.json v2
|
|
141
154
|
|
|
142
155
|
```json
|
|
143
156
|
{
|
|
@@ -153,56 +166,27 @@ shit-cli/
|
|
|
153
166
|
"summary": "Fixed: Fix authentication timeout issue"
|
|
154
167
|
},
|
|
155
168
|
"changes": {
|
|
156
|
-
"files": [{
|
|
157
|
-
"path": "src/auth/auth.service.ts",
|
|
158
|
-
"category": "source",
|
|
159
|
-
"operations": ["edit"],
|
|
160
|
-
"editCount": 2,
|
|
161
|
-
"editSummary": "Modified timeout logic"
|
|
162
|
-
}],
|
|
169
|
+
"files": [{ "path": "src/auth.ts", "category": "source", "operations": ["edit"] }],
|
|
163
170
|
"summary": { "source": 3, "test": 1 }
|
|
164
171
|
},
|
|
165
172
|
"activity": {
|
|
166
173
|
"tools": { "Read": 15, "Edit": 3, "Bash": 5 },
|
|
167
|
-
"commands": {
|
|
168
|
-
"test": ["npm run test"],
|
|
169
|
-
"git": ["git status"]
|
|
170
|
-
},
|
|
174
|
+
"commands": { "test": ["npm run test"], "git": ["git status"] },
|
|
171
175
|
"errors": []
|
|
172
176
|
},
|
|
173
177
|
"review_hints": {
|
|
174
178
|
"tests_run": true,
|
|
175
179
|
"build_verified": false,
|
|
176
|
-
"files_without_tests": ["src/auth
|
|
180
|
+
"files_without_tests": ["src/auth.ts"],
|
|
177
181
|
"large_change": false,
|
|
178
182
|
"config_changed": false,
|
|
179
183
|
"migration_added": false
|
|
180
184
|
},
|
|
181
|
-
"prompts": ["Fix the auth timeout bug"
|
|
185
|
+
"prompts": [{ "time": "...", "text": "Fix the auth timeout bug" }],
|
|
182
186
|
"scope": ["auth"]
|
|
183
187
|
}
|
|
184
188
|
```
|
|
185
189
|
|
|
186
|
-
### Cross-Session Index
|
|
187
|
-
|
|
188
|
-
```json
|
|
189
|
-
{
|
|
190
|
-
"project": "my-project",
|
|
191
|
-
"sessions": [{
|
|
192
|
-
"id": "f608c31e...",
|
|
193
|
-
"date": "2026-02-27",
|
|
194
|
-
"type": "bugfix",
|
|
195
|
-
"intent": "Fix auth timeout",
|
|
196
|
-
"files": ["src/auth/auth.service.ts"],
|
|
197
|
-
"duration": 45,
|
|
198
|
-
"risk": "medium"
|
|
199
|
-
}],
|
|
200
|
-
"file_history": {
|
|
201
|
-
"src/auth/auth.service.ts": ["f608c31e...", "a1b2c3d4..."]
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
190
|
## Session Types
|
|
207
191
|
|
|
208
192
|
| Type | Description |
|
|
@@ -219,19 +203,20 @@ shit-cli/
|
|
|
219
203
|
| `style` | Formatting, UI |
|
|
220
204
|
| `security` | Security-related |
|
|
221
205
|
| `perf` | Performance optimization |
|
|
222
|
-
| `unknown` | Unclassified |
|
|
223
206
|
|
|
224
207
|
## Risk Levels
|
|
225
208
|
|
|
226
|
-
- **low
|
|
227
|
-
- **medium
|
|
228
|
-
- **high
|
|
209
|
+
- **low** - Few files, no config/migration changes, tests run
|
|
210
|
+
- **medium** - Multiple files, some config changes
|
|
211
|
+
- **high** - Many files (>10), migration changes, infra changes without tests
|
|
229
212
|
|
|
230
213
|
## Bot Integration
|
|
231
214
|
|
|
232
215
|
```javascript
|
|
216
|
+
import { readFileSync } from 'fs';
|
|
217
|
+
|
|
233
218
|
// Read session data
|
|
234
|
-
const summary = JSON.parse(
|
|
219
|
+
const summary = JSON.parse(readFileSync('.shit-logs/<id>/summary.json', 'utf-8'));
|
|
235
220
|
|
|
236
221
|
// Check review hints
|
|
237
222
|
if (!summary.review_hints.tests_run && summary.changes.files.length > 0) {
|
|
@@ -242,16 +227,40 @@ if (summary.review_hints.migration_added) {
|
|
|
242
227
|
}
|
|
243
228
|
|
|
244
229
|
// Query file history via index
|
|
245
|
-
const index = JSON.parse(
|
|
230
|
+
const index = JSON.parse(readFileSync('.shit-logs/index.json', 'utf-8'));
|
|
246
231
|
const history = index.file_history['src/auth/auth.service.ts'];
|
|
247
|
-
if (history.length > 3) {
|
|
232
|
+
if (history && history.length > 3) {
|
|
248
233
|
review.note('This file has been modified frequently');
|
|
249
234
|
}
|
|
250
235
|
```
|
|
251
236
|
|
|
237
|
+
## AI Summary
|
|
238
|
+
|
|
239
|
+
Set one of these environment variables to enable AI-powered session summaries:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
export OPENAI_API_KEY=sk-... # Uses gpt-4o-mini by default
|
|
243
|
+
export ANTHROPIC_API_KEY=sk-... # Uses claude-3-haiku by default
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Then run:
|
|
247
|
+
```bash
|
|
248
|
+
shit summarize <session-id>
|
|
249
|
+
```
|
|
250
|
+
|
|
252
251
|
## Environment Variables
|
|
253
252
|
|
|
254
|
-
|
|
253
|
+
| Variable | Description |
|
|
254
|
+
|----------|-------------|
|
|
255
|
+
| `SHIT_LOG_DIR` | Custom log directory (default: `.shit-logs` in project root) |
|
|
256
|
+
| `OPENAI_API_KEY` | Enable AI summaries via OpenAI |
|
|
257
|
+
| `ANTHROPIC_API_KEY` | Enable AI summaries via Anthropic |
|
|
258
|
+
|
|
259
|
+
## Security
|
|
260
|
+
|
|
261
|
+
- Session logs are stored locally in `.shit-logs/` (added to `.gitignore` automatically)
|
|
262
|
+
- Secrets (API keys, tokens, passwords) are automatically redacted when writing to shadow branches
|
|
263
|
+
- Checkpoint data uses git plumbing commands — no impact on your working tree
|
|
255
264
|
|
|
256
265
|
## License
|
|
257
266
|
|
package/lib/checkpoint.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Supports multiple agents (Claude Code, Gemini CLI, Cursor, etc.)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
12
12
|
import { execSync } from 'child_process';
|
|
13
13
|
import { join, dirname } from 'path';
|
|
14
14
|
import { redactSecrets } from './redact.js';
|
|
@@ -149,9 +149,10 @@ export async function commitCheckpoint(projectRoot, sessionDir, sessionId, commi
|
|
|
149
149
|
return { success: false, reason: 'not a git repo' };
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
// Branch naming: shit/checkpoints/v1/YYYY-MM-DD-<short
|
|
153
|
-
const datePart =
|
|
154
|
-
const
|
|
152
|
+
// Branch naming: shit/checkpoints/v1/YYYY-MM-DD-<session-short>
|
|
153
|
+
const datePart = new Date().toISOString().slice(0, 10);
|
|
154
|
+
const sessionCompact = sessionId.toLowerCase().replace(/[^a-f0-9]/g, '');
|
|
155
|
+
const uuidPart = (sessionCompact.slice(-8) || sessionCompact.slice(0, 8) || 'unknown00').padEnd(8, '0');
|
|
155
156
|
const branchName = `shit/checkpoints/v1/${datePart}-${uuidPart}`;
|
|
156
157
|
const refPath = `refs/heads/${branchName}`;
|
|
157
158
|
|
|
@@ -208,13 +209,6 @@ export async function commitCheckpoint(projectRoot, sessionDir, sessionId, commi
|
|
|
208
209
|
// best effort
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
return {
|
|
212
|
-
success: true,
|
|
213
|
-
branch: branchName,
|
|
214
|
-
commit: commitHash,
|
|
215
|
-
linked_commit: linkedCommit,
|
|
216
|
-
};
|
|
217
|
-
|
|
218
212
|
// Auto-summarize if enabled
|
|
219
213
|
if (autoSummarize) {
|
|
220
214
|
try {
|
|
@@ -227,6 +221,13 @@ export async function commitCheckpoint(projectRoot, sessionDir, sessionId, commi
|
|
|
227
221
|
// Best effort - summarize is optional
|
|
228
222
|
}
|
|
229
223
|
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
success: true,
|
|
227
|
+
branch: branchName,
|
|
228
|
+
commit: commitHash,
|
|
229
|
+
linked_commit: linkedCommit,
|
|
230
|
+
};
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
/**
|
|
@@ -241,29 +242,35 @@ export function listCheckpoints(projectRoot) {
|
|
|
241
242
|
|
|
242
243
|
for (const branch of branches) {
|
|
243
244
|
try {
|
|
244
|
-
// Extract session info from branch name
|
|
245
|
+
// Extract session info from branch name (supports current and legacy formats)
|
|
245
246
|
const match = branch.match(/^shit\/checkpoints\/v1\/(\d{4}-\d{2}-\d{2})-([a-f0-9]+)$/);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Get commit info
|
|
250
|
-
const log = git(`log ${branch} --oneline -1`, projectRoot);
|
|
251
|
-
const commitMatch = log.match(/^([a-f0-9]+)\s+checkpoint/);
|
|
252
|
-
const commit = commitMatch ? commitMatch[1] : log.split(' ')[0];
|
|
253
|
-
|
|
254
|
-
// Get linked commit from message
|
|
255
|
-
const fullLog = git(`log ${branch} --format=%B -1`, projectRoot);
|
|
256
|
-
const linkedMatch = fullLog.match(/@ ([a-f0-9]+)/);
|
|
257
|
-
const linkedCommit = linkedMatch ? linkedMatch[1] : null;
|
|
258
|
-
|
|
259
|
-
checkpoints.push({
|
|
260
|
-
branch,
|
|
261
|
-
commit: commit.slice(0, 12),
|
|
262
|
-
linked_commit: linkedCommit,
|
|
263
|
-
date,
|
|
264
|
-
uuid: uuidShort,
|
|
265
|
-
});
|
|
247
|
+
const legacyMatch = branch.match(/^shit\/checkpoints\/v1\/([a-f0-9-]+)$/);
|
|
248
|
+
if (!match && !legacyMatch) {
|
|
249
|
+
continue;
|
|
266
250
|
}
|
|
251
|
+
|
|
252
|
+
const date = match ? match[1] : git(`log ${branch} --format=%cs -1`, projectRoot);
|
|
253
|
+
const uuidShort = match
|
|
254
|
+
? match[2]
|
|
255
|
+
: ((legacyMatch[1].replace(/[^a-f0-9]/g, '').slice(-8) || legacyMatch[1].slice(0, 8)).toLowerCase());
|
|
256
|
+
|
|
257
|
+
// Get commit info
|
|
258
|
+
const log = git(`log ${branch} --oneline -1`, projectRoot);
|
|
259
|
+
const commitMatch = log.match(/^([a-f0-9]+)\s+checkpoint/);
|
|
260
|
+
const commit = commitMatch ? commitMatch[1] : log.split(' ')[0];
|
|
261
|
+
|
|
262
|
+
// Get linked commit from message
|
|
263
|
+
const fullLog = git(`log ${branch} --format=%B -1`, projectRoot);
|
|
264
|
+
const linkedMatch = fullLog.match(/@ ([a-f0-9]+)/);
|
|
265
|
+
const linkedCommit = linkedMatch ? linkedMatch[1] : null;
|
|
266
|
+
|
|
267
|
+
checkpoints.push({
|
|
268
|
+
branch,
|
|
269
|
+
commit: commit.slice(0, 12),
|
|
270
|
+
linked_commit: linkedCommit,
|
|
271
|
+
date,
|
|
272
|
+
uuid: uuidShort,
|
|
273
|
+
});
|
|
267
274
|
} catch {
|
|
268
275
|
// Skip invalid branches
|
|
269
276
|
}
|
package/lib/checkpoints.js
CHANGED
|
@@ -5,24 +5,12 @@
|
|
|
5
5
|
* Inspired by Entire's checkpoint system
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { listCheckpoints, getCheckpoint } from './checkpoint.js';
|
|
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
|
-
}
|
|
8
|
+
import { listCheckpoints } from './checkpoint.js';
|
|
9
|
+
import { getProjectRoot } from './config.js';
|
|
22
10
|
|
|
23
11
|
export default async function checkpoints(args) {
|
|
24
12
|
try {
|
|
25
|
-
const projectRoot =
|
|
13
|
+
const projectRoot = getProjectRoot();
|
|
26
14
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
27
15
|
const json = args.includes('--json');
|
|
28
16
|
|
package/lib/commit.js
CHANGED
|
@@ -5,25 +5,15 @@
|
|
|
5
5
|
* Similar to Entire's approach: checkpoint created on git commit
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync,
|
|
8
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { commitCheckpoint } from './checkpoint.js';
|
|
12
|
-
|
|
13
|
-
function findProjectRoot() {
|
|
14
|
-
let dir = process.cwd();
|
|
15
|
-
while (dir !== '/') {
|
|
16
|
-
if (existsSync(join(dir, '.git'))) {
|
|
17
|
-
return dir;
|
|
18
|
-
}
|
|
19
|
-
dir = join(dir, '..');
|
|
20
|
-
}
|
|
21
|
-
throw new Error('Not in a git repository');
|
|
22
|
-
}
|
|
12
|
+
import { getProjectRoot, SESSION_ID_REGEX } from './config.js';
|
|
23
13
|
|
|
24
14
|
export default async function commitHook(args) {
|
|
25
15
|
try {
|
|
26
|
-
const projectRoot =
|
|
16
|
+
const projectRoot = getProjectRoot();
|
|
27
17
|
|
|
28
18
|
// Get the commit that was just created
|
|
29
19
|
const commitSha = args[0] || execSync('git rev-parse HEAD', { cwd: projectRoot, encoding: 'utf-8' }).trim();
|
|
@@ -39,9 +29,11 @@ export default async function commitHook(args) {
|
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
// Find the most recent session
|
|
42
|
-
const { readdirSync, statSync } = await import('fs');
|
|
43
32
|
const sessions = readdirSync(shitLogsDir)
|
|
44
|
-
.filter(name =>
|
|
33
|
+
.filter(name => {
|
|
34
|
+
const fullPath = join(shitLogsDir, name);
|
|
35
|
+
return SESSION_ID_REGEX.test(name) && statSync(fullPath).isDirectory();
|
|
36
|
+
})
|
|
45
37
|
.map(name => ({
|
|
46
38
|
name,
|
|
47
39
|
path: join(shitLogsDir, name),
|
package/lib/config.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { join, relative } from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
|
|
6
|
-
export const
|
|
6
|
+
export const UUID_SESSION_ID_REGEX = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
7
|
+
export const LEGACY_SESSION_ID_REGEX = /^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/;
|
|
8
|
+
export const SESSION_ID_REGEX = /^(?:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|\d{4}-\d{2}-\d{2}-[a-f0-9-]+)$/;
|
|
7
9
|
|
|
8
10
|
export function getProjectRoot() {
|
|
9
11
|
try {
|
package/lib/disable.js
CHANGED
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
import { getProjectRoot } from './config.js';
|
|
11
|
+
|
|
12
|
+
const CLAUDE_HOOK_TYPES = [
|
|
13
|
+
'SessionStart',
|
|
14
|
+
'SessionEnd',
|
|
15
|
+
'UserPromptSubmit',
|
|
16
|
+
'PreToolUse',
|
|
17
|
+
'PostToolUse',
|
|
18
|
+
'Stop',
|
|
19
|
+
'Notification',
|
|
20
|
+
];
|
|
21
21
|
|
|
22
22
|
function removeClaudeHooks(projectRoot) {
|
|
23
23
|
const settingsFile = join(projectRoot, '.claude', 'settings.json');
|
|
@@ -28,13 +28,49 @@ function removeClaudeHooks(projectRoot) {
|
|
|
28
28
|
|
|
29
29
|
try {
|
|
30
30
|
const settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));
|
|
31
|
+
let removed = false;
|
|
31
32
|
|
|
32
33
|
if (settings.hooks) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
for (const hookType of CLAUDE_HOOK_TYPES) {
|
|
35
|
+
const rawEntries = settings.hooks[hookType];
|
|
36
|
+
if (Array.isArray(rawEntries)) {
|
|
37
|
+
const nextEntries = rawEntries
|
|
38
|
+
.map(entry => {
|
|
39
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
const nextHooks = entry.hooks.filter(hook =>
|
|
43
|
+
!(typeof hook?.command === 'string' && hook.command.includes('shit log'))
|
|
44
|
+
);
|
|
45
|
+
if (nextHooks.length !== entry.hooks.length) {
|
|
46
|
+
removed = true;
|
|
47
|
+
}
|
|
48
|
+
if (nextHooks.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return { ...entry, hooks: nextHooks };
|
|
52
|
+
})
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
|
|
55
|
+
if (nextEntries.length > 0) {
|
|
56
|
+
settings.hooks[hookType] = nextEntries;
|
|
57
|
+
} else {
|
|
58
|
+
delete settings.hooks[hookType];
|
|
59
|
+
}
|
|
60
|
+
} else if (typeof rawEntries === 'string' && rawEntries.includes('shit log')) {
|
|
61
|
+
delete settings.hooks[hookType];
|
|
62
|
+
removed = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Cleanup legacy wrong names written by old versions.
|
|
67
|
+
const legacyKeys = ['session_start', 'session_end', 'tool_use', 'edit_applied'];
|
|
68
|
+
for (const key of legacyKeys) {
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(settings.hooks, key)) {
|
|
70
|
+
delete settings.hooks[key];
|
|
71
|
+
removed = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
38
74
|
|
|
39
75
|
// If hooks object is empty, remove it
|
|
40
76
|
if (Object.keys(settings.hooks).length === 0) {
|
|
@@ -43,7 +79,7 @@ function removeClaudeHooks(projectRoot) {
|
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
46
|
-
return
|
|
82
|
+
return removed;
|
|
47
83
|
} catch {
|
|
48
84
|
return false;
|
|
49
85
|
}
|
|
@@ -74,7 +110,7 @@ function removeFromGitignore(projectRoot) {
|
|
|
74
110
|
|
|
75
111
|
export default async function disable(args) {
|
|
76
112
|
try {
|
|
77
|
-
const projectRoot =
|
|
113
|
+
const projectRoot = getProjectRoot();
|
|
78
114
|
const cleanData = args.includes('--clean') || args.includes('--purge');
|
|
79
115
|
|
|
80
116
|
console.log('🔧 Disabling shit-cli in repository...');
|