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