@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/doctor.js ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Doctor command - fix or clean stuck sessions
5
+ * Similar to 'entire doctor' - repairs corrupted state and cleans up
6
+ */
7
+
8
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, 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 git(cmd, cwd) {
14
+ return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
15
+ }
16
+
17
+ function checkSesLogsStructure(projectRoot) {
18
+ const issues = [];
19
+ const sesLogsDir = join(projectRoot, '.ses-logs');
20
+
21
+ if (!existsSync(sesLogsDir)) {
22
+ issues.push({
23
+ type: 'missing_directory',
24
+ message: '.ses-logs directory not found',
25
+ fix: () => {
26
+ mkdirSync(sesLogsDir, { recursive: true });
27
+ return 'Created .ses-logs directory';
28
+ }
29
+ });
30
+ return issues;
31
+ }
32
+
33
+ // Check index.json
34
+ const indexFile = join(sesLogsDir, 'index.json');
35
+ if (!existsSync(indexFile)) {
36
+ issues.push({
37
+ type: 'missing_index',
38
+ message: 'index.json not found',
39
+ fix: () => {
40
+ const initialIndex = {
41
+ version: 2,
42
+ sessions: [],
43
+ files: {},
44
+ created: new Date().toISOString()
45
+ };
46
+ writeFileSync(indexFile, JSON.stringify(initialIndex, null, 2));
47
+ return 'Created index.json';
48
+ }
49
+ });
50
+ } else {
51
+ try {
52
+ JSON.parse(readFileSync(indexFile, 'utf-8'));
53
+ } catch {
54
+ issues.push({
55
+ type: 'corrupted_index',
56
+ message: 'index.json is corrupted',
57
+ fix: () => {
58
+ const backup = `${indexFile}.backup.${Date.now()}`;
59
+ copyFileSync(indexFile, backup);
60
+
61
+ const initialIndex = {
62
+ version: 2,
63
+ sessions: [],
64
+ files: {},
65
+ created: new Date().toISOString(),
66
+ recovered: true
67
+ };
68
+ writeFileSync(indexFile, JSON.stringify(initialIndex, null, 2));
69
+ return `Recreated index.json (backup saved as ${backup})`;
70
+ }
71
+ });
72
+ }
73
+ }
74
+
75
+ return issues;
76
+ }
77
+
78
+ function checkSessionDirectories(projectRoot) {
79
+ const issues = [];
80
+ const sesLogsDir = join(projectRoot, '.ses-logs');
81
+
82
+ if (!existsSync(sesLogsDir)) {
83
+ return issues;
84
+ }
85
+
86
+ const entries = readdirSync(sesLogsDir);
87
+ const sessionDirs = entries.filter(name => {
88
+ const fullPath = join(sesLogsDir, name);
89
+ return statSync(fullPath).isDirectory() && SESSION_ID_REGEX.test(name);
90
+ });
91
+
92
+ for (const sessionDir of sessionDirs) {
93
+ const sessionPath = join(sesLogsDir, sessionDir);
94
+ const stateFile = join(sessionPath, 'state.json');
95
+
96
+ if (!existsSync(stateFile)) {
97
+ issues.push({
98
+ type: 'missing_state',
99
+ message: `Session ${sessionDir} missing state.json`,
100
+ sessionDir,
101
+ fix: () => {
102
+ const defaultState = {
103
+ session_id: sessionDir,
104
+ start_time: new Date().toISOString(),
105
+ event_count: 0,
106
+ files: {},
107
+ recovered: true
108
+ };
109
+ writeFileSync(stateFile, JSON.stringify(defaultState, null, 2));
110
+ return `Created state.json for ${sessionDir}`;
111
+ }
112
+ });
113
+ } else {
114
+ try {
115
+ JSON.parse(readFileSync(stateFile, 'utf-8'));
116
+ } catch {
117
+ issues.push({
118
+ type: 'corrupted_state',
119
+ message: `Session ${sessionDir} has corrupted state.json`,
120
+ sessionDir,
121
+ fix: () => {
122
+ const backup = `${stateFile}.backup.${Date.now()}`;
123
+ copyFileSync(stateFile, backup);
124
+
125
+ const defaultState = {
126
+ session_id: sessionDir,
127
+ start_time: new Date().toISOString(),
128
+ event_count: 0,
129
+ files: {},
130
+ recovered: true
131
+ };
132
+ writeFileSync(stateFile, JSON.stringify(defaultState, null, 2));
133
+ return `Recreated state.json for ${sessionDir} (backup: ${backup})`;
134
+ }
135
+ });
136
+ }
137
+ }
138
+ }
139
+
140
+ return issues;
141
+ }
142
+
143
+ function checkOrphanedShadowBranches(projectRoot) {
144
+ const issues = [];
145
+ const sesLogsDir = join(projectRoot, '.ses-logs');
146
+
147
+ try {
148
+ const branches = git('branch --list "ses/*"', projectRoot)
149
+ .split('\n')
150
+ .map(b => b.trim().replace(/^\*?\s*/, ''))
151
+ .filter(Boolean);
152
+
153
+ const sessionDirs = existsSync(sesLogsDir) ?
154
+ readdirSync(sesLogsDir).filter(name => {
155
+ const fullPath = join(sesLogsDir, name);
156
+ return statSync(fullPath).isDirectory() && SESSION_ID_REGEX.test(name);
157
+ }) : [];
158
+
159
+ for (const branch of branches) {
160
+ const match = branch.match(/^ses\/([a-f0-9]+)-([a-f0-9]+)$/);
161
+ if (match) {
162
+ const [, baseCommit, sessionShort] = match;
163
+
164
+ // Check if corresponding session exists
165
+ const hasSession = sessionDirs.some(dir => dir.includes(sessionShort));
166
+
167
+ if (!hasSession) {
168
+ issues.push({
169
+ type: 'orphaned_shadow',
170
+ message: `Orphaned shadow branch: ${branch}`,
171
+ branch,
172
+ fix: () => {
173
+ git(`branch -D ${branch}`, projectRoot);
174
+ return `Deleted orphaned shadow branch: ${branch}`;
175
+ }
176
+ });
177
+ }
178
+ }
179
+ }
180
+ } catch {
181
+ // Git operations failed, skip shadow branch check
182
+ }
183
+
184
+ return issues;
185
+ }
186
+
187
+ function checkStuckSessions(projectRoot) {
188
+ const issues = [];
189
+ const sesLogsDir = join(projectRoot, '.ses-logs');
190
+
191
+ if (!existsSync(sesLogsDir)) {
192
+ return issues;
193
+ }
194
+
195
+ const entries = readdirSync(sesLogsDir);
196
+ const sessionDirs = entries.filter(name => {
197
+ const fullPath = join(sesLogsDir, name);
198
+ return statSync(fullPath).isDirectory() && SESSION_ID_REGEX.test(name);
199
+ });
200
+
201
+ const now = new Date();
202
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
203
+ const endedHookTypes = new Set(['session-end', 'SessionEnd', 'stop', 'session_end', 'end']);
204
+
205
+ for (const sessionDir of sessionDirs) {
206
+ const sessionPath = join(sesLogsDir, sessionDir);
207
+ const stateFile = join(sessionPath, 'state.json');
208
+
209
+ if (existsSync(stateFile)) {
210
+ try {
211
+ const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
212
+ const lastActivity = new Date(state.last_time || state.start_time);
213
+ const hasValidLastActivity = !Number.isNaN(lastActivity.getTime());
214
+ const endedByHook = endedHookTypes.has(state.last_hook_type);
215
+ const hasShadowBranch = typeof state.shadow_branch === 'string' && state.shadow_branch.length > 0;
216
+ const hasCheckpoint = Array.isArray(state.checkpoints) && state.checkpoints.length > 0;
217
+ const consideredEnded = Boolean(state.end_time) || endedByHook || hasShadowBranch || hasCheckpoint;
218
+
219
+ // Stuck means inactive for >24h and not explicitly/implicitly ended.
220
+ if (!consideredEnded && hasValidLastActivity && lastActivity < oneDayAgo) {
221
+ issues.push({
222
+ type: 'stuck_session',
223
+ message: `Stuck session: ${sessionDir} (last activity ${lastActivity.toLocaleString()})`,
224
+ sessionDir,
225
+ fix: () => {
226
+ state.end_time = new Date().toISOString();
227
+ state.stuck_session_recovered = true;
228
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
229
+ return `Marked stuck session as ended: ${sessionDir}`;
230
+ }
231
+ });
232
+ }
233
+ } catch {
234
+ // Already handled by checkSessionDirectories
235
+ }
236
+ }
237
+ }
238
+
239
+ return issues;
240
+ }
241
+
242
+ export default async function doctor(args) {
243
+ try {
244
+ const projectRoot = getProjectRoot();
245
+ const autoFix = args.includes('--fix') || args.includes('--auto-fix');
246
+ const verbose = args.includes('--verbose') || args.includes('-v');
247
+
248
+ console.log('🩺 Running ses-cli diagnostics...\n');
249
+
250
+ // Collect all issues
251
+ const allIssues = [
252
+ ...checkSesLogsStructure(projectRoot),
253
+ ...checkSessionDirectories(projectRoot),
254
+ ...checkOrphanedShadowBranches(projectRoot),
255
+ ...checkStuckSessions(projectRoot)
256
+ ];
257
+
258
+ if (allIssues.length === 0) {
259
+ console.log('āœ… No issues found! ses-cli is healthy.');
260
+ return;
261
+ }
262
+
263
+ console.log(`šŸ” Found ${allIssues.length} issue(s):\n`);
264
+
265
+ let fixedCount = 0;
266
+ for (const [index, issue] of allIssues.entries()) {
267
+ const prefix = `${index + 1}.`;
268
+ console.log(`${prefix} ${issue.message}`);
269
+
270
+ if (verbose) {
271
+ console.log(` Type: ${issue.type}`);
272
+ if (issue.sessionDir) {
273
+ console.log(` Session: ${issue.sessionDir}`);
274
+ }
275
+ if (issue.branch) {
276
+ console.log(` Branch: ${issue.branch}`);
277
+ }
278
+ }
279
+
280
+ if (autoFix && issue.fix) {
281
+ try {
282
+ const result = issue.fix();
283
+ console.log(` āœ… Fixed: ${result}`);
284
+ fixedCount++;
285
+ } catch (error) {
286
+ console.log(` āŒ Fix failed: ${error.message}`);
287
+ }
288
+ }
289
+
290
+ console.log();
291
+ }
292
+
293
+ if (autoFix) {
294
+ console.log(`šŸŽ‰ Fixed ${fixedCount}/${allIssues.length} issues.`);
295
+ if (fixedCount < allIssues.length) {
296
+ console.log(' Some issues could not be automatically fixed.');
297
+ }
298
+ } else {
299
+ console.log('šŸ’” To automatically fix these issues, run:');
300
+ console.log(' ses doctor --fix');
301
+ }
302
+
303
+ } catch (error) {
304
+ console.error('āŒ Doctor failed:', error.message);
305
+ process.exit(1);
306
+ }
307
+ }
package/lib/enable.js ADDED
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Enable ses-cli in repository
5
+ * Similar to 'entire enable' - sets up hooks and configuration
6
+ * Supports multiple agents: Claude Code, Gemini CLI, Cursor, OpenCode
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { execSync } from 'child_process';
12
+ import { getProjectRoot } from './config.js';
13
+
14
+ // Agent-specific hook configurations
15
+ const AGENT_HOOKS = {
16
+ 'claude-code': {
17
+ dir: '.claude',
18
+ settingsFile: '.claude/settings.json',
19
+ hooks: {
20
+ 'SessionStart': 'ses log session-start',
21
+ 'SessionEnd': 'ses log session-end',
22
+ 'UserPromptSubmit': 'ses log user-prompt-submit',
23
+ 'PreToolUse': 'ses log pre-tool-use',
24
+ 'PostToolUse': 'ses log post-tool-use',
25
+ 'Stop': 'ses log stop',
26
+ 'Notification': 'ses log notification',
27
+ }
28
+ },
29
+ 'gemini-cli': {
30
+ dir: '.gemini',
31
+ settingsFile: '.gemini/settings.json',
32
+ hooks: {
33
+ 'start': 'ses log start',
34
+ 'end': 'ses log end',
35
+ 'prompt': 'ses log prompt',
36
+ 'tool_call': 'ses log tool_call',
37
+ 'tool_result': 'ses log tool_result',
38
+ }
39
+ },
40
+ 'cursor': {
41
+ dir: '.cursor',
42
+ settingsFile: '.cursor/hooks.json',
43
+ hooks: {
44
+ 'session_start': 'ses log session_start',
45
+ 'session_end': 'ses log session_end',
46
+ 'user_message': 'ses log user_message',
47
+ 'tool_before': 'ses log tool_before',
48
+ 'tool_after': 'ses log tool_after',
49
+ }
50
+ },
51
+ 'opencode': {
52
+ dir: '.opencode',
53
+ settingsFile: '.opencode/plugins/ses.ts',
54
+ hooks: {
55
+ 'onSessionStart': 'ses log onSessionStart',
56
+ 'onSessionEnd': 'ses log onSessionEnd',
57
+ 'onUserMessage': 'ses log onUserMessage',
58
+ 'onToolCall': 'ses log onToolCall',
59
+ 'onToolResult': 'ses log onToolResult',
60
+ }
61
+ }
62
+ };
63
+
64
+ function setupAgentHooks(projectRoot, agentType) {
65
+ const config = AGENT_HOOKS[agentType];
66
+ if (!config) {
67
+ throw new Error(`Unknown agent: ${agentType}. Supported: ${Object.keys(AGENT_HOOKS).join(', ')}`);
68
+ }
69
+
70
+ const agentDir = join(projectRoot, config.dir);
71
+ const settingsFile = join(projectRoot, config.settingsFile);
72
+
73
+ // Create agent directory if it doesn't exist
74
+ if (!existsSync(agentDir)) {
75
+ mkdirSync(agentDir, { recursive: true });
76
+ }
77
+
78
+ let settings = {};
79
+ if (existsSync(settingsFile)) {
80
+ try {
81
+ settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));
82
+ } catch {
83
+ // Invalid JSON, start fresh
84
+ }
85
+ }
86
+
87
+ // Add ses-cli hooks
88
+ if (!settings.hooks) {
89
+ settings.hooks = {};
90
+ }
91
+
92
+ for (const [hookName, command] of Object.entries(config.hooks)) {
93
+ if (agentType === 'claude-code') {
94
+ const current = settings.hooks[hookName];
95
+ const existing = Array.isArray(current)
96
+ ? current
97
+ : (typeof current === 'string'
98
+ ? [{ matcher: '', hooks: [{ type: 'command', command: current }] }]
99
+ : []);
100
+ const alreadyExists = existing.some(entry =>
101
+ Array.isArray(entry?.hooks) &&
102
+ entry.hooks.some(hook => typeof hook?.command === 'string' && hook.command.includes('ses log'))
103
+ );
104
+
105
+ if (!alreadyExists) {
106
+ existing.push({ matcher: '', hooks: [{ type: 'command', command }] });
107
+ }
108
+ settings.hooks[hookName] = existing;
109
+ continue;
110
+ }
111
+
112
+ settings.hooks[hookName] = command;
113
+ }
114
+
115
+ // Ensure directory structure for opencode
116
+ if (agentType === 'opencode') {
117
+ const pluginsDir = join(projectRoot, '.opencode', 'plugins');
118
+ if (!existsSync(pluginsDir)) {
119
+ mkdirSync(pluginsDir, { recursive: true });
120
+ }
121
+ }
122
+
123
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
124
+ return settingsFile;
125
+ }
126
+
127
+ function setupGitCommitHook(projectRoot) {
128
+ // Add a git alias for automatic checkpoint on commit
129
+ const gitConfigFile = join(projectRoot, '.git', 'config');
130
+
131
+ try {
132
+ // Check if alias already exists
133
+ const config = existsSync(gitConfigFile) ? readFileSync(gitConfigFile, 'utf-8') : '';
134
+
135
+ if (!config.includes('ses-commit')) {
136
+ // Create a post-commit hook instead of using alias
137
+ const hooksDir = join(projectRoot, '.git', 'hooks');
138
+ if (!existsSync(hooksDir)) {
139
+ mkdirSync(hooksDir, { recursive: true });
140
+ }
141
+
142
+ const postCommitHook = join(hooksDir, 'post-commit');
143
+ const hookContent = `#!/bin/bash
144
+ # ses-cli: Create checkpoint on git commit
145
+ ses commit $GIT_COMMIT 2>/dev/null || true
146
+ `;
147
+
148
+ writeFileSync(postCommitHook, hookContent);
149
+ execSync(`chmod +x "${postCommitHook}"`, { stdio: 'ignore' });
150
+ console.log('āœ… Created post-commit hook for automatic checkpoints');
151
+ }
152
+ } catch {
153
+ // Best effort - git hooks are optional
154
+ }
155
+ }
156
+
157
+ function initializeSesLogs(projectRoot) {
158
+ const sesDir = join(projectRoot, '.ses-logs');
159
+ if (!existsSync(sesDir)) {
160
+ mkdirSync(sesDir, { recursive: true });
161
+ }
162
+
163
+ // Create index.json if it doesn't exist
164
+ const indexFile = join(sesDir, 'index.json');
165
+ if (!existsSync(indexFile)) {
166
+ const initialIndex = {
167
+ version: 2,
168
+ sessions: [],
169
+ files: {},
170
+ created: new Date().toISOString()
171
+ };
172
+ writeFileSync(indexFile, JSON.stringify(initialIndex, null, 2));
173
+ }
174
+ }
175
+
176
+ export default async function enable(args) {
177
+ try {
178
+ const projectRoot = getProjectRoot();
179
+
180
+ // Parse arguments
181
+ const agents = [];
182
+ let addCheckpointHook = false;
183
+ let useLocal = false;
184
+ let force = false;
185
+ let pushSessions = true;
186
+ let telemetry = true;
187
+ let summarize = true;
188
+
189
+ for (const arg of args) {
190
+ if (arg === '--all') {
191
+ // Enable for all supported agents
192
+ agents.push(...Object.keys(AGENT_HOOKS));
193
+ } else if (arg === '--checkpoint' || arg === '-c') {
194
+ addCheckpointHook = true;
195
+ } else if (arg === '--local') {
196
+ useLocal = true;
197
+ } else if (arg === '--project') {
198
+ useLocal = false; // Force project-level settings
199
+ } else if (arg === '--force' || arg === '-f') {
200
+ force = true;
201
+ } else if (arg === '--skip-push-sessions') {
202
+ pushSessions = false;
203
+ } else if (arg === '--no-summarize') {
204
+ summarize = false;
205
+ } else if (arg.startsWith('--telemetry=')) {
206
+ telemetry = arg.split('=')[1] !== 'false';
207
+ } else if (arg === '--telemetry') {
208
+ telemetry = true;
209
+ } else if (!arg.startsWith('-')) {
210
+ // Assume it's an agent name
211
+ if (AGENT_HOOKS[arg]) {
212
+ agents.push(arg);
213
+ } else {
214
+ console.log(`āš ļø Unknown agent: ${arg}. Supported: ${Object.keys(AGENT_HOOKS).join(', ')}`);
215
+ }
216
+ }
217
+ }
218
+
219
+ // Default to Claude Code if no agent specified
220
+ if (agents.length === 0) {
221
+ agents.push('claude-code');
222
+ }
223
+
224
+ console.log('šŸ”§ Enabling ses-cli in repository...\n');
225
+
226
+ // Write configuration
227
+ const configData = {
228
+ enabled: true,
229
+ push_sessions: pushSessions,
230
+ summarize: summarize,
231
+ telemetry: telemetry,
232
+ log_level: 'info'
233
+ };
234
+
235
+ const settingsFileName = useLocal ? 'settings.local.json' : 'settings.json';
236
+
237
+ // Write to .claude/ directory (project or local)
238
+ const configDir = join(projectRoot, '.claude');
239
+ if (!existsSync(configDir)) {
240
+ mkdirSync(configDir, { recursive: true });
241
+ }
242
+
243
+ const configFile = join(configDir, settingsFileName);
244
+ writeFileSync(configFile, JSON.stringify(configData, null, 2));
245
+ console.log(`āœ… Wrote configuration: ${configFile}`);
246
+
247
+ // Setup hooks for each agent
248
+ for (const agent of agents) {
249
+ const settingsFile = setupAgentHooks(projectRoot, agent);
250
+ console.log(`āœ… Enabled ${AGENT_HOOKS[agent].dir} hooks: ${settingsFile}`);
251
+ }
252
+
253
+ // Setup git commit hook for checkpoints
254
+ if (addCheckpointHook) {
255
+ setupGitCommitHook(projectRoot);
256
+ }
257
+
258
+ // Initialize .ses-logs directory
259
+ initializeSesLogs(projectRoot);
260
+ console.log('āœ… Initialized .ses-logs directory');
261
+
262
+ // Add .ses-logs to .gitignore if not already there
263
+ const gitignoreFile = join(projectRoot, '.gitignore');
264
+ let gitignore = '';
265
+ if (existsSync(gitignoreFile)) {
266
+ gitignore = readFileSync(gitignoreFile, 'utf-8');
267
+ }
268
+
269
+ if (!gitignore.includes('.ses-logs')) {
270
+ const newLine = gitignore.endsWith('\n') || gitignore === '' ? '' : '\n';
271
+ writeFileSync(gitignoreFile, gitignore + newLine + '.ses-logs/\n');
272
+ console.log('āœ… Added .ses-logs to .gitignore');
273
+ }
274
+
275
+ console.log('\nšŸŽ‰ ses-cli enabled successfully!');
276
+ console.log('\nUsage:');
277
+ console.log(' ses status # Show current session');
278
+ console.log(' ses list # List all sessions');
279
+ console.log(' ses checkpoints # List all checkpoints');
280
+ console.log(' ses commit # Manually create checkpoint after git commit');
281
+ console.log('\nOptions:');
282
+ console.log(' --all # Enable for all supported agents');
283
+ console.log(' --checkpoint, -c # Enable automatic checkpoint on git commit');
284
+ console.log(' --no-summarize # Disable AI summary generation');
285
+ console.log(' --skip-push-sessions # Disable auto-push to remote');
286
+ console.log(' --telemetry=false # Disable anonymous telemetry');
287
+ console.log('\nAI Summary:');
288
+ console.log(' Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI summaries');
289
+
290
+ } catch (error) {
291
+ console.error('āŒ Failed to enable ses-cli:', error.message);
292
+ process.exit(1);
293
+ }
294
+ }