@claudemini/shit-cli 1.2.0 → 1.4.0
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/bin/shit.js +19 -16
- package/lib/checkpoint.js +16 -1
- package/lib/enable.js +46 -1
- package/lib/explain.js +207 -0
- package/lib/reset.js +130 -0
- package/lib/summarize.js +342 -0
- package/package.json +1 -1
package/bin/shit.js
CHANGED
|
@@ -4,22 +4,25 @@ const args = process.argv.slice(2);
|
|
|
4
4
|
const command = args[0];
|
|
5
5
|
|
|
6
6
|
const commands = {
|
|
7
|
-
enable:
|
|
8
|
-
disable:
|
|
9
|
-
status:
|
|
10
|
-
init:
|
|
11
|
-
log:
|
|
12
|
-
commit:
|
|
13
|
-
list:
|
|
14
|
-
checkpoints:'List all checkpoints',
|
|
15
|
-
view:
|
|
16
|
-
query:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
enable: 'Enable shit-cli in repository',
|
|
8
|
+
disable: 'Remove shit-cli hooks',
|
|
9
|
+
status: 'Show current session status',
|
|
10
|
+
init: 'Initialize hooks in .claude/settings.json',
|
|
11
|
+
log: 'Log a hook event (called by hooks)',
|
|
12
|
+
commit: 'Create checkpoint on git commit (hook)',
|
|
13
|
+
list: 'List all sessions',
|
|
14
|
+
checkpoints: 'List all checkpoints',
|
|
15
|
+
view: 'View session details',
|
|
16
|
+
query: 'Query session memory (cross-session)',
|
|
17
|
+
explain: 'Explain a session or commit',
|
|
18
|
+
summarize: 'Generate AI summary for a session',
|
|
19
|
+
rewind: 'Rollback to previous checkpoint',
|
|
20
|
+
resume: 'Resume session from checkpoint',
|
|
21
|
+
reset: 'Delete checkpoint for current HEAD',
|
|
22
|
+
doctor: 'Fix or clean stuck sessions',
|
|
23
|
+
shadow: 'List shadow branches',
|
|
24
|
+
clean: 'Clean old sessions',
|
|
25
|
+
help: 'Show help',
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
function showHelp() {
|
package/lib/checkpoint.js
CHANGED
|
@@ -139,7 +139,9 @@ function buildTree(cwd, dirPath, redact = true) {
|
|
|
139
139
|
* Commit session checkpoint to shadow branch
|
|
140
140
|
* Branch format: shit/checkpoints/v1/YYYY-MM-DD-<session-id>
|
|
141
141
|
*/
|
|
142
|
-
export async function commitCheckpoint(projectRoot, sessionDir, sessionId, commitSha = null) {
|
|
142
|
+
export async function commitCheckpoint(projectRoot, sessionDir, sessionId, commitSha = null, options = {}) {
|
|
143
|
+
const autoSummarize = options.autoSummarize !== false; // default true
|
|
144
|
+
|
|
143
145
|
// Verify we're in a git repo
|
|
144
146
|
try {
|
|
145
147
|
git('rev-parse --git-dir', projectRoot);
|
|
@@ -212,6 +214,19 @@ export async function commitCheckpoint(projectRoot, sessionDir, sessionId, commi
|
|
|
212
214
|
commit: commitHash,
|
|
213
215
|
linked_commit: linkedCommit,
|
|
214
216
|
};
|
|
217
|
+
|
|
218
|
+
// Auto-summarize if enabled
|
|
219
|
+
if (autoSummarize) {
|
|
220
|
+
try {
|
|
221
|
+
const { summarizeSession } = await import('./summarize.js');
|
|
222
|
+
const summaryResult = await summarizeSession(projectRoot, sessionId, sessionDir);
|
|
223
|
+
if (summaryResult.success) {
|
|
224
|
+
console.log(`✅ AI summary generated: ${summaryResult.model}`);
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// Best effort - summarize is optional
|
|
228
|
+
}
|
|
229
|
+
}
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
/**
|
package/lib/enable.js
CHANGED
|
@@ -169,6 +169,11 @@ export default async function enable(args) {
|
|
|
169
169
|
// Parse arguments
|
|
170
170
|
const agents = [];
|
|
171
171
|
let addCheckpointHook = false;
|
|
172
|
+
let useLocal = false;
|
|
173
|
+
let force = false;
|
|
174
|
+
let pushSessions = true;
|
|
175
|
+
let telemetry = true;
|
|
176
|
+
let summarize = true;
|
|
172
177
|
|
|
173
178
|
for (const arg of args) {
|
|
174
179
|
if (arg === '--all') {
|
|
@@ -176,6 +181,20 @@ export default async function enable(args) {
|
|
|
176
181
|
agents.push(...Object.keys(AGENT_HOOKS));
|
|
177
182
|
} else if (arg === '--checkpoint' || arg === '-c') {
|
|
178
183
|
addCheckpointHook = true;
|
|
184
|
+
} else if (arg === '--local') {
|
|
185
|
+
useLocal = true;
|
|
186
|
+
} else if (arg === '--project') {
|
|
187
|
+
useLocal = false; // Force project-level settings
|
|
188
|
+
} else if (arg === '--force' || arg === '-f') {
|
|
189
|
+
force = true;
|
|
190
|
+
} else if (arg === '--skip-push-sessions') {
|
|
191
|
+
pushSessions = false;
|
|
192
|
+
} else if (arg === '--no-summarize') {
|
|
193
|
+
summarize = false;
|
|
194
|
+
} else if (arg.startsWith('--telemetry=')) {
|
|
195
|
+
telemetry = arg.split('=')[1] !== 'false';
|
|
196
|
+
} else if (arg === '--telemetry') {
|
|
197
|
+
telemetry = true;
|
|
179
198
|
} else if (!arg.startsWith('-')) {
|
|
180
199
|
// Assume it's an agent name
|
|
181
200
|
if (AGENT_HOOKS[arg]) {
|
|
@@ -193,6 +212,27 @@ export default async function enable(args) {
|
|
|
193
212
|
|
|
194
213
|
console.log('🔧 Enabling shit-cli in repository...\n');
|
|
195
214
|
|
|
215
|
+
// Write configuration
|
|
216
|
+
const configData = {
|
|
217
|
+
enabled: true,
|
|
218
|
+
push_sessions: pushSessions,
|
|
219
|
+
summarize: summarize,
|
|
220
|
+
telemetry: telemetry,
|
|
221
|
+
log_level: 'info'
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const settingsFileName = useLocal ? 'settings.local.json' : 'settings.json';
|
|
225
|
+
|
|
226
|
+
// Write to .claude/ directory (project or local)
|
|
227
|
+
const configDir = join(projectRoot, '.claude');
|
|
228
|
+
if (!existsSync(configDir)) {
|
|
229
|
+
mkdirSync(configDir, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const configFile = join(configDir, settingsFileName);
|
|
233
|
+
writeFileSync(configFile, JSON.stringify(configData, null, 2));
|
|
234
|
+
console.log(`✅ Wrote configuration: ${configFile}`);
|
|
235
|
+
|
|
196
236
|
// Setup hooks for each agent
|
|
197
237
|
for (const agent of agents) {
|
|
198
238
|
const settingsFile = setupAgentHooks(projectRoot, agent);
|
|
@@ -228,8 +268,13 @@ export default async function enable(args) {
|
|
|
228
268
|
console.log(' shit checkpoints # List all checkpoints');
|
|
229
269
|
console.log(' shit commit # Manually create checkpoint after git commit');
|
|
230
270
|
console.log('\nOptions:');
|
|
231
|
-
console.log(' --all
|
|
271
|
+
console.log(' --all # Enable for all supported agents');
|
|
232
272
|
console.log(' --checkpoint, -c # Enable automatic checkpoint on git commit');
|
|
273
|
+
console.log(' --no-summarize # Disable AI summary generation');
|
|
274
|
+
console.log(' --skip-push-sessions # Disable auto-push to remote');
|
|
275
|
+
console.log(' --telemetry=false # Disable anonymous telemetry');
|
|
276
|
+
console.log('\nAI Summary:');
|
|
277
|
+
console.log(' Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI summaries');
|
|
233
278
|
|
|
234
279
|
} catch (error) {
|
|
235
280
|
console.error('❌ Failed to enable shit-cli:', error.message);
|
package/lib/explain.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Explain a session or checkpoint
|
|
5
|
+
* Provides human-readable explanation of what happened in a session
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
function findProjectRoot() {
|
|
13
|
+
let dir = process.cwd();
|
|
14
|
+
while (dir !== '/') {
|
|
15
|
+
if (existsSync(join(dir, '.git'))) {
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
dir = join(dir, '..');
|
|
19
|
+
}
|
|
20
|
+
throw new Error('Not in a git repository');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getSessionData(sessionId, projectRoot) {
|
|
24
|
+
const sessionDir = join(projectRoot, '.shit-logs', sessionId);
|
|
25
|
+
|
|
26
|
+
if (!existsSync(sessionDir)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const files = {};
|
|
31
|
+
const fileNames = ['summary.json', 'summary.txt', 'context.md', 'metadata.json'];
|
|
32
|
+
|
|
33
|
+
for (const file of fileNames) {
|
|
34
|
+
const filePath = join(sessionDir, file);
|
|
35
|
+
if (existsSync(filePath)) {
|
|
36
|
+
files[file] = readFileSync(filePath, 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { dir: sessionDir, files };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function explainFromSummary(summaryText) {
|
|
44
|
+
const lines = summaryText.split('\n');
|
|
45
|
+
const explanation = [];
|
|
46
|
+
|
|
47
|
+
// Extract key information
|
|
48
|
+
const typeMatch = summaryText.match(/Type:\s*(\w+)/i);
|
|
49
|
+
const riskMatch = summaryText.match(/Risk:\s*(\w+)/i);
|
|
50
|
+
const intentMatch = summaryText.match(/Goal:\s*(.+)/i);
|
|
51
|
+
const durationMatch = summaryText.match(/Duration:\s*(\d+)min/i);
|
|
52
|
+
|
|
53
|
+
if (typeMatch) {
|
|
54
|
+
explanation.push(`📋 Session type: **${typeMatch[1]}**`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (riskMatch) {
|
|
58
|
+
const risk = riskMatch[1].toLowerCase();
|
|
59
|
+
const emoji = risk === 'high' ? '🔴' : risk === 'medium' ? '🟡' : '🟢';
|
|
60
|
+
explanation.push(`${emoji} Risk level: **${risk}**`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (intentMatch) {
|
|
64
|
+
explanation.push(`🎯 Intent: ${intentMatch[1].slice(0, 100)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (durationMatch) {
|
|
68
|
+
explanation.push(`⏱️ Duration: ${durationMatch[1]} minutes`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract files changed
|
|
72
|
+
const changesMatch = summaryText.match(/## Changes\n([\s\S]+?)##/);
|
|
73
|
+
if (changesMatch) {
|
|
74
|
+
const fileLines = changesMatch[1].split('\n').filter(l => l.trim().startsWith('['));
|
|
75
|
+
if (fileLines.length > 0) {
|
|
76
|
+
explanation.push(`\n📝 Files changed (${fileLines.length}):`);
|
|
77
|
+
fileLines.slice(0, 5).forEach(line => {
|
|
78
|
+
explanation.push(` ${line.trim()}`);
|
|
79
|
+
});
|
|
80
|
+
if (fileLines.length > 5) {
|
|
81
|
+
explanation.push(` ... and ${fileLines.length - 5} more`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Extract tools used
|
|
87
|
+
const toolsMatch = summaryText.match(/## Tools\n([\s\S]+?)##/);
|
|
88
|
+
if (toolsMatch) {
|
|
89
|
+
const toolLines = toolsMatch[1].split('\n').filter(l => l.includes(':'));
|
|
90
|
+
if (toolLines.length > 0) {
|
|
91
|
+
explanation.push(`\n🔧 Tools used:`);
|
|
92
|
+
toolLines.slice(0, 5).forEach(line => {
|
|
93
|
+
explanation.push(` ${line.trim()}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Extract errors
|
|
99
|
+
if (summaryText.includes('## Errors')) {
|
|
100
|
+
const errorsMatch = summaryText.match(/## Errors\n([\s\S]+)/);
|
|
101
|
+
if (errorsMatch && errorsMatch[1].trim()) {
|
|
102
|
+
explanation.push(`\n⚠️ Errors encountered:`);
|
|
103
|
+
const errorLines = errorsMatch[1].split('\n').filter(l => l.trim());
|
|
104
|
+
errorLines.slice(0, 3).forEach(line => {
|
|
105
|
+
explanation.push(` ${line.trim()}`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return explanation.join('\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function explainFromCommit(commitSha, projectRoot) {
|
|
114
|
+
try {
|
|
115
|
+
// Try to find associated checkpoint
|
|
116
|
+
const checkpoints = execSync('git branch --list "shit/checkpoints/v1/*"', {
|
|
117
|
+
cwd: projectRoot,
|
|
118
|
+
encoding: 'utf-8'
|
|
119
|
+
}).split('\n').filter(Boolean);
|
|
120
|
+
|
|
121
|
+
for (const branch of checkpoints) {
|
|
122
|
+
try {
|
|
123
|
+
const log = execSync(`git log ${branch} --oneline -1`, {
|
|
124
|
+
cwd: projectRoot,
|
|
125
|
+
encoding: 'utf-8'
|
|
126
|
+
}).trim();
|
|
127
|
+
|
|
128
|
+
if (log.includes(commitSha.slice(0, 12))) {
|
|
129
|
+
const message = execSync(`git log ${branch} --format=%B -1`, {
|
|
130
|
+
cwd: projectRoot,
|
|
131
|
+
encoding: 'utf-8'
|
|
132
|
+
}).trim();
|
|
133
|
+
|
|
134
|
+
return `📸 This commit has an associated checkpoint:\n\nBranch: ${branch}\n\n${message}`;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Skip this branch
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return `No checkpoint found for commit ${commitSha}`;
|
|
142
|
+
} catch {
|
|
143
|
+
return 'No checkpoint information available';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default async function explain(args) {
|
|
148
|
+
try {
|
|
149
|
+
const projectRoot = findProjectRoot();
|
|
150
|
+
const target = args[0];
|
|
151
|
+
|
|
152
|
+
if (!target) {
|
|
153
|
+
console.log('Usage: shit explain <session-id | commit-sha>');
|
|
154
|
+
console.log('\nExamples:');
|
|
155
|
+
console.log(' shit explain 2026-02-28-abc12345 # Explain a session');
|
|
156
|
+
console.log(' shit explain abc1234 # Explain a commit');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`🔍 Explaining: ${target}\n`);
|
|
161
|
+
|
|
162
|
+
// Try to find session first
|
|
163
|
+
const sessionData = getSessionData(target, projectRoot);
|
|
164
|
+
|
|
165
|
+
if (sessionData && sessionData.files['summary.txt']) {
|
|
166
|
+
console.log('📋 Session Explanation\n');
|
|
167
|
+
console.log(explainFromSummary(sessionData.files['summary.txt']));
|
|
168
|
+
console.log('\n---\n💡 Use "shit view ' + target + '" for full details');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Try as commit
|
|
173
|
+
try {
|
|
174
|
+
execSync('git rev-parse --verify ' + target + '^{commit}', {
|
|
175
|
+
cwd: projectRoot,
|
|
176
|
+
stdio: 'ignore'
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
console.log('📸 Commit Explanation\n');
|
|
180
|
+
console.log(explainFromCommit(target, projectRoot));
|
|
181
|
+
return;
|
|
182
|
+
} catch {
|
|
183
|
+
// Not a commit
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Try partial match
|
|
187
|
+
const sessions = execSync('ls .shit-logs', { cwd: projectRoot, encoding: 'utf-8' })
|
|
188
|
+
.split('\n')
|
|
189
|
+
.filter(s => s.includes(target));
|
|
190
|
+
|
|
191
|
+
if (sessions.length > 0) {
|
|
192
|
+
const matchedSession = sessions[0];
|
|
193
|
+
const data = getSessionData(matchedSession, projectRoot);
|
|
194
|
+
if (data && data.files['summary.txt']) {
|
|
195
|
+
console.log('📋 Found session: ' + matchedSession + '\n');
|
|
196
|
+
console.log(explainFromSummary(data.files['summary.txt']));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`❌ Could not find session or commit: ${target}`);
|
|
202
|
+
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('❌ Failed to explain:', error.message);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
package/lib/reset.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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, rmSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
function findProjectRoot() {
|
|
13
|
+
let dir = process.cwd();
|
|
14
|
+
while (dir !== '/') {
|
|
15
|
+
if (existsSync(join(dir, '.git'))) {
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
dir = join(dir, '..');
|
|
19
|
+
}
|
|
20
|
+
throw new Error('Not in a git repository');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function git(cmd, cwd) {
|
|
24
|
+
return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findCheckpointForCommit(projectRoot, commitSha) {
|
|
28
|
+
try {
|
|
29
|
+
const branches = git('branch --list "shit/checkpoints/v1/*"', projectRoot)
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map(b => b.trim().replace(/^\*?\s*/, ''))
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
|
|
34
|
+
for (const branch of branches) {
|
|
35
|
+
try {
|
|
36
|
+
const log = git(`log ${branch} --oneline -1`, projectRoot);
|
|
37
|
+
if (log.includes(commitSha.slice(0, 12))) {
|
|
38
|
+
return branch;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Skip
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findActiveSession(projectRoot) {
|
|
51
|
+
const shitLogsDir = join(projectRoot, '.shit-logs');
|
|
52
|
+
if (!existsSync(shitLogsDir)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { readdirSync, statSync } = require('fs');
|
|
57
|
+
const sessions = readdirSync(shitLogsDir)
|
|
58
|
+
.filter(name => name.match(/^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/))
|
|
59
|
+
.map(name => ({
|
|
60
|
+
name,
|
|
61
|
+
path: join(shitLogsDir, name),
|
|
62
|
+
mtime: statSync(join(shitLogsDir, name)).mtime
|
|
63
|
+
}))
|
|
64
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
65
|
+
|
|
66
|
+
return sessions.length > 0 ? sessions[0] : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default async function reset(args) {
|
|
70
|
+
try {
|
|
71
|
+
const projectRoot = findProjectRoot();
|
|
72
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
73
|
+
|
|
74
|
+
// Get current commit
|
|
75
|
+
const commitSha = git('rev-parse HEAD', projectRoot);
|
|
76
|
+
const commitShort = commitSha.slice(0, 12);
|
|
77
|
+
|
|
78
|
+
console.log(`🔄 Resetting checkpoint for commit: ${commitShort}\n`);
|
|
79
|
+
|
|
80
|
+
// Find checkpoint for this commit
|
|
81
|
+
const branch = findCheckpointForCommit(projectRoot, commitSha);
|
|
82
|
+
|
|
83
|
+
if (!branch) {
|
|
84
|
+
console.log('ℹ️ No checkpoint found for current commit.');
|
|
85
|
+
console.log(' Checkpoints are created on git commit, not automatically on session start.');
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`📸 Found checkpoint branch: ${branch}`);
|
|
90
|
+
|
|
91
|
+
if (!force) {
|
|
92
|
+
console.log('\n⚠️ This will delete the checkpoint and its branch.');
|
|
93
|
+
console.log(' Use --force to proceed without confirmation.');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Delete the checkpoint branch
|
|
98
|
+
try {
|
|
99
|
+
git(`branch -D ${branch}`, projectRoot);
|
|
100
|
+
console.log(`✅ Deleted checkpoint branch: ${branch}`);
|
|
101
|
+
} catch {
|
|
102
|
+
console.log('ℹ️ Branch already deleted or not found');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Optionally clean active session if it's linked to this commit
|
|
106
|
+
const activeSession = findActiveSession(projectRoot);
|
|
107
|
+
if (activeSession) {
|
|
108
|
+
const stateFile = join(activeSession.path, 'state.json');
|
|
109
|
+
if (existsSync(stateFile)) {
|
|
110
|
+
try {
|
|
111
|
+
const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
112
|
+
if (state.checkpoints && state.checkpoints.some(cp => cp.linked_commit === commitSha)) {
|
|
113
|
+
// Remove checkpoint references
|
|
114
|
+
state.checkpoints = state.checkpoints.filter(cp => cp.linked_commit !== commitSha);
|
|
115
|
+
require('fs').writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
116
|
+
console.log('✅ Updated session state');
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Best effort
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('\n✅ Reset complete!');
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('❌ Failed to reset:', error.message);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
package/lib/summarize.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI Summarization module
|
|
5
|
+
* Automatically generates AI-powered summaries of sessions using LLM APIs
|
|
6
|
+
* Supports OpenAI and Anthropic APIs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
|
|
13
|
+
// Default configuration
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
provider: 'openai', // or 'anthropic'
|
|
16
|
+
model: 'gpt-4o-mini',
|
|
17
|
+
max_tokens: 1000,
|
|
18
|
+
temperature: 0.7,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get API configuration from environment or config file
|
|
23
|
+
*/
|
|
24
|
+
function getApiConfig(projectRoot) {
|
|
25
|
+
// Check for environment variables first
|
|
26
|
+
const config = { ...DEFAULT_CONFIG };
|
|
27
|
+
|
|
28
|
+
// OpenAI
|
|
29
|
+
if (process.env.OPENAI_API_KEY) {
|
|
30
|
+
config.provider = 'openai';
|
|
31
|
+
config.api_key = process.env.OPENAI_API_KEY;
|
|
32
|
+
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
33
|
+
config.provider = 'anthropic';
|
|
34
|
+
config.api_key = process.env.ANTHROPIC_API_KEY;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check for project config
|
|
38
|
+
const configFile = join(projectRoot, '.shit-logs', 'config.json');
|
|
39
|
+
if (existsSync(configFile)) {
|
|
40
|
+
try {
|
|
41
|
+
const fileConfig = JSON.parse(readFileSync(configFile, 'utf-8'));
|
|
42
|
+
Object.assign(config, fileConfig);
|
|
43
|
+
} catch {
|
|
44
|
+
// Use defaults
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract relevant context from session for summarization
|
|
53
|
+
*/
|
|
54
|
+
function extractContext(sessionDir) {
|
|
55
|
+
const context = {
|
|
56
|
+
prompts: [],
|
|
57
|
+
changes: [],
|
|
58
|
+
tools: {},
|
|
59
|
+
errors: [],
|
|
60
|
+
summary: null,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Read summary.json
|
|
64
|
+
const summaryFile = join(sessionDir, 'summary.json');
|
|
65
|
+
if (existsSync(summaryFile)) {
|
|
66
|
+
try {
|
|
67
|
+
const summary = JSON.parse(readFileSync(summaryFile, 'utf-8'));
|
|
68
|
+
context.summary = summary;
|
|
69
|
+
context.prompts = summary.prompts || [];
|
|
70
|
+
context.tools = summary.activity?.tools || {};
|
|
71
|
+
context.errors = summary.activity?.errors || [];
|
|
72
|
+
context.changes = summary.changes?.files || [];
|
|
73
|
+
} catch {
|
|
74
|
+
// Best effort
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Read prompts.txt
|
|
79
|
+
const promptsFile = join(sessionDir, 'prompts.txt');
|
|
80
|
+
if (existsSync(promptsFile)) {
|
|
81
|
+
try {
|
|
82
|
+
context.prompts_text = readFileSync(promptsFile, 'utf-8').slice(0, 3000);
|
|
83
|
+
} catch {
|
|
84
|
+
// Best effort
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Read context.md
|
|
89
|
+
const contextFile = join(sessionDir, 'context.md');
|
|
90
|
+
if (existsSync(contextFile)) {
|
|
91
|
+
try {
|
|
92
|
+
context.context_md = readFileSync(contextFile, 'utf-8').slice(0, 2000);
|
|
93
|
+
} catch {
|
|
94
|
+
// Best effort
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return context;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build prompt for LLM summarization
|
|
103
|
+
*/
|
|
104
|
+
function buildSummarizePrompt(context) {
|
|
105
|
+
const parts = [];
|
|
106
|
+
|
|
107
|
+
// System prompt
|
|
108
|
+
parts.push(`You are a helpful assistant that summarizes AI coding sessions. Generate a concise summary that explains:`);
|
|
109
|
+
parts.push(`1. What the user wanted to accomplish`);
|
|
110
|
+
parts.push(`2. What changes were made`);
|
|
111
|
+
parts.push(`3. Any issues or errors encountered`);
|
|
112
|
+
parts.push(`4. Overall outcome`);
|
|
113
|
+
|
|
114
|
+
parts.push(`\n---\n`);
|
|
115
|
+
|
|
116
|
+
// User prompts
|
|
117
|
+
if (context.prompts_text) {
|
|
118
|
+
parts.push(`## User Prompts\n${context.prompts_text}\n`);
|
|
119
|
+
} else if (context.prompts && context.prompts.length > 0) {
|
|
120
|
+
parts.push(`## User Prompts\n`);
|
|
121
|
+
context.prompts.slice(0, 5).forEach(p => {
|
|
122
|
+
const text = typeof p === 'string' ? p : p.text || '';
|
|
123
|
+
parts.push(`- ${text.slice(0, 200)}`);
|
|
124
|
+
});
|
|
125
|
+
parts.push('');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Changes summary
|
|
129
|
+
if (context.changes && context.changes.length > 0) {
|
|
130
|
+
parts.push(`## Files Changed\n`);
|
|
131
|
+
context.changes.slice(0, 10).forEach(f => {
|
|
132
|
+
const ops = f.operations?.join(', ') || 'modified';
|
|
133
|
+
parts.push(`- ${f.path}: ${ops}`);
|
|
134
|
+
});
|
|
135
|
+
parts.push('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Tool usage
|
|
139
|
+
if (context.tools && Object.keys(context.tools).length > 0) {
|
|
140
|
+
parts.push(`## Tools Used\n`);
|
|
141
|
+
Object.entries(context.tools).forEach(([tool, count]) => {
|
|
142
|
+
parts.push(`- ${tool}: ${count} times`);
|
|
143
|
+
});
|
|
144
|
+
parts.push('');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Errors
|
|
148
|
+
if (context.errors && context.errors.length > 0) {
|
|
149
|
+
parts.push(`## Errors\n`);
|
|
150
|
+
context.errors.slice(0, 5).forEach(e => {
|
|
151
|
+
parts.push(`- ${e.tool}: ${(e.message || '').slice(0, 100)}`);
|
|
152
|
+
});
|
|
153
|
+
parts.push('');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return parts.join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Call OpenAI API
|
|
161
|
+
*/
|
|
162
|
+
async function callOpenAI(apiKey, model, prompt, maxTokens, temperature) {
|
|
163
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
model,
|
|
171
|
+
messages: [
|
|
172
|
+
{ role: 'system', content: 'You are a helpful assistant that summarizes AI coding sessions.' },
|
|
173
|
+
{ role: 'user', content: prompt }
|
|
174
|
+
],
|
|
175
|
+
max_tokens: maxTokens,
|
|
176
|
+
temperature,
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
const error = await response.text();
|
|
182
|
+
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const data = await response.json();
|
|
186
|
+
return data.choices[0].message.content;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Call Anthropic API
|
|
191
|
+
*/
|
|
192
|
+
async function callAnthropic(apiKey, model, prompt, maxTokens, temperature) {
|
|
193
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
'x-api-key': apiKey,
|
|
198
|
+
'anthropic-version': '2023-06-01',
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
model,
|
|
202
|
+
max_tokens: maxTokens,
|
|
203
|
+
temperature,
|
|
204
|
+
messages: [
|
|
205
|
+
{ role: 'user', content: prompt }
|
|
206
|
+
],
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
const error = await response.text();
|
|
212
|
+
throw new Error(`Anthropic API error: ${response.status} - ${error}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
return data.content[0].text;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Generate AI summary for a session
|
|
221
|
+
*/
|
|
222
|
+
export async function summarizeSession(projectRoot, sessionId, sessionDir) {
|
|
223
|
+
const config = getApiConfig(projectRoot);
|
|
224
|
+
|
|
225
|
+
if (!config.api_key) {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
reason: 'No API key configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable.'
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Extract context from session
|
|
233
|
+
const context = extractContext(sessionDir);
|
|
234
|
+
|
|
235
|
+
// Build prompt
|
|
236
|
+
const prompt = buildSummarizePrompt(context);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
let summary;
|
|
240
|
+
|
|
241
|
+
if (config.provider === 'anthropic') {
|
|
242
|
+
summary = await callAnthropic(
|
|
243
|
+
config.api_key,
|
|
244
|
+
config.model || 'claude-3-haiku-20240307',
|
|
245
|
+
prompt,
|
|
246
|
+
config.max_tokens,
|
|
247
|
+
config.temperature
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
summary = await callOpenAI(
|
|
251
|
+
config.api_key,
|
|
252
|
+
config.model || 'gpt-4o-mini',
|
|
253
|
+
prompt,
|
|
254
|
+
config.max_tokens,
|
|
255
|
+
config.temperature
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Save summary
|
|
260
|
+
const aiSummaryFile = join(sessionDir, 'ai-summary.md');
|
|
261
|
+
writeFileSync(aiSummaryFile, summary);
|
|
262
|
+
|
|
263
|
+
// Update state
|
|
264
|
+
const stateFile = join(sessionDir, 'state.json');
|
|
265
|
+
if (existsSync(stateFile)) {
|
|
266
|
+
try {
|
|
267
|
+
const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
268
|
+
state.ai_summary = {
|
|
269
|
+
provider: config.provider,
|
|
270
|
+
model: config.model,
|
|
271
|
+
generated_at: new Date().toISOString(),
|
|
272
|
+
};
|
|
273
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
274
|
+
} catch {
|
|
275
|
+
// Best effort
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
success: true,
|
|
281
|
+
summary,
|
|
282
|
+
provider: config.provider,
|
|
283
|
+
model: config.model,
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
reason: error.message
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* CLI command for manual summarization
|
|
295
|
+
*/
|
|
296
|
+
export default async function summarize(args) {
|
|
297
|
+
const projectRoot = findProjectRoot();
|
|
298
|
+
const sessionId = args[0];
|
|
299
|
+
|
|
300
|
+
if (!sessionId) {
|
|
301
|
+
console.log('Usage: shit summarize <session-id>');
|
|
302
|
+
console.log('\nEnvironment variables:');
|
|
303
|
+
console.log(' OPENAI_API_KEY # Use OpenAI for summarization');
|
|
304
|
+
console.log(' ANTHROPIC_API_KEY # Use Anthropic for summarization');
|
|
305
|
+
console.log('\nConfiguration (.shit-logs/config.json):');
|
|
306
|
+
console.log(` {"provider": "openai", "model": "gpt-4o-mini"}`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const sessionDir = join(projectRoot, '.shit-logs', sessionId);
|
|
311
|
+
|
|
312
|
+
if (!existsSync(sessionDir)) {
|
|
313
|
+
console.error(`Session not found: ${sessionId}`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(`🤖 Generating AI summary for session: ${sessionId}\n`);
|
|
318
|
+
|
|
319
|
+
const result = await summarizeSession(projectRoot, sessionId, sessionDir);
|
|
320
|
+
|
|
321
|
+
if (result.success) {
|
|
322
|
+
console.log('✅ AI Summary generated!\n');
|
|
323
|
+
console.log(result.summary);
|
|
324
|
+
console.log(`\n---`);
|
|
325
|
+
console.log(`Provider: ${result.provider}`);
|
|
326
|
+
console.log(`Model: ${result.model}`);
|
|
327
|
+
} else {
|
|
328
|
+
console.error('❌ Failed to generate summary:', result.reason);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function findProjectRoot() {
|
|
334
|
+
let dir = process.cwd();
|
|
335
|
+
while (dir !== '/') {
|
|
336
|
+
if (existsSync(join(dir, '.git'))) {
|
|
337
|
+
return dir;
|
|
338
|
+
}
|
|
339
|
+
dir = join(dir, '..');
|
|
340
|
+
}
|
|
341
|
+
throw new Error('Not in a git repository');
|
|
342
|
+
}
|