@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/report.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Report generation module.
|
|
5
|
+
* Produces summary.json (v2, bot-readable), summary.txt (human-readable),
|
|
6
|
+
* prompts.txt, context.md, and metadata.json from session state + semantic data.
|
|
7
|
+
* Reference: Entire CLI's checkpoint format
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { writeFileSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate all report files for a session.
|
|
15
|
+
*/
|
|
16
|
+
export function generateReports(sessionDir, sessionId, state, intent, changes, classification) {
|
|
17
|
+
const durationMs = state.start_time && state.last_time
|
|
18
|
+
? new Date(state.last_time) - new Date(state.start_time) : 0;
|
|
19
|
+
const durationMin = Math.round(durationMs / 60000);
|
|
20
|
+
|
|
21
|
+
writeSummaryJson(sessionDir, sessionId, state, intent, changes, classification, durationMin);
|
|
22
|
+
writeSummaryTxt(sessionDir, sessionId, state, intent, changes, classification, durationMin);
|
|
23
|
+
writePromptsTxt(sessionDir, state);
|
|
24
|
+
writeMetadataJson(sessionDir, sessionId, state, intent, classification, durationMin);
|
|
25
|
+
writeContextMd(sessionDir, sessionId, state, intent, changes, durationMin);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeSummaryJson(sessionDir, sessionId, state, intent, changes, classification, durationMin) {
|
|
29
|
+
const summary = {
|
|
30
|
+
version: '2.0',
|
|
31
|
+
session: {
|
|
32
|
+
id: sessionId,
|
|
33
|
+
start: state.start_time,
|
|
34
|
+
end: state.last_time,
|
|
35
|
+
duration_minutes: durationMin,
|
|
36
|
+
type: classification.type,
|
|
37
|
+
intent: intent.goal,
|
|
38
|
+
risk: classification.risk,
|
|
39
|
+
summary: classification.summary,
|
|
40
|
+
},
|
|
41
|
+
changes: {
|
|
42
|
+
files: changes.files
|
|
43
|
+
.filter(f => f.operations.some(op => op !== 'read'))
|
|
44
|
+
.map(f => ({
|
|
45
|
+
path: f.path,
|
|
46
|
+
category: f.category,
|
|
47
|
+
operations: f.operations.filter(op => op !== 'read'),
|
|
48
|
+
editCount: f.editCount,
|
|
49
|
+
editSummary: f.editSummary,
|
|
50
|
+
})),
|
|
51
|
+
summary: changes.summary,
|
|
52
|
+
},
|
|
53
|
+
activity: {
|
|
54
|
+
tools: state.tool_counts,
|
|
55
|
+
commands: changes.commands,
|
|
56
|
+
errors: state.errors,
|
|
57
|
+
},
|
|
58
|
+
review_hints: {
|
|
59
|
+
tests_run: classification.reviewHints.testsRun,
|
|
60
|
+
build_verified: classification.reviewHints.buildVerified,
|
|
61
|
+
files_without_tests: classification.reviewHints.filesWithoutTests,
|
|
62
|
+
large_change: classification.reviewHints.largeChange,
|
|
63
|
+
config_changed: classification.reviewHints.configChanged,
|
|
64
|
+
migration_added: classification.reviewHints.migrationAdded,
|
|
65
|
+
},
|
|
66
|
+
prompts: state.prompts.map(p => typeof p === 'string' ? p : { time: p.time, text: p.text, type: p.type }),
|
|
67
|
+
transcript: state.transcript_path,
|
|
68
|
+
model: state.model,
|
|
69
|
+
scope: intent.scope,
|
|
70
|
+
};
|
|
71
|
+
writeFileSync(join(sessionDir, 'summary.json'), JSON.stringify(summary, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeSummaryTxt(sessionDir, sessionId, state, intent, changes, classification, durationMin) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
|
|
77
|
+
// Header
|
|
78
|
+
lines.push(`# Session: ${sessionId.slice(0, 8)}...`);
|
|
79
|
+
lines.push(`Type: ${classification.type} | Risk: ${classification.risk} | Duration: ${durationMin}min`);
|
|
80
|
+
if (classification.summary) {
|
|
81
|
+
lines.push(`Summary: ${classification.summary}`);
|
|
82
|
+
}
|
|
83
|
+
lines.push('');
|
|
84
|
+
|
|
85
|
+
// Intent
|
|
86
|
+
if (intent.goal) {
|
|
87
|
+
lines.push('## Intent');
|
|
88
|
+
lines.push(` Goal: ${intent.goal.slice(0, 200)}`);
|
|
89
|
+
if (intent.scope.length > 0) {
|
|
90
|
+
lines.push(` Scope: ${intent.scope.join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Changes
|
|
96
|
+
const modified = changes.files.filter(f => f.operations.some(op => op !== 'read'));
|
|
97
|
+
if (modified.length > 0) {
|
|
98
|
+
lines.push('## Changes');
|
|
99
|
+
for (const f of modified.slice(0, 20)) {
|
|
100
|
+
const ops = f.operations.filter(op => op !== 'read').join(',');
|
|
101
|
+
lines.push(` [${f.category}] ${f.path} (${ops}${f.editCount > 0 ? ` x${f.editCount}` : ''})`);
|
|
102
|
+
}
|
|
103
|
+
if (modified.length > 20) {
|
|
104
|
+
lines.push(` ... and ${modified.length - 20} more files`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Tools
|
|
110
|
+
if (Object.keys(state.tool_counts).length > 0) {
|
|
111
|
+
lines.push('## Tools');
|
|
112
|
+
Object.entries(state.tool_counts)
|
|
113
|
+
.sort((a, b) => b[1] - a[1])
|
|
114
|
+
.forEach(([t, c]) => lines.push(` ${t}: ${c}`));
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Commands by category
|
|
119
|
+
const cmdEntries = Object.entries(changes.commands).filter(([, cmds]) => cmds.length > 0);
|
|
120
|
+
if (cmdEntries.length > 0) {
|
|
121
|
+
lines.push('## Commands');
|
|
122
|
+
for (const [cat, cmds] of cmdEntries) {
|
|
123
|
+
lines.push(` [${cat}] ${cmds.slice(0, 5).join(' | ')}${cmds.length > 5 ? ` (+${cmds.length - 5})` : ''}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push('');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Review hints
|
|
129
|
+
const hints = classification.reviewHints;
|
|
130
|
+
lines.push('## Review Hints');
|
|
131
|
+
lines.push(` Tests run: ${hints.testsRun ? 'YES' : 'NO'}`);
|
|
132
|
+
lines.push(` Build verified: ${hints.buildVerified ? 'YES' : 'NO'}`);
|
|
133
|
+
if (hints.configChanged) lines.push(' WARNING: Config files changed');
|
|
134
|
+
if (hints.migrationAdded) lines.push(' WARNING: Database migration added');
|
|
135
|
+
if (hints.largeChange) lines.push(' WARNING: Large change (>10 files)');
|
|
136
|
+
if (hints.filesWithoutTests.length > 0) {
|
|
137
|
+
lines.push(` Files without tests: ${hints.filesWithoutTests.slice(0, 5).join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push('');
|
|
140
|
+
|
|
141
|
+
// Errors
|
|
142
|
+
if (state.errors.length > 0) {
|
|
143
|
+
lines.push('## Errors');
|
|
144
|
+
state.errors.slice(-5).forEach(e => lines.push(` [${e.tool}] ${(e.message || '').slice(0, 100)}`));
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Prompts
|
|
149
|
+
if (state.prompts.length > 0) {
|
|
150
|
+
lines.push('## Prompts');
|
|
151
|
+
state.prompts.forEach(p => {
|
|
152
|
+
const text = typeof p === 'string' ? p : p.text;
|
|
153
|
+
lines.push(` > ${text.slice(0, 120)}`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
writeFileSync(join(sessionDir, 'summary.txt'), lines.join('\n') + '\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writePromptsTxt(sessionDir, state) {
|
|
161
|
+
if (state.prompts.length > 0) {
|
|
162
|
+
const promptLines = state.prompts
|
|
163
|
+
.map(p => {
|
|
164
|
+
const time = typeof p === 'string' ? '' : p.time;
|
|
165
|
+
const text = typeof p === 'string' ? p : p.text;
|
|
166
|
+
return time ? `=== ${time} ===\n${text}\n` : `${text}\n`;
|
|
167
|
+
})
|
|
168
|
+
.join('\n');
|
|
169
|
+
writeFileSync(join(sessionDir, 'prompts.txt'), promptLines);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function writeMetadataJson(sessionDir, sessionId, state, intent, classification, durationMin) {
|
|
174
|
+
writeFileSync(join(sessionDir, 'metadata.json'), JSON.stringify({
|
|
175
|
+
session_id: sessionId,
|
|
176
|
+
start_time: state.start_time,
|
|
177
|
+
last_updated: state.last_time,
|
|
178
|
+
duration_minutes: durationMin,
|
|
179
|
+
type: classification.type,
|
|
180
|
+
intent: intent.goal.slice(0, 200),
|
|
181
|
+
risk: classification.risk,
|
|
182
|
+
event_count: state.event_count,
|
|
183
|
+
tool_calls: Object.values(state.tool_counts).reduce((a, b) => a + b, 0),
|
|
184
|
+
files_touched: state.file_ops.write.length + state.file_ops.edit.length,
|
|
185
|
+
errors: state.errors.length,
|
|
186
|
+
scope: intent.scope,
|
|
187
|
+
last_hook_type: state.last_hook_type,
|
|
188
|
+
// Enhanced metadata for Entire-style tracking
|
|
189
|
+
transcript_path: state.transcript_path || null,
|
|
190
|
+
model: state.model || null,
|
|
191
|
+
cwd: state.cwd || null,
|
|
192
|
+
}, null, 2));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Write context.md - human-readable session context (Entire-style)
|
|
197
|
+
*/
|
|
198
|
+
function writeContextMd(sessionDir, sessionId, state, intent, changes, durationMin) {
|
|
199
|
+
const lines = [];
|
|
200
|
+
|
|
201
|
+
// Header
|
|
202
|
+
lines.push(`# Session Context: ${sessionId}`);
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push(`**Started**: ${state.start_time ? new Date(state.start_time).toLocaleString() : 'unknown'}`);
|
|
205
|
+
lines.push(`**Ended**: ${state.last_time ? new Date(state.last_time).toLocaleString() : 'in progress'}`);
|
|
206
|
+
lines.push(`**Duration**: ${durationMin} minutes`);
|
|
207
|
+
lines.push(`**Model**: ${state.model || 'unknown'}`);
|
|
208
|
+
lines.push(`**Working Directory**: ${state.cwd || 'unknown'}`);
|
|
209
|
+
lines.push(`**Events**: ${state.event_count}`);
|
|
210
|
+
lines.push('');
|
|
211
|
+
|
|
212
|
+
// Transcript reference (Entire-style)
|
|
213
|
+
if (state.transcript_path) {
|
|
214
|
+
lines.push('## Transcript');
|
|
215
|
+
lines.push(`Full transcript available at: \`${state.transcript_path}\``);
|
|
216
|
+
lines.push('');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// User Prompts
|
|
220
|
+
if (state.prompts && state.prompts.length > 0) {
|
|
221
|
+
lines.push('## User Prompts');
|
|
222
|
+
lines.push('');
|
|
223
|
+
state.prompts.forEach((p, i) => {
|
|
224
|
+
const time = p.time ? new Date(p.time).toLocaleString() : '';
|
|
225
|
+
const text = typeof p === 'string' ? p : p.text;
|
|
226
|
+
const type = typeof p === 'object' && p.type ? ` [${p.type}]` : '';
|
|
227
|
+
|
|
228
|
+
lines.push(`### Prompt ${i + 1}${type}${time ? ` - ${time}` : ''}`);
|
|
229
|
+
lines.push('');
|
|
230
|
+
lines.push('```');
|
|
231
|
+
lines.push(text.slice(0, 2000));
|
|
232
|
+
lines.push('```');
|
|
233
|
+
lines.push('');
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Intent & Scope
|
|
238
|
+
if (intent.goal) {
|
|
239
|
+
lines.push('## Intent');
|
|
240
|
+
lines.push('');
|
|
241
|
+
lines.push(intent.goal);
|
|
242
|
+
lines.push('');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (intent.scope && intent.scope.length > 0) {
|
|
246
|
+
lines.push('## Scope');
|
|
247
|
+
lines.push('');
|
|
248
|
+
intent.scope.forEach(s => lines.push(`- ${s}`));
|
|
249
|
+
lines.push('');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Changes Summary
|
|
253
|
+
const modified = changes.files.filter(f => f.operations.some(op => op !== 'read'));
|
|
254
|
+
if (modified.length > 0) {
|
|
255
|
+
lines.push('## Files Changed');
|
|
256
|
+
lines.push('');
|
|
257
|
+
modified.slice(0, 30).forEach(f => {
|
|
258
|
+
const ops = f.operations.filter(op => op !== 'read').join(', ');
|
|
259
|
+
lines.push(`- \`${f.path}\` (${ops})`);
|
|
260
|
+
});
|
|
261
|
+
if (modified.length > 30) {
|
|
262
|
+
lines.push(`- ... and ${modified.length - 30} more files`);
|
|
263
|
+
}
|
|
264
|
+
lines.push('');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Tool Usage
|
|
268
|
+
if (Object.keys(state.tool_counts).length > 0) {
|
|
269
|
+
lines.push('## Tool Usage');
|
|
270
|
+
lines.push('');
|
|
271
|
+
Object.entries(state.tool_counts)
|
|
272
|
+
.sort((a, b) => b[1] - a[1])
|
|
273
|
+
.forEach(([tool, count]) => {
|
|
274
|
+
lines.push(`- ${tool}: ${count}`);
|
|
275
|
+
});
|
|
276
|
+
lines.push('');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Errors
|
|
280
|
+
if (state.errors.length > 0) {
|
|
281
|
+
lines.push('## Errors');
|
|
282
|
+
lines.push('');
|
|
283
|
+
state.errors.slice(0, 10).forEach(e => {
|
|
284
|
+
lines.push(`- **[${e.tool}]** ${(e.message || '').slice(0, 200)}`);
|
|
285
|
+
});
|
|
286
|
+
lines.push('');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Footer
|
|
290
|
+
lines.push('---');
|
|
291
|
+
lines.push('');
|
|
292
|
+
lines.push(`*Generated by ses-cli at ${new Date().toISOString()}*`);
|
|
293
|
+
lines.push(`*See \`events.jsonl\` for complete event history*`);
|
|
294
|
+
|
|
295
|
+
writeFileSync(join(sessionDir, 'context.md'), lines.join('\n') + '\n');
|
|
296
|
+
}
|
package/lib/reset.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reset - delete checkpoint for current HEAD commit
|
|
5
|
+
* Similar to 'entire reset' - removes shadow branch and session state
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } 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 findCheckpointForCommit(projectRoot, commitSha) {
|
|
18
|
+
try {
|
|
19
|
+
const branches = git('branch --list "ses/checkpoints/v1/*"', projectRoot)
|
|
20
|
+
.split('\n')
|
|
21
|
+
.map(b => b.trim().replace(/^\*?\s*/, ''))
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
|
|
24
|
+
for (const branch of branches) {
|
|
25
|
+
try {
|
|
26
|
+
const log = git(`log ${branch} --oneline -1`, projectRoot);
|
|
27
|
+
if (log.includes(commitSha.slice(0, 12))) {
|
|
28
|
+
return branch;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Skip
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findActiveSession(projectRoot) {
|
|
41
|
+
const sesLogsDir = join(projectRoot, '.ses-logs');
|
|
42
|
+
if (!existsSync(sesLogsDir)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sessions = readdirSync(sesLogsDir)
|
|
47
|
+
.filter(name => {
|
|
48
|
+
const fullPath = join(sesLogsDir, name);
|
|
49
|
+
return SESSION_ID_REGEX.test(name) && statSync(fullPath).isDirectory();
|
|
50
|
+
})
|
|
51
|
+
.map(name => ({
|
|
52
|
+
name,
|
|
53
|
+
path: join(sesLogsDir, name),
|
|
54
|
+
mtime: statSync(join(sesLogsDir, name)).mtime
|
|
55
|
+
}))
|
|
56
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
57
|
+
|
|
58
|
+
return sessions.length > 0 ? sessions[0] : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default async function reset(args) {
|
|
62
|
+
try {
|
|
63
|
+
const projectRoot = getProjectRoot();
|
|
64
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
65
|
+
|
|
66
|
+
// Get current commit
|
|
67
|
+
const commitSha = git('rev-parse HEAD', projectRoot);
|
|
68
|
+
const commitShort = commitSha.slice(0, 12);
|
|
69
|
+
|
|
70
|
+
console.log(`🔄 Resetting checkpoint for commit: ${commitShort}\n`);
|
|
71
|
+
|
|
72
|
+
// Find checkpoint for this commit
|
|
73
|
+
const branch = findCheckpointForCommit(projectRoot, commitSha);
|
|
74
|
+
|
|
75
|
+
if (!branch) {
|
|
76
|
+
console.log('ℹ️ No checkpoint found for current commit.');
|
|
77
|
+
console.log(' Checkpoints are created on git commit, not automatically on session start.');
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`📸 Found checkpoint branch: ${branch}`);
|
|
82
|
+
|
|
83
|
+
if (!force) {
|
|
84
|
+
console.log('\n⚠️ This will delete the checkpoint and its branch.');
|
|
85
|
+
console.log(' Use --force to proceed without confirmation.');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Delete the checkpoint branch
|
|
90
|
+
try {
|
|
91
|
+
git(`branch -D ${branch}`, projectRoot);
|
|
92
|
+
console.log(`✅ Deleted checkpoint branch: ${branch}`);
|
|
93
|
+
} catch {
|
|
94
|
+
console.log('ℹ️ Branch already deleted or not found');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Optionally clean active session if it's linked to this commit
|
|
98
|
+
const activeSession = findActiveSession(projectRoot);
|
|
99
|
+
if (activeSession) {
|
|
100
|
+
const stateFile = join(activeSession.path, 'state.json');
|
|
101
|
+
if (existsSync(stateFile)) {
|
|
102
|
+
try {
|
|
103
|
+
const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
104
|
+
if (state.checkpoints && state.checkpoints.some(cp => cp.linked_commit === commitSha)) {
|
|
105
|
+
// Remove checkpoint references
|
|
106
|
+
state.checkpoints = state.checkpoints.filter(cp => cp.linked_commit !== commitSha);
|
|
107
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
108
|
+
console.log('✅ Updated session state');
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Best effort
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log('\n✅ Reset complete!');
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('❌ Failed to reset:', error.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/lib/resume.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resume session from checkpoint
|
|
5
|
+
* Similar to 'entire resume' - restore branch and session metadata
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { getProjectRoot } from './config.js';
|
|
13
|
+
|
|
14
|
+
function git(cmd, cwd) {
|
|
15
|
+
return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function findCheckpoint(projectRoot, checkpointId) {
|
|
19
|
+
try {
|
|
20
|
+
const branches = git('branch --list "ses/checkpoints/v1/*" "ses/*"', projectRoot)
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map(b => b.trim().replace(/^\*?\s*/, ''))
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
|
|
25
|
+
for (const branch of branches) {
|
|
26
|
+
const timestamp = git(`log ${branch} --format=%ci -1`, projectRoot);
|
|
27
|
+
const log = git(`log ${branch} --oneline -1`, projectRoot);
|
|
28
|
+
|
|
29
|
+
const checkpointMatch = branch.match(/^ses\/checkpoints\/v1\/(\d{4}-\d{2}-\d{2})-([a-f0-9]+)$/);
|
|
30
|
+
if (checkpointMatch) {
|
|
31
|
+
const branchKey = `${checkpointMatch[1]}-${checkpointMatch[2]}`;
|
|
32
|
+
const message = git(`log ${branch} --format=%B -1`, projectRoot);
|
|
33
|
+
const linkedMatch = message.match(/@ ([a-f0-9]+)/);
|
|
34
|
+
const baseCommit = linkedMatch ? linkedMatch[1] : null;
|
|
35
|
+
|
|
36
|
+
if (branchKey.startsWith(checkpointId) || checkpointMatch[2].startsWith(checkpointId) || (baseCommit && baseCommit.startsWith(checkpointId))) {
|
|
37
|
+
return {
|
|
38
|
+
branch,
|
|
39
|
+
baseCommit,
|
|
40
|
+
sessionShort: checkpointMatch[2],
|
|
41
|
+
timestamp,
|
|
42
|
+
message: log.split(' ').slice(1).join(' ')
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const shadowMatch = branch.match(/^ses\/([a-f0-9]+)-([a-f0-9]+)$/);
|
|
48
|
+
if (shadowMatch) {
|
|
49
|
+
const [, baseCommit, sessionShort] = shadowMatch;
|
|
50
|
+
if (sessionShort.startsWith(checkpointId) || baseCommit.startsWith(checkpointId)) {
|
|
51
|
+
return {
|
|
52
|
+
branch,
|
|
53
|
+
baseCommit,
|
|
54
|
+
sessionShort,
|
|
55
|
+
timestamp,
|
|
56
|
+
message: log.split(' ').slice(1).join(' ')
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractSessionFromShadow(projectRoot, checkpoint) {
|
|
68
|
+
try {
|
|
69
|
+
// Get files from shadow branch
|
|
70
|
+
const files = git(`ls-tree -r --name-only ${checkpoint.branch}`, projectRoot)
|
|
71
|
+
.split('\n')
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
|
|
74
|
+
const sessionData = {};
|
|
75
|
+
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (file.startsWith('.ses-logs/')) {
|
|
78
|
+
try {
|
|
79
|
+
const content = git(`show ${checkpoint.branch}:${file}`, projectRoot);
|
|
80
|
+
sessionData[file] = content;
|
|
81
|
+
} catch {
|
|
82
|
+
// Skip files that can't be read
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return sessionData;
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function restoreSessionData(projectRoot, sessionData, newSessionId) {
|
|
94
|
+
const sesLogsDir = join(projectRoot, '.ses-logs');
|
|
95
|
+
if (!existsSync(sesLogsDir)) {
|
|
96
|
+
mkdirSync(sesLogsDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const sessionDir = join(sesLogsDir, newSessionId);
|
|
100
|
+
if (!existsSync(sessionDir)) {
|
|
101
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let restoredFiles = 0;
|
|
105
|
+
|
|
106
|
+
for (const [filePath, content] of Object.entries(sessionData)) {
|
|
107
|
+
// Extract relative path within session
|
|
108
|
+
const match = filePath.match(/^\.ses-logs\/[^/]+\/(.+)$/);
|
|
109
|
+
if (match) {
|
|
110
|
+
const relativePath = match[1];
|
|
111
|
+
const targetPath = join(sessionDir, relativePath);
|
|
112
|
+
|
|
113
|
+
// Create directory if needed
|
|
114
|
+
const targetDir = join(targetPath, '..');
|
|
115
|
+
if (!existsSync(targetDir)) {
|
|
116
|
+
mkdirSync(targetDir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
writeFileSync(targetPath, content);
|
|
120
|
+
restoredFiles++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { sessionDir, restoredFiles };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function generateSessionId() {
|
|
128
|
+
return randomUUID();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function updateSessionState(sessionDir, checkpoint) {
|
|
132
|
+
const stateFile = join(sessionDir, 'state.json');
|
|
133
|
+
|
|
134
|
+
let state = {};
|
|
135
|
+
if (existsSync(stateFile)) {
|
|
136
|
+
try {
|
|
137
|
+
state = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
138
|
+
} catch {
|
|
139
|
+
// Invalid JSON, start fresh
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Update state for resumed session
|
|
144
|
+
state.resumed_from = checkpoint.sessionShort;
|
|
145
|
+
state.resumed_at = new Date().toISOString();
|
|
146
|
+
state.original_checkpoint = checkpoint.branch;
|
|
147
|
+
|
|
148
|
+
// Reset some fields for new session
|
|
149
|
+
delete state.end_time;
|
|
150
|
+
delete state.shadow_branch;
|
|
151
|
+
|
|
152
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default async function resume(args) {
|
|
156
|
+
try {
|
|
157
|
+
const projectRoot = getProjectRoot();
|
|
158
|
+
const checkpointId = args.find(arg => !arg.startsWith('-'));
|
|
159
|
+
|
|
160
|
+
if (!checkpointId) {
|
|
161
|
+
console.error('❌ Please specify a checkpoint ID');
|
|
162
|
+
console.error(' Usage: ses resume <checkpoint-id>');
|
|
163
|
+
console.error(' Use "ses list" to see available checkpoints');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`🔍 Looking for checkpoint: ${checkpointId}`);
|
|
168
|
+
|
|
169
|
+
const checkpoint = findCheckpoint(projectRoot, checkpointId);
|
|
170
|
+
if (!checkpoint) {
|
|
171
|
+
console.error(`❌ Checkpoint not found: ${checkpointId}`);
|
|
172
|
+
console.error(' Use "ses checkpoints" to list available checkpoints');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`✅ Found checkpoint: ${checkpoint.sessionShort}`);
|
|
177
|
+
console.log(` Branch: ${checkpoint.branch}`);
|
|
178
|
+
console.log(` Base commit: ${checkpoint.baseCommit || 'unknown'}`);
|
|
179
|
+
console.log(` Created: ${new Date(checkpoint.timestamp).toLocaleString()}`);
|
|
180
|
+
console.log();
|
|
181
|
+
|
|
182
|
+
// Check if we're already at the right commit
|
|
183
|
+
const currentCommit = git('rev-parse HEAD', projectRoot);
|
|
184
|
+
if (checkpoint.baseCommit && !currentCommit.startsWith(checkpoint.baseCommit)) {
|
|
185
|
+
console.log(`🔄 Checking out base commit: ${checkpoint.baseCommit}`);
|
|
186
|
+
git(`checkout ${checkpoint.baseCommit}`, projectRoot);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Extract session data from shadow branch
|
|
190
|
+
console.log('📦 Extracting session data from shadow branch...');
|
|
191
|
+
const sessionData = extractSessionFromShadow(projectRoot, checkpoint);
|
|
192
|
+
|
|
193
|
+
if (Object.keys(sessionData).length === 0) {
|
|
194
|
+
console.error('❌ No session data found in shadow branch');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Create new session ID for resumed session
|
|
199
|
+
const newSessionId = generateSessionId();
|
|
200
|
+
console.log(`🆕 Creating new session: ${newSessionId}`);
|
|
201
|
+
|
|
202
|
+
// Restore session data
|
|
203
|
+
const { sessionDir, restoredFiles } = restoreSessionData(projectRoot, sessionData, newSessionId);
|
|
204
|
+
console.log(`✅ Restored ${restoredFiles} files to ${sessionDir}`);
|
|
205
|
+
|
|
206
|
+
// Update session state
|
|
207
|
+
updateSessionState(sessionDir, checkpoint);
|
|
208
|
+
console.log('✅ Updated session state');
|
|
209
|
+
|
|
210
|
+
console.log();
|
|
211
|
+
console.log('🎉 Session resumed successfully!');
|
|
212
|
+
console.log(` New session ID: ${newSessionId}`);
|
|
213
|
+
console.log(` Resumed from: ${checkpoint.sessionShort}`);
|
|
214
|
+
console.log();
|
|
215
|
+
console.log('💡 Next steps:');
|
|
216
|
+
console.log(' 1. Start Claude Code to continue the session');
|
|
217
|
+
console.log(' 2. Use "ses status" to verify session tracking');
|
|
218
|
+
console.log(' 3. Use "ses view" to see session history');
|
|
219
|
+
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error('❌ Failed to resume session:', error.message);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|