@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.
package/lib/rewind.js ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Rewind to previous checkpoint
5
+ * Similar to 'entire rewind' - rollback to known good state
6
+ */
7
+
8
+ import { execSync } from 'child_process';
9
+ import { getProjectRoot } from './config.js';
10
+
11
+ function git(cmd, cwd) {
12
+ return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
13
+ }
14
+
15
+ function parseCheckpointRef(projectRoot, branch) {
16
+ const shadowMatch = branch.match(/^ses\/([a-f0-9]+)-([a-f0-9]+)$/);
17
+ if (shadowMatch) {
18
+ return {
19
+ type: 'shadow',
20
+ baseCommit: shadowMatch[1],
21
+ sessionShort: shadowMatch[2],
22
+ lookupKey: shadowMatch[2],
23
+ };
24
+ }
25
+
26
+ const checkpointMatch = branch.match(/^ses\/checkpoints\/v1\/(\d{4}-\d{2}-\d{2})-([a-f0-9]+)$/);
27
+ if (checkpointMatch) {
28
+ const message = git(`log ${branch} --format=%B -1`, projectRoot);
29
+ const linkedMatch = message.match(/@ ([a-f0-9]+)/);
30
+ const date = checkpointMatch[1];
31
+ const sessionShort = checkpointMatch[2];
32
+ return {
33
+ type: 'checkpoint',
34
+ baseCommit: linkedMatch ? linkedMatch[1] : null,
35
+ sessionShort,
36
+ lookupKey: `${date}-${sessionShort}`,
37
+ };
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ function listCheckpoints(projectRoot) {
44
+ try {
45
+ const branches = git('branch --list "ses/checkpoints/v1/*" "ses/*"', projectRoot)
46
+ .split('\n')
47
+ .map(b => b.trim().replace(/^\*?\s*/, ''))
48
+ .filter(Boolean);
49
+ const uniqueBranches = [...new Set(branches)];
50
+
51
+ const checkpoints = [];
52
+
53
+ for (const branch of uniqueBranches) {
54
+ try {
55
+ const parsed = parseCheckpointRef(projectRoot, branch);
56
+ if (!parsed) {
57
+ continue;
58
+ }
59
+
60
+ const log = git(`log ${branch} --oneline -1`, projectRoot);
61
+ const [commit, ...messageParts] = log.split(' ');
62
+ const message = messageParts.join(' ');
63
+ checkpoints.push({
64
+ branch,
65
+ commit,
66
+ baseCommit: parsed.baseCommit,
67
+ sessionShort: parsed.sessionShort,
68
+ lookupKey: parsed.lookupKey,
69
+ type: parsed.type,
70
+ message,
71
+ timestamp: git(`log ${branch} --format=%ci -1`, projectRoot)
72
+ });
73
+ } catch {
74
+ // Skip invalid branches
75
+ }
76
+ }
77
+
78
+ return checkpoints.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
79
+ } catch {
80
+ return [];
81
+ }
82
+ }
83
+
84
+ function getCurrentCommit(projectRoot) {
85
+ try {
86
+ return git('rev-parse HEAD', projectRoot);
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function hasUncommittedChanges(projectRoot) {
93
+ try {
94
+ const status = git('status --porcelain', projectRoot);
95
+ return status.length > 0;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ function rewindToCheckpoint(projectRoot, checkpoint, force = false) {
102
+ const currentCommit = getCurrentCommit(projectRoot);
103
+
104
+ if (!checkpoint.baseCommit) {
105
+ throw new Error(`Checkpoint ${checkpoint.lookupKey || checkpoint.sessionShort} is missing a linked base commit`);
106
+ }
107
+
108
+ if (!force && hasUncommittedChanges(projectRoot)) {
109
+ throw new Error('Working directory has uncommitted changes. Use --force to override or commit changes first.');
110
+ }
111
+
112
+ // Reset to the base commit of the checkpoint
113
+ git(`reset --hard ${checkpoint.baseCommit}`, projectRoot);
114
+
115
+ console.log(`✅ Rewound to checkpoint ${checkpoint.sessionShort}`);
116
+ console.log(` Base commit: ${checkpoint.baseCommit}`);
117
+ console.log(` Previous HEAD: ${currentCommit}`);
118
+
119
+ return currentCommit;
120
+ }
121
+
122
+ export default async function rewind(args) {
123
+ try {
124
+ const projectRoot = getProjectRoot();
125
+ const force = args.includes('--force') || args.includes('-f');
126
+ const interactive = args.includes('--interactive') || args.includes('-i');
127
+
128
+ // Get target checkpoint
129
+ let targetCheckpoint = null;
130
+ const checkpointArg = args.find(arg => !arg.startsWith('-'));
131
+
132
+ const checkpoints = listCheckpoints(projectRoot);
133
+
134
+ if (checkpoints.length === 0) {
135
+ console.log('❌ No checkpoints found');
136
+ console.log(' Checkpoints are created when you run "ses commit".');
137
+ process.exit(1);
138
+ }
139
+
140
+ if (checkpointArg) {
141
+ // Find specific checkpoint
142
+ targetCheckpoint = checkpoints.find(cp =>
143
+ cp.sessionShort.startsWith(checkpointArg) ||
144
+ cp.lookupKey.startsWith(checkpointArg) ||
145
+ (cp.baseCommit && cp.baseCommit.startsWith(checkpointArg))
146
+ );
147
+
148
+ if (!targetCheckpoint) {
149
+ console.error(`❌ Checkpoint not found: ${checkpointArg}`);
150
+ process.exit(1);
151
+ }
152
+ } else if (interactive) {
153
+ // Interactive selection
154
+ console.log('📋 Available checkpoints:\n');
155
+ checkpoints.forEach((cp, i) => {
156
+ const date = new Date(cp.timestamp).toLocaleString();
157
+ const base = cp.baseCommit ? cp.baseCommit.slice(0, 7) : 'unknown';
158
+ const key = cp.lookupKey || cp.sessionShort;
159
+ console.log(`${i + 1}. ${key} (${base}) - ${date}`);
160
+ console.log(` ${cp.message}`);
161
+ console.log();
162
+ });
163
+
164
+ // For now, just use the most recent
165
+ targetCheckpoint = checkpoints[0];
166
+ console.log(`Using most recent checkpoint: ${targetCheckpoint.lookupKey || targetCheckpoint.sessionShort}`);
167
+ } else {
168
+ // Use most recent checkpoint
169
+ targetCheckpoint = checkpoints[0];
170
+ }
171
+
172
+ const selectedKey = targetCheckpoint.lookupKey || targetCheckpoint.sessionShort;
173
+ console.log(`🔄 Rewinding to checkpoint: ${selectedKey}`);
174
+ console.log(` Branch: ${targetCheckpoint.branch}`);
175
+ console.log(` Base commit: ${targetCheckpoint.baseCommit || 'unknown'}`);
176
+ console.log(` Created: ${new Date(targetCheckpoint.timestamp).toLocaleString()}`);
177
+ console.log();
178
+
179
+ if (!force && hasUncommittedChanges(projectRoot)) {
180
+ console.error('❌ Working directory has uncommitted changes.');
181
+ console.error(' Commit your changes or use --force to discard them.');
182
+ process.exit(1);
183
+ }
184
+
185
+ const previousCommit = rewindToCheckpoint(projectRoot, targetCheckpoint, force);
186
+
187
+ console.log();
188
+ console.log('💡 To undo this rewind:');
189
+ console.log(` git reset --hard ${previousCommit}`);
190
+ console.log();
191
+ console.log('💡 To resume from this checkpoint:');
192
+ console.log(` ses resume ${selectedKey}`);
193
+
194
+ } catch (error) {
195
+ console.error('❌ Failed to rewind:', error.message);
196
+ process.exit(1);
197
+ }
198
+ }
package/lib/session.js ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Session state management and cross-session index.
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { toRelative } from './config.js';
10
+
11
+ // --- State management ---
12
+
13
+ const EMPTY_STATE = {
14
+ start_time: null,
15
+ last_time: null,
16
+ event_count: 0,
17
+ tool_counts: {},
18
+ file_ops: { read: [], write: [], edit: [], glob: [], grep: [] },
19
+ commands: [],
20
+ edits: [],
21
+ errors: [],
22
+ prompts: [],
23
+ last_hook_type: null,
24
+ shadow_branch: null,
25
+ // Enhanced fields for Entire-style tracking
26
+ transcript_path: null,
27
+ cwd: null,
28
+ model: null,
29
+ };
30
+
31
+ export function loadState(sessionDir) {
32
+ const stateFile = join(sessionDir, 'state.json');
33
+ if (!existsSync(stateFile)) {
34
+ return {
35
+ ...EMPTY_STATE,
36
+ file_ops: { read: [], write: [], edit: [], glob: [], grep: [] },
37
+ commands: [],
38
+ edits: [],
39
+ errors: [],
40
+ prompts: [],
41
+ };
42
+ }
43
+ try {
44
+ return JSON.parse(readFileSync(stateFile, 'utf-8'));
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ export function saveState(sessionDir, state) {
51
+ writeFileSync(join(sessionDir, 'state.json'), JSON.stringify(state, null, 2));
52
+ }
53
+
54
+ // --- Event processing ---
55
+
56
+ function addUnique(arr, val) {
57
+ if (val && !arr.includes(val)) arr.push(val);
58
+ }
59
+
60
+ export function processEvent(state, event, hookType, projectRoot) {
61
+ const now = new Date().toISOString();
62
+ state.event_count++;
63
+ state.last_hook_type = hookType;
64
+ state.last_time = now;
65
+ if (!state.start_time) state.start_time = now;
66
+
67
+ const toolName = event.tool_name;
68
+ const input = event.tool_input || {};
69
+
70
+ // User prompts - support both camelCase and kebab-case
71
+ if (hookType === 'user-prompt-submit' || hookType === 'UserPromptSubmit') {
72
+ // Extract prompt from various possible fields
73
+ let prompt = '';
74
+ if (event.prompt) {
75
+ prompt = typeof event.prompt === 'string' ? event.prompt :
76
+ event.prompt.text || event.prompt.message || JSON.stringify(event.prompt).slice(0, 500);
77
+ } else if (event.message) {
78
+ prompt = typeof event.message === 'string' ? event.message :
79
+ event.message.text || JSON.stringify(event.message).slice(0, 500);
80
+ } else if (event.text) {
81
+ prompt = event.text;
82
+ }
83
+
84
+ if (prompt) {
85
+ state.prompts.push({
86
+ time: now,
87
+ text: prompt.slice(0, 2000), // Limit prompt length
88
+ type: event.prompt_type || 'user'
89
+ });
90
+ }
91
+ return;
92
+ }
93
+
94
+ // Record transcript path from SessionStart
95
+ if (hookType === 'session-start' || hookType === 'SessionStart') {
96
+ if (event.transcript_path) {
97
+ state.transcript_path = event.transcript_path;
98
+ }
99
+ if (event.cwd) {
100
+ state.cwd = event.cwd;
101
+ }
102
+ if (event.model) {
103
+ state.model = event.model;
104
+ }
105
+ return;
106
+ }
107
+
108
+ // Mark session as ended for end/stop hooks.
109
+ if (hookType === 'session-end' || hookType === 'SessionEnd' || hookType === 'stop' || hookType === 'session_end' || hookType === 'end') {
110
+ state.end_time = now;
111
+ return;
112
+ }
113
+
114
+ // Tool events
115
+ if (hookType === 'post-tool-use' && toolName) {
116
+ state.tool_counts[toolName] = (state.tool_counts[toolName] || 0) + 1;
117
+
118
+ const rel = (p) => toRelative(projectRoot, p);
119
+
120
+ switch (toolName) {
121
+ case 'Read':
122
+ addUnique(state.file_ops.read, rel(input.file_path));
123
+ break;
124
+ case 'Write':
125
+ addUnique(state.file_ops.write, rel(input.file_path));
126
+ break;
127
+ case 'Edit':
128
+ addUnique(state.file_ops.edit, rel(input.file_path));
129
+ if (input.old_string || input.new_string) {
130
+ state.edits.push({
131
+ file: rel(input.file_path),
132
+ old: (input.old_string || '').slice(0, 200),
133
+ new: (input.new_string || '').slice(0, 200),
134
+ });
135
+ }
136
+ break;
137
+ case 'Glob':
138
+ addUnique(state.file_ops.glob, input.pattern);
139
+ break;
140
+ case 'Grep':
141
+ addUnique(state.file_ops.grep, input.pattern);
142
+ break;
143
+ case 'Bash':
144
+ if (input.command) state.commands.push(input.command.slice(0, 500));
145
+ break;
146
+ }
147
+
148
+ // Track errors
149
+ const result = event.tool_result;
150
+ if (result && typeof result === 'object' && result.isError) {
151
+ state.errors.push({
152
+ tool: toolName,
153
+ time: now,
154
+ message: (typeof result.content === 'string'
155
+ ? result.content : JSON.stringify(result.content)).slice(0, 300),
156
+ });
157
+ }
158
+ }
159
+ }
160
+
161
+ // --- Cross-session index ---
162
+
163
+ export function updateIndex(logDir, sessionId, intent, classification, changes, state) {
164
+ const indexFile = join(logDir, 'index.json');
165
+ let index = { project: '', sessions: [], file_history: {} };
166
+
167
+ if (existsSync(indexFile)) {
168
+ try {
169
+ index = JSON.parse(readFileSync(indexFile, 'utf-8'));
170
+ } catch { /* start fresh */ }
171
+ }
172
+
173
+ // Detect project name from git or directory
174
+ if (!index.project) {
175
+ const parts = logDir.split('/');
176
+ // logDir is typically <project>/.ses-logs
177
+ index.project = parts[parts.length - 2] || 'unknown';
178
+ }
179
+
180
+ const durationMs = state.start_time && state.last_time
181
+ ? new Date(state.last_time) - new Date(state.start_time) : 0;
182
+ const durationMin = Math.round(durationMs / 60000);
183
+
184
+ // Upsert session entry
185
+ const modifiedFiles = changes.files
186
+ .filter(f => f.operations.some(op => op !== 'read'))
187
+ .map(f => f.path);
188
+
189
+ const existingIdx = index.sessions.findIndex(s => s.id === sessionId);
190
+ const sessionEntry = {
191
+ id: sessionId,
192
+ date: (state.start_time || new Date().toISOString()).slice(0, 10),
193
+ type: classification.type,
194
+ intent: intent.goal.slice(0, 200),
195
+ files: modifiedFiles,
196
+ duration: durationMin,
197
+ risk: classification.risk,
198
+ };
199
+
200
+ if (existingIdx >= 0) {
201
+ index.sessions[existingIdx] = sessionEntry;
202
+ } else {
203
+ index.sessions.push(sessionEntry);
204
+ }
205
+
206
+ // Keep last 100 sessions
207
+ if (index.sessions.length > 100) {
208
+ index.sessions = index.sessions.slice(-100);
209
+ }
210
+
211
+ // Update file history
212
+ if (!index.file_history) index.file_history = {};
213
+ for (const file of modifiedFiles) {
214
+ if (!index.file_history[file]) index.file_history[file] = [];
215
+ if (!index.file_history[file].includes(sessionId)) {
216
+ index.file_history[file].push(sessionId);
217
+ }
218
+ // Keep last 20 sessions per file
219
+ if (index.file_history[file].length > 20) {
220
+ index.file_history[file] = index.file_history[file].slice(-20);
221
+ }
222
+ }
223
+
224
+ writeFileSync(indexFile, JSON.stringify(index, null, 2));
225
+ }
package/lib/shadow.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'child_process';
4
+ import { getProjectRoot } from './config.js';
5
+
6
+ export { commitShadow, listShadowBranches, shadowInfo } from './git-shadow.js';
7
+
8
+ export default async function shadowCmd(args) {
9
+ const projectRoot = getProjectRoot();
10
+ const sub = args[0];
11
+
12
+ if (sub === 'info' && args[1]) {
13
+ const { shadowInfo } = await import('./git-shadow.js');
14
+ const info = shadowInfo(projectRoot, args[1]);
15
+ if (!info) {
16
+ console.error(`Branch not found: ${args[1]}`);
17
+ process.exit(1);
18
+ }
19
+ console.log('Recent commits:');
20
+ console.log(info.log);
21
+ console.log('\nFiles:');
22
+ info.files.forEach(f => console.log(` ${f}`));
23
+ return;
24
+ }
25
+
26
+ // Default: list shadow branches
27
+ const { listShadowBranches } = await import('./git-shadow.js');
28
+ const branches = listShadowBranches(projectRoot);
29
+
30
+ if (branches.length === 0) {
31
+ console.log('No shadow branches found.');
32
+ console.log('Shadow branches are created on session-end.');
33
+ return;
34
+ }
35
+
36
+ console.log(`${branches.length} shadow branch(es):\n`);
37
+ branches.forEach(b => {
38
+ try {
39
+ const log = execSync(`git log ${b} --oneline -1`, {
40
+ cwd: projectRoot, encoding: 'utf-8',
41
+ }).trim();
42
+ console.log(` ${b} ${log}`);
43
+ } catch {
44
+ console.log(` ${b}`);
45
+ }
46
+ });
47
+
48
+ console.log('\nUsage:');
49
+ console.log(' ses shadow info <branch> # Show branch details');
50
+ console.log(' git log <branch> --oneline # View commits');
51
+ }
package/lib/status.js ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Show current session status
5
+ * Similar to 'entire status' - displays active session info
6
+ */
7
+
8
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { execSync } from 'child_process';
11
+ import { getProjectRoot, SESSION_ID_REGEX } from './config.js';
12
+
13
+ function getCurrentSession(projectRoot) {
14
+ const sesLogsDir = join(projectRoot, '.ses-logs');
15
+ if (!existsSync(sesLogsDir)) {
16
+ return null;
17
+ }
18
+
19
+ // Find the most recent session directory
20
+ const sessions = readdirSync(sesLogsDir)
21
+ .filter(name => {
22
+ const fullPath = join(sesLogsDir, name);
23
+ return SESSION_ID_REGEX.test(name) && statSync(fullPath).isDirectory();
24
+ })
25
+ .map(name => ({
26
+ name,
27
+ path: join(sesLogsDir, name),
28
+ mtime: statSync(join(sesLogsDir, name)).mtime
29
+ }))
30
+ .sort((a, b) => b.mtime - a.mtime);
31
+
32
+ if (sessions.length === 0) {
33
+ return null;
34
+ }
35
+
36
+ const latestSession = sessions[0];
37
+ const stateFile = join(latestSession.path, 'state.json');
38
+
39
+ if (!existsSync(stateFile)) {
40
+ return { ...latestSession, state: null };
41
+ }
42
+
43
+ try {
44
+ const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
45
+ return { ...latestSession, state };
46
+ } catch {
47
+ return { ...latestSession, state: null };
48
+ }
49
+ }
50
+
51
+ function getGitInfo(projectRoot) {
52
+ try {
53
+ const branch = execSync('git branch --show-current', { cwd: projectRoot, encoding: 'utf-8' }).trim();
54
+ const commit = execSync('git rev-parse --short HEAD', { cwd: projectRoot, encoding: 'utf-8' }).trim();
55
+ const status = execSync('git status --porcelain', { cwd: projectRoot, encoding: 'utf-8' }).trim();
56
+
57
+ return {
58
+ branch,
59
+ commit,
60
+ dirty: status.length > 0,
61
+ changes: status.split('\n').filter(Boolean).length
62
+ };
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function formatDuration(startTime) {
69
+ if (!startTime) return 'unknown';
70
+
71
+ const start = new Date(startTime);
72
+ const now = new Date();
73
+ const diff = now - start;
74
+
75
+ const minutes = Math.floor(diff / 60000);
76
+ const hours = Math.floor(minutes / 60);
77
+
78
+ if (hours > 0) {
79
+ return `${hours}h ${minutes % 60}m`;
80
+ } else {
81
+ return `${minutes}m`;
82
+ }
83
+ }
84
+
85
+ function getTouchedFileCount(state) {
86
+ const ops = state?.file_ops;
87
+ if (!ops || typeof ops !== 'object') {
88
+ return 0;
89
+ }
90
+
91
+ const touched = new Set([
92
+ ...(Array.isArray(ops.write) ? ops.write : []),
93
+ ...(Array.isArray(ops.edit) ? ops.edit : []),
94
+ ...(Array.isArray(ops.read) ? ops.read : []),
95
+ ].filter(Boolean));
96
+
97
+ return touched.size;
98
+ }
99
+
100
+ function hasSesHooks(settings) {
101
+ if (!settings?.hooks || typeof settings.hooks !== 'object') {
102
+ return false;
103
+ }
104
+
105
+ return Object.values(settings.hooks).some(value => {
106
+ if (typeof value === 'string') {
107
+ return value.includes('ses log');
108
+ }
109
+ if (!Array.isArray(value)) {
110
+ return false;
111
+ }
112
+ return value.some(entry =>
113
+ Array.isArray(entry?.hooks) &&
114
+ entry.hooks.some(hook => typeof hook?.command === 'string' && hook.command.includes('ses log'))
115
+ );
116
+ });
117
+ }
118
+
119
+ export default async function status(args) {
120
+ try {
121
+ const projectRoot = getProjectRoot();
122
+ const gitInfo = getGitInfo(projectRoot);
123
+ const currentSession = getCurrentSession(projectRoot);
124
+
125
+ console.log('📊 ses-cli Status\n');
126
+
127
+ // Git info
128
+ if (gitInfo) {
129
+ console.log(`📂 Repository: ${gitInfo.branch} @ ${gitInfo.commit}`);
130
+ if (gitInfo.dirty) {
131
+ console.log(`⚠️ Working tree: ${gitInfo.changes} uncommitted changes`);
132
+ } else {
133
+ console.log('✅ Working tree: clean');
134
+ }
135
+ } else {
136
+ console.log('❌ Git repository: not found');
137
+ }
138
+
139
+ console.log();
140
+
141
+ // Session info
142
+ if (currentSession) {
143
+ console.log(`🎯 Current Session: ${currentSession.name}`);
144
+
145
+ if (currentSession.state) {
146
+ const state = currentSession.state;
147
+ console.log(` Started: ${new Date(state.start_time).toLocaleString()}`);
148
+ console.log(` Duration: ${formatDuration(state.start_time)}`);
149
+ console.log(` Events: ${state.event_count || 0}`);
150
+ console.log(` Files: ${getTouchedFileCount(state)}`);
151
+
152
+ if (state.shadow_branch) {
153
+ console.log(` Shadow: ${state.shadow_branch}`);
154
+ }
155
+
156
+ if (state.session_type) {
157
+ console.log(` Type: ${state.session_type}`);
158
+ }
159
+
160
+ if (state.risk_level) {
161
+ const riskEmoji = state.risk_level === 'high' ? '🔴' :
162
+ state.risk_level === 'medium' ? '🟡' : '🟢';
163
+ console.log(` Risk: ${riskEmoji} ${state.risk_level}`);
164
+ }
165
+ } else {
166
+ console.log(' State: no state file found');
167
+ }
168
+ } else {
169
+ console.log('💤 No active session found');
170
+ console.log(' Run "ses enable" to start tracking sessions');
171
+ }
172
+
173
+ // Check if ses-cli is enabled
174
+ const claudeSettings = join(projectRoot, '.claude', 'settings.json');
175
+ if (existsSync(claudeSettings)) {
176
+ try {
177
+ const settings = JSON.parse(readFileSync(claudeSettings, 'utf-8'));
178
+ const hasHooks = hasSesHooks(settings);
179
+
180
+ console.log();
181
+ if (hasHooks) {
182
+ console.log('✅ ses-cli: enabled and configured');
183
+ } else {
184
+ console.log('⚠️ ses-cli: not configured (run "ses enable")');
185
+ }
186
+ } catch {
187
+ console.log('⚠️ ses-cli: configuration error');
188
+ }
189
+ } else {
190
+ console.log();
191
+ console.log('❌ ses-cli: not enabled (run "ses enable")');
192
+ }
193
+
194
+ } catch (error) {
195
+ console.error('❌ Failed to get status:', error.message);
196
+ process.exit(1);
197
+ }
198
+ }