@claudemini/ses-cli 1.4.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.
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Checkpoint module - manages checkpoints on git commit
5
+ * Inspired by Entire's approach:
6
+ * - Branch: ses/checkpoints/v1/YYYY-MM-DD-<uuid>
7
+ * - Each checkpoint is a commit linked to a code commit
8
+ * - Supports multiple agents (Claude Code, Gemini CLI, Cursor, etc.)
9
+ */
10
+
11
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
12
+ import { execSync } from 'child_process';
13
+ import { join, dirname } from 'path';
14
+ import { redactSecrets } from './redact.js';
15
+
16
+ function git(cmd, cwd) {
17
+ return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
18
+ }
19
+
20
+ function gitRaw(cmd, cwd, input) {
21
+ return execSync(`git ${cmd}`, {
22
+ cwd, encoding: 'utf-8', timeout: 10000,
23
+ input,
24
+ }).trim();
25
+ }
26
+
27
+ /**
28
+ * Detect which agent is running based on environment/hooks
29
+ */
30
+ export function detectAgent() {
31
+ // Check for Claude Code
32
+ if (process.env.CLAUDE_AGENT_ID || process.env.CLAUDE_SESSION_ID) {
33
+ return { type: 'claude-code', name: 'Claude Code' };
34
+ }
35
+
36
+ // Check for Gemini CLI
37
+ if (process.env.GEMINI_API_KEY || process.env.GEMINI_CLI) {
38
+ return { type: 'gemini-cli', name: 'Gemini CLI' };
39
+ }
40
+
41
+ // Check for Cursor
42
+ if (process.env.CURSOR_SESSION_ID) {
43
+ return { type: 'cursor', name: 'Cursor' };
44
+ }
45
+
46
+ // Check for OpenCode
47
+ if (process.env.OPENCODE_SESSION_ID) {
48
+ return { type: 'opencode', name: 'OpenCode' };
49
+ }
50
+
51
+ // Check for common hook files
52
+ if (existsSync(join(process.cwd(), '.claude', 'settings.json'))) {
53
+ return { type: 'claude-code', name: 'Claude Code' };
54
+ }
55
+ if (existsSync(join(process.cwd(), '.gemini', 'settings.json'))) {
56
+ return { type: 'gemini-cli', name: 'Gemini CLI' };
57
+ }
58
+ if (existsSync(join(process.cwd(), '.cursor', 'hooks.json'))) {
59
+ return { type: 'cursor', name: 'Cursor' };
60
+ }
61
+
62
+ return { type: 'unknown', name: 'Unknown' };
63
+ }
64
+
65
+ /**
66
+ * Get agent-specific hook configuration
67
+ */
68
+ export function getAgentHooks(agentType) {
69
+ const hooks = {
70
+ 'claude-code': {
71
+ settingsFile: '.claude/settings.json',
72
+ sessionStart: 'SessionStart',
73
+ sessionEnd: 'SessionEnd',
74
+ prompt: 'UserPromptSubmit',
75
+ toolPre: 'PreToolUse',
76
+ toolPost: 'PostToolUse',
77
+ },
78
+ 'gemini-cli': {
79
+ settingsFile: '.gemini/settings.json',
80
+ sessionStart: 'start',
81
+ sessionEnd: 'end',
82
+ prompt: 'prompt',
83
+ toolPre: 'tool_call',
84
+ toolPost: 'tool_result',
85
+ },
86
+ 'cursor': {
87
+ settingsFile: '.cursor/hooks.json',
88
+ sessionStart: 'session_start',
89
+ sessionEnd: 'session_end',
90
+ prompt: 'user_message',
91
+ toolPre: 'tool_before',
92
+ toolPost: 'tool_after',
93
+ },
94
+ 'opencode': {
95
+ settingsFile: '.opencode/plugins/ses.ts',
96
+ sessionStart: 'onSessionStart',
97
+ sessionEnd: 'onSessionEnd',
98
+ prompt: 'onUserMessage',
99
+ toolPre: 'onToolCall',
100
+ toolPost: 'onToolResult',
101
+ },
102
+ };
103
+
104
+ return hooks[agentType] || hooks['claude-code'];
105
+ }
106
+
107
+ /**
108
+ * Build a git tree from a directory with secret redaction
109
+ */
110
+ function buildTree(cwd, dirPath, redact = true) {
111
+ const entries = readdirSync(dirPath);
112
+ const treeLines = [];
113
+
114
+ for (const name of entries) {
115
+ const fullPath = join(dirPath, name);
116
+ const stat = statSync(fullPath);
117
+
118
+ if (stat.isDirectory()) {
119
+ const subTreeSha = buildTree(cwd, fullPath, redact);
120
+ treeLines.push(`040000 tree ${subTreeSha}\t${name}`);
121
+ } else {
122
+ let content = readFileSync(fullPath);
123
+
124
+ // Redact secrets for sensitive files
125
+ if (redact && (name === 'events.jsonl' || name === 'prompts.txt')) {
126
+ content = redactSecrets(content.toString());
127
+ }
128
+
129
+ const blobSha = gitRaw('hash-object -w --stdin', cwd, content);
130
+ treeLines.push(`100644 blob ${blobSha}\t${name}`);
131
+ }
132
+ }
133
+
134
+ if (treeLines.length === 0) return null;
135
+ return gitRaw('mktree', cwd, treeLines.join('\n'));
136
+ }
137
+
138
+ /**
139
+ * Commit session checkpoint to shadow branch
140
+ * Branch format: ses/checkpoints/v1/YYYY-MM-DD-<session-id>
141
+ */
142
+ export async function commitCheckpoint(projectRoot, sessionDir, sessionId, commitSha = null, options = {}) {
143
+ const autoSummarize = options.autoSummarize !== false; // default true
144
+
145
+ // Verify we're in a git repo
146
+ try {
147
+ git('rev-parse --git-dir', projectRoot);
148
+ } catch {
149
+ return { success: false, reason: 'not a git repo' };
150
+ }
151
+
152
+ // Branch naming: ses/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');
156
+ const branchName = `ses/checkpoints/v1/${datePart}-${uuidPart}`;
157
+ const refPath = `refs/heads/${branchName}`;
158
+
159
+ // Get linked commit SHA
160
+ const linkedCommit = commitSha || git('rev-parse HEAD', projectRoot);
161
+ const linkedCommitShort = linkedCommit.slice(0, 12);
162
+
163
+ // Build tree from session directory with redaction
164
+ const sessionTree = buildTree(projectRoot, sessionDir, true);
165
+ if (!sessionTree) {
166
+ return { success: false, reason: 'empty session' };
167
+ }
168
+
169
+ // Wrap session files: .ses-logs/<session-id>/files
170
+ const wrapperLine = `040000 tree ${sessionTree}\t${sessionId}`;
171
+ const logsTree = gitRaw('mktree', projectRoot, wrapperLine);
172
+ const rootLine = `040000 tree ${logsTree}\t.ses-logs`;
173
+ const rootTree = gitRaw('mktree', projectRoot, rootLine);
174
+
175
+ // Check if branch already exists (for parent chaining)
176
+ let parentArg = '';
177
+ try {
178
+ const existing = git(`rev-parse ${refPath}`, projectRoot);
179
+ parentArg = `-p ${existing}`;
180
+ } catch {
181
+ // orphan - no parent
182
+ }
183
+
184
+ // Create commit message with linked commit
185
+ const timestamp = new Date().toISOString().replace('T', ' ').split('.')[0];
186
+ const message = `checkpoint ${sessionId.slice(0, 8)} @ ${linkedCommitShort} | ${timestamp}`;
187
+
188
+ const commitHash = git(
189
+ `commit-tree ${rootTree} ${parentArg} -m "${message}"`,
190
+ projectRoot
191
+ );
192
+
193
+ // Update branch ref
194
+ git(`update-ref ${refPath} ${commitHash}`, projectRoot);
195
+
196
+ // Save checkpoint info to state
197
+ const stateFile = join(sessionDir, 'state.json');
198
+ try {
199
+ let state = JSON.parse(readFileSync(stateFile, 'utf-8'));
200
+ state.checkpoints = state.checkpoints || [];
201
+ state.checkpoints.push({
202
+ branch: branchName,
203
+ commit: commitHash,
204
+ linked_commit: linkedCommit,
205
+ timestamp: new Date().toISOString(),
206
+ });
207
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
208
+ } catch {
209
+ // best effort
210
+ }
211
+
212
+ // Auto-summarize if enabled
213
+ if (autoSummarize) {
214
+ try {
215
+ const { summarizeSession } = await import('./summarize.js');
216
+ const summaryResult = await summarizeSession(projectRoot, sessionId, sessionDir);
217
+ if (summaryResult.success) {
218
+ console.log(`āœ… AI summary generated: ${summaryResult.model}`);
219
+ }
220
+ } catch {
221
+ // Best effort - summarize is optional
222
+ }
223
+ }
224
+
225
+ return {
226
+ success: true,
227
+ branch: branchName,
228
+ commit: commitHash,
229
+ linked_commit: linkedCommit,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * List all checkpoints in the repo
235
+ */
236
+ export function listCheckpoints(projectRoot) {
237
+ try {
238
+ const output = git('branch --list "ses/checkpoints/v1/*"', projectRoot);
239
+ const branches = output.split('\n').map(b => b.trim().replace(/^\*?\s*/, '')).filter(Boolean);
240
+
241
+ const checkpoints = [];
242
+
243
+ for (const branch of branches) {
244
+ try {
245
+ // Extract session info from branch name (supports current and legacy formats)
246
+ const match = branch.match(/^(?:ses|shit)\/checkpoints\/v1\/(\d{4}-\d{2}-\d{2})-([a-f0-9]+)$/);
247
+ const legacyMatch = branch.match(/^(?:ses|shit)\/checkpoints\/v1\/([a-f0-9-]+)$/);
248
+ if (!match && !legacyMatch) {
249
+ continue;
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
+ });
274
+ } catch {
275
+ // Skip invalid branches
276
+ }
277
+ }
278
+
279
+ return checkpoints.sort((a, b) => b.date.localeCompare(a.date));
280
+ } catch {
281
+ return [];
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Get checkpoint details
287
+ */
288
+ export function getCheckpoint(projectRoot, branchOrCommit) {
289
+ try {
290
+ // Try as branch first
291
+ let branch = branchOrCommit;
292
+ try {
293
+ git('rev-parse --verify ' + branch, projectRoot);
294
+ } catch {
295
+ // Try as short commit
296
+ try {
297
+ branch = git('rev-parse --verify ' + branchOrCommit + '^{commit}', projectRoot);
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
302
+
303
+ const log = git(`log ${branch} --format=%B -1`, projectRoot);
304
+ const files = git(`ls-tree -r --name-only ${branch}`, projectRoot);
305
+
306
+ // Extract linked commit
307
+ const linkedMatch = log.match(/@ ([a-f0-9]+)/);
308
+ const linkedCommit = linkedMatch ? linkedMatch[1] : null;
309
+
310
+ return {
311
+ branch,
312
+ commit: branch.slice(0, 12),
313
+ linked_commit: linkedCommit,
314
+ message: log,
315
+ files: files.split('\n').filter(Boolean),
316
+ };
317
+ } catch {
318
+ return null;
319
+ }
320
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * List checkpoints - shows all checkpoints created on git commits
5
+ * Inspired by Entire's checkpoint system
6
+ */
7
+
8
+ import { listCheckpoints } from './checkpoint.js';
9
+ import { getProjectRoot } from './config.js';
10
+
11
+ export default async function checkpoints(args) {
12
+ try {
13
+ const projectRoot = getProjectRoot();
14
+ const verbose = args.includes('--verbose') || args.includes('-v');
15
+ const json = args.includes('--json');
16
+
17
+ const list = listCheckpoints(projectRoot);
18
+
19
+ if (list.length === 0) {
20
+ if (json) {
21
+ console.log(JSON.stringify({ checkpoints: [] }));
22
+ } else {
23
+ console.log('No checkpoints found.');
24
+ console.log('Checkpoints are created when you run "ses commit" after git commit.');
25
+ }
26
+ return;
27
+ }
28
+
29
+ if (json) {
30
+ console.log(JSON.stringify({ checkpoints: list }, null, 2));
31
+ return;
32
+ }
33
+
34
+ console.log(`šŸ“ø Found ${list.length} checkpoint(s):\n`);
35
+
36
+ for (const cp of list) {
37
+ console.log(`Branch: ${cp.branch}`);
38
+ console.log(` Commit: ${cp.commit}`);
39
+ console.log(` Linked: ${cp.linked_commit || 'none'}`);
40
+ console.log(` Date: ${cp.date}`);
41
+ console.log();
42
+ }
43
+
44
+ if (verbose) {
45
+ console.log('šŸ’” Usage:');
46
+ console.log(' ses rewind <branch> # Rollback to checkpoint');
47
+ console.log(' ses view <checkpoint> # View checkpoint details');
48
+ }
49
+
50
+ } catch (error) {
51
+ console.error('āŒ Failed to list checkpoints:', error.message);
52
+ process.exit(1);
53
+ }
54
+ }
package/lib/clean.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readdirSync, statSync, rmSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { getProjectRoot, getLogDir, SESSION_ID_REGEX } from './config.js';
6
+
7
+ export default async function clean(args) {
8
+ const logDir = getLogDir(getProjectRoot());
9
+ const daysArg = args.find(a => a.startsWith('--days='));
10
+ const days = daysArg ? parseInt(daysArg.split('=')[1]) : 7;
11
+ const dryRun = args.includes('--dry-run');
12
+ const cutoffTime = Date.now() - days * 24 * 60 * 60 * 1000;
13
+
14
+ try {
15
+ const entries = readdirSync(logDir);
16
+ const old = entries
17
+ .filter(name => SESSION_ID_REGEX.test(name))
18
+ .map(id => {
19
+ const dir = join(logDir, id);
20
+ return { id, dir, mtime: statSync(dir).mtime.getTime() };
21
+ })
22
+ .filter(s => s.mtime < cutoffTime);
23
+
24
+ if (old.length === 0) {
25
+ console.log(`No sessions older than ${days} days.`);
26
+ return;
27
+ }
28
+
29
+ console.log(`${old.length} session(s) older than ${days} days:`);
30
+ old.forEach(s => {
31
+ const age = Math.floor((Date.now() - s.mtime) / 86400000);
32
+ console.log(` ${s.id.slice(0, 8)}... (${age}d)`);
33
+ });
34
+
35
+ if (dryRun) {
36
+ console.log('\n[DRY RUN] No files deleted.');
37
+ } else {
38
+ old.forEach(s => rmSync(s.dir, { recursive: true, force: true }));
39
+ console.log(`\nDeleted ${old.length} session(s).`);
40
+ }
41
+ } catch (error) {
42
+ if (error.code === 'ENOENT') console.log('No log directory found.');
43
+ else throw error;
44
+ }
45
+ }
package/lib/commit.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Git commit hook - creates checkpoint on every git commit
5
+ * Similar to Entire's approach: checkpoint created on git commit
6
+ */
7
+
8
+ import { existsSync, readdirSync, statSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { execSync } from 'child_process';
11
+ import { commitCheckpoint } from './checkpoint.js';
12
+ import { getProjectRoot, SESSION_ID_REGEX } from './config.js';
13
+
14
+ export default async function commitHook(args) {
15
+ try {
16
+ const projectRoot = getProjectRoot();
17
+
18
+ // Get the commit that was just created
19
+ const commitSha = args[0] || execSync('git rev-parse HEAD', { cwd: projectRoot, encoding: 'utf-8' }).trim();
20
+ const commitShort = commitSha.slice(0, 12);
21
+
22
+ console.log(`šŸ“ø Creating checkpoint for commit ${commitShort}...`);
23
+
24
+ // Find active session
25
+ const sesLogsDir = join(projectRoot, '.ses-logs');
26
+ if (!existsSync(sesLogsDir)) {
27
+ console.log('No .ses-logs directory found, skipping checkpoint');
28
+ return;
29
+ }
30
+
31
+ // Find the most recent session
32
+ const sessions = readdirSync(sesLogsDir)
33
+ .filter(name => {
34
+ const fullPath = join(sesLogsDir, name);
35
+ return SESSION_ID_REGEX.test(name) && statSync(fullPath).isDirectory();
36
+ })
37
+ .map(name => ({
38
+ name,
39
+ path: join(sesLogsDir, name),
40
+ mtime: statSync(join(sesLogsDir, name)).mtime
41
+ }))
42
+ .sort((a, b) => b.mtime - a.mtime);
43
+
44
+ if (sessions.length === 0) {
45
+ console.log('No active session found, skipping checkpoint');
46
+ return;
47
+ }
48
+
49
+ const activeSession = sessions[0];
50
+
51
+ // Create checkpoint
52
+ await commitCheckpoint(projectRoot, activeSession.path, activeSession.name, commitSha);
53
+
54
+ console.log(`āœ… Checkpoint created for session ${activeSession.name}`);
55
+
56
+ } catch (error) {
57
+ console.error('Checkpoint creation failed:', error.message);
58
+ // Don't exit with error - checkpoint is best-effort
59
+ }
60
+ }
package/lib/config.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join, relative } from 'path';
4
+ import { execSync } from 'child_process';
5
+
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-]+)$/;
9
+
10
+ export function getProjectRoot() {
11
+ try {
12
+ return execSync('git rev-parse --show-toplevel', {
13
+ encoding: 'utf-8',
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ }).trim();
16
+ } catch {
17
+ return process.cwd();
18
+ }
19
+ }
20
+
21
+ export function getLogDir(projectRoot) {
22
+ return process.env.SES_LOG_DIR || join(projectRoot || getProjectRoot(), '.ses-logs');
23
+ }
24
+
25
+ export function toRelative(projectRoot, filePath) {
26
+ if (!filePath || !filePath.startsWith('/')) return filePath;
27
+ return relative(projectRoot, filePath) || filePath;
28
+ }
package/lib/disable.js ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Disable ses-cli in repository
5
+ * Similar to 'entire disable' - removes hooks and optionally cleans data
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs';
9
+ import { join } from 'path';
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
+
22
+ function removeClaudeHooks(projectRoot) {
23
+ const settingsFile = join(projectRoot, '.claude', 'settings.json');
24
+
25
+ if (!existsSync(settingsFile)) {
26
+ return false;
27
+ }
28
+
29
+ try {
30
+ const settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));
31
+ let removed = false;
32
+
33
+ if (settings.hooks) {
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('ses 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('ses 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
+ }
74
+
75
+ // If hooks object is empty, remove it
76
+ if (Object.keys(settings.hooks).length === 0) {
77
+ delete settings.hooks;
78
+ }
79
+ }
80
+
81
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
82
+ return removed;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function removeFromGitignore(projectRoot) {
89
+ const gitignoreFile = join(projectRoot, '.gitignore');
90
+
91
+ if (!existsSync(gitignoreFile)) {
92
+ return false;
93
+ }
94
+
95
+ try {
96
+ let gitignore = readFileSync(gitignoreFile, 'utf-8');
97
+ const lines = gitignore.split('\n');
98
+ const filtered = lines.filter(line => !line.trim().startsWith('.ses-logs'));
99
+
100
+ if (filtered.length !== lines.length) {
101
+ writeFileSync(gitignoreFile, filtered.join('\n'));
102
+ return true;
103
+ }
104
+ } catch {
105
+ // Ignore errors
106
+ }
107
+
108
+ return false;
109
+ }
110
+
111
+ export default async function disable(args) {
112
+ try {
113
+ const projectRoot = getProjectRoot();
114
+ const cleanData = args.includes('--clean') || args.includes('--purge');
115
+
116
+ console.log('šŸ”§ Disabling ses-cli in repository...');
117
+
118
+ // Remove Claude Code hooks
119
+ const hooksRemoved = removeClaudeHooks(projectRoot);
120
+ if (hooksRemoved) {
121
+ console.log('āœ… Removed Claude hooks from .claude/settings.json');
122
+ } else {
123
+ console.log('ā„¹ļø No Claude hooks found to remove');
124
+ }
125
+
126
+ // Remove from .gitignore
127
+ const gitignoreUpdated = removeFromGitignore(projectRoot);
128
+ if (gitignoreUpdated) {
129
+ console.log('āœ… Removed .ses-logs from .gitignore');
130
+ }
131
+
132
+ // Optionally clean data
133
+ if (cleanData) {
134
+ const sesLogsDir = join(projectRoot, '.ses-logs');
135
+ if (existsSync(sesLogsDir)) {
136
+ rmSync(sesLogsDir, { recursive: true, force: true });
137
+ console.log('āœ… Removed .ses-logs directory');
138
+ }
139
+ }
140
+
141
+ console.log('\nšŸŽ‰ ses-cli disabled successfully!');
142
+
143
+ if (!cleanData) {
144
+ console.log('\nNote: .ses-logs directory preserved.');
145
+ console.log('Use "ses disable --clean" to remove all data.');
146
+ }
147
+
148
+ } catch (error) {
149
+ console.error('āŒ Failed to disable ses-cli:', error.message);
150
+ process.exit(1);
151
+ }
152
+ }