@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/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
+ }