@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/README.md +465 -0
- package/bin/ses.js +85 -0
- package/lib/agent-review.js +722 -0
- package/lib/checkpoint.js +320 -0
- package/lib/checkpoints.js +54 -0
- package/lib/clean.js +45 -0
- package/lib/commit.js +60 -0
- package/lib/config.js +28 -0
- package/lib/disable.js +152 -0
- package/lib/doctor.js +307 -0
- package/lib/enable.js +294 -0
- package/lib/explain.js +212 -0
- package/lib/extract.js +265 -0
- package/lib/git-shadow.js +136 -0
- package/lib/init.js +83 -0
- package/lib/list.js +62 -0
- package/lib/log.js +77 -0
- package/lib/prompts.js +125 -0
- package/lib/query.js +110 -0
- package/lib/redact.js +170 -0
- package/lib/report.js +296 -0
- package/lib/reset.js +122 -0
- package/lib/resume.js +224 -0
- package/lib/review-common.js +100 -0
- package/lib/review.js +652 -0
- package/lib/rewind.js +198 -0
- package/lib/session.js +225 -0
- package/lib/shadow.js +51 -0
- package/lib/status.js +198 -0
- package/lib/summarize.js +315 -0
- package/lib/view.js +50 -0
- package/lib/webhook.js +224 -0
- package/package.json +41 -0
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
|
+
}
|