@claudemini/shit-cli 1.3.0 → 1.5.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/lib/rewind.js CHANGED
@@ -5,54 +5,71 @@
5
5
  * Similar to 'entire rewind' - rollback to known good state
6
6
  */
7
7
 
8
- import { existsSync, readFileSync, writeFileSync } from 'fs';
9
- import { join } from 'path';
10
8
  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
- }
9
+ import { getProjectRoot } from './config.js';
22
10
 
23
11
  function git(cmd, cwd) {
24
12
  return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 10000 }).trim();
25
13
  }
26
14
 
15
+ function parseCheckpointRef(projectRoot, branch) {
16
+ const shadowMatch = branch.match(/^shit\/([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(/^shit\/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
+
27
43
  function listCheckpoints(projectRoot) {
28
44
  try {
29
- // Get shadow branches
30
- const branches = git('branch --list "shit/*"', projectRoot)
45
+ const branches = git('branch --list "shit/checkpoints/v1/*" "shit/*"', projectRoot)
31
46
  .split('\n')
32
47
  .map(b => b.trim().replace(/^\*?\s*/, ''))
33
48
  .filter(Boolean);
49
+ const uniqueBranches = [...new Set(branches)];
34
50
 
35
51
  const checkpoints = [];
36
52
 
37
- for (const branch of branches) {
53
+ for (const branch of uniqueBranches) {
38
54
  try {
55
+ const parsed = parseCheckpointRef(projectRoot, branch);
56
+ if (!parsed) {
57
+ continue;
58
+ }
59
+
39
60
  const log = git(`log ${branch} --oneline -1`, projectRoot);
40
61
  const [commit, ...messageParts] = log.split(' ');
41
62
  const message = messageParts.join(' ');
42
-
43
- // Extract session info from branch name: shit/<commit>-<session>
44
- const match = branch.match(/^shit\/([a-f0-9]+)-([a-f0-9]+)$/);
45
- if (match) {
46
- const [, baseCommit, sessionShort] = match;
47
- checkpoints.push({
48
- branch,
49
- commit,
50
- baseCommit,
51
- sessionShort,
52
- message,
53
- timestamp: git(`log ${branch} --format=%ci -1`, projectRoot)
54
- });
55
- }
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
+ });
56
73
  } catch {
57
74
  // Skip invalid branches
58
75
  }
@@ -84,6 +101,10 @@ function hasUncommittedChanges(projectRoot) {
84
101
  function rewindToCheckpoint(projectRoot, checkpoint, force = false) {
85
102
  const currentCommit = getCurrentCommit(projectRoot);
86
103
 
104
+ if (!checkpoint.baseCommit) {
105
+ throw new Error(`Checkpoint ${checkpoint.lookupKey || checkpoint.sessionShort} is missing a linked base commit`);
106
+ }
107
+
87
108
  if (!force && hasUncommittedChanges(projectRoot)) {
88
109
  throw new Error('Working directory has uncommitted changes. Use --force to override or commit changes first.');
89
110
  }
@@ -100,7 +121,7 @@ function rewindToCheckpoint(projectRoot, checkpoint, force = false) {
100
121
 
101
122
  export default async function rewind(args) {
102
123
  try {
103
- const projectRoot = findProjectRoot();
124
+ const projectRoot = getProjectRoot();
104
125
  const force = args.includes('--force') || args.includes('-f');
105
126
  const interactive = args.includes('--interactive') || args.includes('-i');
106
127
 
@@ -112,7 +133,7 @@ export default async function rewind(args) {
112
133
 
113
134
  if (checkpoints.length === 0) {
114
135
  console.log('❌ No checkpoints found');
115
- console.log(' Checkpoints are created automatically when sessions end.');
136
+ console.log(' Checkpoints are created when you run "shit commit".');
116
137
  process.exit(1);
117
138
  }
118
139
 
@@ -120,7 +141,8 @@ export default async function rewind(args) {
120
141
  // Find specific checkpoint
121
142
  targetCheckpoint = checkpoints.find(cp =>
122
143
  cp.sessionShort.startsWith(checkpointArg) ||
123
- cp.baseCommit.startsWith(checkpointArg)
144
+ cp.lookupKey.startsWith(checkpointArg) ||
145
+ (cp.baseCommit && cp.baseCommit.startsWith(checkpointArg))
124
146
  );
125
147
 
126
148
  if (!targetCheckpoint) {
@@ -132,22 +154,25 @@ export default async function rewind(args) {
132
154
  console.log('📋 Available checkpoints:\n');
133
155
  checkpoints.forEach((cp, i) => {
134
156
  const date = new Date(cp.timestamp).toLocaleString();
135
- console.log(`${i + 1}. ${cp.sessionShort} (${cp.baseCommit.slice(0, 7)}) - ${date}`);
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}`);
136
160
  console.log(` ${cp.message}`);
137
161
  console.log();
138
162
  });
139
163
 
140
164
  // For now, just use the most recent
141
165
  targetCheckpoint = checkpoints[0];
142
- console.log(`Using most recent checkpoint: ${targetCheckpoint.sessionShort}`);
166
+ console.log(`Using most recent checkpoint: ${targetCheckpoint.lookupKey || targetCheckpoint.sessionShort}`);
143
167
  } else {
144
168
  // Use most recent checkpoint
145
169
  targetCheckpoint = checkpoints[0];
146
170
  }
147
171
 
148
- console.log(`🔄 Rewinding to checkpoint: ${targetCheckpoint.sessionShort}`);
172
+ const selectedKey = targetCheckpoint.lookupKey || targetCheckpoint.sessionShort;
173
+ console.log(`🔄 Rewinding to checkpoint: ${selectedKey}`);
149
174
  console.log(` Branch: ${targetCheckpoint.branch}`);
150
- console.log(` Base commit: ${targetCheckpoint.baseCommit}`);
175
+ console.log(` Base commit: ${targetCheckpoint.baseCommit || 'unknown'}`);
151
176
  console.log(` Created: ${new Date(targetCheckpoint.timestamp).toLocaleString()}`);
152
177
  console.log();
153
178
 
@@ -164,7 +189,7 @@ export default async function rewind(args) {
164
189
  console.log(` git reset --hard ${previousCommit}`);
165
190
  console.log();
166
191
  console.log('💡 To resume from this checkpoint:');
167
- console.log(` shit resume ${targetCheckpoint.sessionShort}`);
192
+ console.log(` shit resume ${selectedKey}`);
168
193
 
169
194
  } catch (error) {
170
195
  console.error('❌ Failed to rewind:', error.message);
package/lib/session.js CHANGED
@@ -105,6 +105,12 @@ export function processEvent(state, event, hookType, projectRoot) {
105
105
  return;
106
106
  }
107
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
+
108
114
  // Tool events
109
115
  if (hookType === 'post-tool-use' && toolName) {
110
116
  state.tool_counts[toolName] = (state.tool_counts[toolName] || 0) + 1;
package/lib/status.js CHANGED
@@ -5,20 +5,10 @@
5
5
  * Similar to 'entire status' - displays active session info
6
6
  */
7
7
 
8
- import { existsSync, readFileSync } from 'fs';
8
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
9
9
  import { join } from 'path';
10
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
- }
11
+ import { getProjectRoot, SESSION_ID_REGEX } from './config.js';
22
12
 
23
13
  function getCurrentSession(projectRoot) {
24
14
  const shitLogsDir = join(projectRoot, '.shit-logs');
@@ -27,9 +17,11 @@ function getCurrentSession(projectRoot) {
27
17
  }
28
18
 
29
19
  // Find the most recent session directory
30
- const { readdirSync, statSync } = await import('fs');
31
20
  const sessions = readdirSync(shitLogsDir)
32
- .filter(name => name.match(/^\d{4}-\d{2}-\d{2}-[a-f0-9-]+$/)) // session format
21
+ .filter(name => {
22
+ const fullPath = join(shitLogsDir, name);
23
+ return SESSION_ID_REGEX.test(name) && statSync(fullPath).isDirectory();
24
+ })
33
25
  .map(name => ({
34
26
  name,
35
27
  path: join(shitLogsDir, name),
@@ -90,11 +82,45 @@ function formatDuration(startTime) {
90
82
  }
91
83
  }
92
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 hasShitHooks(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('shit 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('shit log'))
115
+ );
116
+ });
117
+ }
118
+
93
119
  export default async function status(args) {
94
120
  try {
95
- const projectRoot = findProjectRoot();
121
+ const projectRoot = getProjectRoot();
96
122
  const gitInfo = getGitInfo(projectRoot);
97
- const currentSession = await getCurrentSession(projectRoot);
123
+ const currentSession = getCurrentSession(projectRoot);
98
124
 
99
125
  console.log('📊 shit-cli Status\n');
100
126
 
@@ -121,7 +147,7 @@ export default async function status(args) {
121
147
  console.log(` Started: ${new Date(state.start_time).toLocaleString()}`);
122
148
  console.log(` Duration: ${formatDuration(state.start_time)}`);
123
149
  console.log(` Events: ${state.event_count || 0}`);
124
- console.log(` Files: ${Object.keys(state.files || {}).length}`);
150
+ console.log(` Files: ${getTouchedFileCount(state)}`);
125
151
 
126
152
  if (state.shadow_branch) {
127
153
  console.log(` Shadow: ${state.shadow_branch}`);
@@ -149,8 +175,7 @@ export default async function status(args) {
149
175
  if (existsSync(claudeSettings)) {
150
176
  try {
151
177
  const settings = JSON.parse(readFileSync(claudeSettings, 'utf-8'));
152
- const hasHooks = settings.hooks &&
153
- (settings.hooks.session_start || settings.hooks.session_end);
178
+ const hasHooks = hasShitHooks(settings);
154
179
 
155
180
  console.log();
156
181
  if (hasHooks) {
@@ -0,0 +1,331 @@
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 { getProjectRoot } from './config.js';
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 = getProjectRoot();
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
+ }
package/package.json CHANGED
@@ -1,20 +1,37 @@
1
1
  {
2
2
  "name": "@claudemini/shit-cli",
3
- "version": "1.3.0",
4
- "description": "Session-based Hook Intelligence Tracker - CLI tool for logging Claude Code hooks",
3
+ "version": "1.5.0",
4
+ "description": "Session-based Hook Intelligence Tracker - Zero-dependency memory system for human-AI coding sessions",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "shit": "./bin/shit.js"
8
8
  },
9
+ "files": [
10
+ "bin/",
11
+ "lib/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18.0.0"
17
+ },
9
18
  "scripts": {
10
19
  "test": "echo \"Error: no test specified\" && exit 1"
11
20
  },
12
21
  "keywords": [
13
22
  "claude-code",
23
+ "gemini-cli",
24
+ "cursor",
25
+ "ai-coding",
14
26
  "hooks",
15
- "logging",
16
- "session-tracking"
27
+ "session-tracking",
28
+ "code-review",
29
+ "checkpoint"
17
30
  ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/anthropics/shit-cli.git"
34
+ },
18
35
  "author": "",
19
36
  "license": "MIT",
20
37
  "dependencies": {},