@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 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: '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
- rewind: 'Rollback to previous checkpoint',
18
- resume: 'Resume session from checkpoint',
19
- doctor: 'Fix or clean stuck sessions',
20
- shadow: 'List shadow branches',
21
- clean: 'Clean old sessions',
22
- help: 'Show help',
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 # Enable for all supported agents');
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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudemini/shit-cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Session-based Hook Intelligence Tracker - CLI tool for logging Claude Code hooks",
5
5
  "type": "module",
6
6
  "bin": {