@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 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 interactions, designed to provide reliable data support for code review automation.
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
- ## Design Vision
7
+ Supports **Claude Code**, **Gemini CLI**, **Cursor**, and **OpenCode**.
8
8
 
9
- 1. **Human-AI Interaction Memory System** - Long-term memory, not temporary logs
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
- cd shit-cli
18
- npm link
19
- ```
12
+ npm install -g @claudemini/shit-cli
20
13
 
21
- Or use directly:
22
- ```bash
23
- node bin/shit.js <command>
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
- ## Usage
27
-
28
- ### Initialize hooks
21
+ ## Installation
29
22
 
30
23
  ```bash
31
- cd /path/to/your/project
32
- shit init
24
+ npm install -g @claudemini/shit-cli
33
25
  ```
34
26
 
35
- Registers all hooks in `.claude/settings.json` automatically.
36
-
37
- ### List sessions
27
+ Or use directly without installing:
38
28
 
39
29
  ```bash
40
- shit list
30
+ npx @claudemini/shit-cli <command>
41
31
  ```
42
32
 
43
- Output:
44
- ```
45
- 3 session(s):
33
+ ## Commands
46
34
 
47
- 1. f608c31e [bugfix] risk:medium
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
- 2. a1b2c3d4 [feature] risk:low
53
- Add user profile endpoint
54
- 30min | 28 events | 15 tools | 2 files | 0 errors
55
- 2/26/2026, 10:30:00 AM
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
- ### View session details
47
+ ### Session Tracking
59
48
 
60
49
  ```bash
61
- shit view f608c31e-453c-435a-b0e2-3116dc56ad71
62
- shit view f608c31e-453c-435a-b0e2-3116dc56ad71 --json # Include raw JSON
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
- Output includes: intent, changes by category, tools, commands, review hints (tests run, build verified, config changed, etc.).
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
- ### Shadow branches
68
+ ### Checkpoints & Recovery
78
69
 
79
70
  ```bash
80
- shit shadow # List shadow branches
81
- shit shadow info <branch> # Show branch details
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
- ### Clean old sessions
79
+ ### Maintenance
85
80
 
86
81
  ```bash
87
- shit clean --days=7 --dry-run # Preview
88
- shit clean --days=7 # Delete sessions older than 7 days
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
- ## Commands
91
+ ## Command Reference
92
92
 
93
93
  | Command | Description |
94
94
  |---------|-------------|
95
- | `init` | Initialize hooks in .claude/settings.json |
96
- | `log <hook-type>` | Log a hook event from stdin (called by hooks) |
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 <session-id> [--json]` | View semantic session report |
99
- | `query [options]` | Query session memory across sessions |
100
- | `shadow [info <branch>]` | List or inspect shadow branches |
101
- | `clean [--days=N] [--dry-run]` | Clean old sessions |
102
- | `help` | Show help |
103
-
104
- ## Architecture
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
- shit-cli/
108
- ├── bin/shit.js # CLI entry point
109
- ├── lib/
110
- │ ├── config.js # Shared config: getProjectRoot(), getLogDir(), toRelative()
111
- │ ├── extract.js # Semantic extraction: intent, changes, classification
112
- │ ├── report.js # Report generation: summary.json v2, summary.txt, metadata
113
- │ ├── session.js # Session state management + cross-session index
114
- │ ├── log.js # Event ingestion dispatcher (stdin → parse → extract → save)
115
- │ ├── init.js # shit init (hook registration)
116
- │ ├── list.js # shit list (semantic session listing)
117
- │ ├── view.js # shit view (semantic report display)
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 (file history, types)
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 schema)
135
- ├── summary.txt # Human-readable semantic report
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
- └── metadata.json # Lightweight session metadata
149
+ ├── metadata.json # Lightweight session metadata
150
+ └── ai-summary.md # AI-generated summary (optional)
138
151
  ```
139
152
 
140
- ### summary.json v2 Schema
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/auth.service.ts"],
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", "Run the tests"],
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**: Few files, no config/migration changes, tests run
227
- - **medium**: Multiple files, some config changes
228
- - **high**: Many files (>10), migration changes, infra changes without tests
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(fs.readFileSync('.shit-logs/<id>/summary.json'));
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(fs.readFileSync('.shit-logs/index.json'));
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
- - `SHIT_LOG_DIR`: Custom log directory (default: `./.shit-logs` in project root)
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-uuid>
153
- const datePart = sessionId.split('-').slice(0, 3).join('-');
154
- const uuidPart = sessionId.split('-').slice(3).join('-').slice(0, 8);
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
- if (match) {
247
- const [, date, uuidShort] = match;
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
  }
@@ -5,24 +5,12 @@
5
5
  * Inspired by Entire's checkpoint system
6
6
  */
7
7
 
8
- import { existsSync } from 'fs';
9
- import { join } from 'path';
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 = findProjectRoot();
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, readFileSync, writeFileSync } from 'fs';
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 = findProjectRoot();
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 => name.match(/^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/))
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 SESSION_ID_REGEX = /^[a-f0-9-]{36}$/;
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
- function findProjectRoot() {
12
- let dir = process.cwd();
13
- while (dir !== '/') {
14
- if (existsSync(join(dir, '.git'))) {
15
- return dir;
16
- }
17
- dir = join(dir, '..');
18
- }
19
- throw new Error('Not in a git repository');
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
- // Remove shit-cli hooks
34
- delete settings.hooks.session_start;
35
- delete settings.hooks.session_end;
36
- delete settings.hooks.tool_use;
37
- delete settings.hooks.edit_applied;
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 true;
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 = findProjectRoot();
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...');