stakeout-cli 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +131 -0
  2. package/README.md +152 -0
  3. package/dist/commands/chat.d.ts +5 -0
  4. package/dist/commands/chat.js +162 -0
  5. package/dist/commands/clear.d.ts +7 -0
  6. package/dist/commands/clear.js +89 -0
  7. package/dist/commands/config.d.ts +10 -0
  8. package/dist/commands/config.js +64 -0
  9. package/dist/commands/dashboard.d.ts +5 -0
  10. package/dist/commands/dashboard.js +9 -0
  11. package/dist/commands/digest.d.ts +6 -0
  12. package/dist/commands/digest.js +113 -0
  13. package/dist/commands/export.d.ts +8 -0
  14. package/dist/commands/export.js +118 -0
  15. package/dist/commands/hook.d.ts +6 -0
  16. package/dist/commands/hook.js +57 -0
  17. package/dist/commands/init.d.ts +6 -0
  18. package/dist/commands/init.js +70 -0
  19. package/dist/commands/log.d.ts +9 -0
  20. package/dist/commands/log.js +103 -0
  21. package/dist/commands/note.d.ts +9 -0
  22. package/dist/commands/note.js +48 -0
  23. package/dist/commands/record.d.ts +6 -0
  24. package/dist/commands/record.js +106 -0
  25. package/dist/commands/repo.d.ts +7 -0
  26. package/dist/commands/repo.js +60 -0
  27. package/dist/commands/search.d.ts +5 -0
  28. package/dist/commands/search.js +69 -0
  29. package/dist/commands/stats.d.ts +1 -0
  30. package/dist/commands/stats.js +99 -0
  31. package/dist/commands/tag.d.ts +8 -0
  32. package/dist/commands/tag.js +61 -0
  33. package/dist/commands/tui.d.ts +1 -0
  34. package/dist/commands/tui.js +5 -0
  35. package/dist/commands/watch.d.ts +6 -0
  36. package/dist/commands/watch.js +101 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +195 -0
  39. package/dist/lib/config.d.ts +5 -0
  40. package/dist/lib/config.js +51 -0
  41. package/dist/lib/database.d.ts +31 -0
  42. package/dist/lib/database.js +222 -0
  43. package/dist/lib/diff.d.ts +3 -0
  44. package/dist/lib/diff.js +118 -0
  45. package/dist/lib/summarizer.d.ts +2 -0
  46. package/dist/lib/summarizer.js +90 -0
  47. package/dist/tui/App.d.ts +1 -0
  48. package/dist/tui/App.js +125 -0
  49. package/dist/types/index.d.ts +38 -0
  50. package/dist/types/index.js +1 -0
  51. package/dist/web/public/app.js +387 -0
  52. package/dist/web/public/index.html +131 -0
  53. package/dist/web/public/styles.css +571 -0
  54. package/dist/web/server.d.ts +1 -0
  55. package/dist/web/server.js +402 -0
  56. package/package.json +69 -0
@@ -0,0 +1,113 @@
1
+ import chalk from 'chalk';
2
+ import { getEntries } from '../lib/database.js';
3
+ import { loadConfig } from '../lib/config.js';
4
+ import { Ollama } from 'ollama';
5
+ import OpenAI from 'openai';
6
+ export async function digest(options) {
7
+ const since = options.since ? parseSince(options.since) : getWeekAgo();
8
+ console.log(chalk.dim('🔍 Gathering intel...'));
9
+ const entries = getEntries({
10
+ limit: 100,
11
+ since,
12
+ path: options.path
13
+ });
14
+ if (entries.length === 0) {
15
+ console.log(chalk.yellow('No entries found in this time period.'));
16
+ return;
17
+ }
18
+ console.log(chalk.dim(`📊 Found ${entries.length} entries`));
19
+ console.log(chalk.dim('🤖 Generating digest...\n'));
20
+ // Build a summary of all entries
21
+ const entriesSummary = entries.map(e => {
22
+ const date = new Date(e.timestamp).toLocaleDateString();
23
+ const dirs = e.directories.length > 0 ? ` (${e.directories.join(', ')})` : '';
24
+ return `- [${date}]${dirs}: ${e.summary}`;
25
+ }).join('\n');
26
+ const prompt = `You are analyzing a development activity log. Create a high-level digest that:
27
+
28
+ 1. Identifies the main themes/areas of work
29
+ 2. Highlights significant changes or milestones
30
+ 3. Notes any patterns or concerns
31
+ 4. Provides a brief "executive summary" (2-3 sentences)
32
+
33
+ Keep the digest concise but insightful. Focus on the big picture.
34
+
35
+ Activity log:
36
+ ${entriesSummary}
37
+
38
+ Generate a digest with sections: Executive Summary, Key Areas, Notable Changes, Patterns/Observations`;
39
+ try {
40
+ const summary = await generateDigest(prompt);
41
+ console.log(chalk.bold.green('📋 STAKEOUT DIGEST'));
42
+ console.log(chalk.dim(`Period: ${new Date(since).toLocaleDateString()} - ${new Date().toLocaleDateString()}`));
43
+ console.log(chalk.dim(`Entries analyzed: ${entries.length}`));
44
+ console.log(chalk.dim('─'.repeat(60)));
45
+ console.log('');
46
+ console.log(summary);
47
+ console.log('');
48
+ }
49
+ catch (error) {
50
+ console.error(chalk.red('Error generating digest: ' + error.message));
51
+ process.exit(1);
52
+ }
53
+ }
54
+ async function generateDigest(prompt) {
55
+ const config = loadConfig();
56
+ if (config.llm_provider === 'ollama') {
57
+ const ollama = new Ollama({ host: config.ollama_host });
58
+ const response = await ollama.chat({
59
+ model: config.ollama_model,
60
+ messages: [{ role: 'user', content: prompt }],
61
+ options: { temperature: 0.4 }
62
+ });
63
+ return response.message.content.trim();
64
+ }
65
+ else {
66
+ if (!config.openai_api_key) {
67
+ throw new Error('OpenAI API key not configured');
68
+ }
69
+ const openai = new OpenAI({ apiKey: config.openai_api_key });
70
+ const response = await openai.chat.completions.create({
71
+ model: config.openai_model,
72
+ messages: [{ role: 'user', content: prompt }],
73
+ temperature: 0.4,
74
+ max_tokens: 1000
75
+ });
76
+ return response.choices[0]?.message?.content?.trim() ?? 'Unable to generate digest';
77
+ }
78
+ }
79
+ function parseSince(since) {
80
+ const now = new Date();
81
+ const match = since.match(/^(\d+)\s*(day|days|week|weeks|month|months)$/i);
82
+ if (match) {
83
+ const amount = parseInt(match[1], 10);
84
+ const unit = match[2].toLowerCase();
85
+ switch (unit) {
86
+ case 'day':
87
+ case 'days':
88
+ now.setDate(now.getDate() - amount);
89
+ break;
90
+ case 'week':
91
+ case 'weeks':
92
+ now.setDate(now.getDate() - (amount * 7));
93
+ break;
94
+ case 'month':
95
+ case 'months':
96
+ now.setMonth(now.getMonth() - amount);
97
+ break;
98
+ }
99
+ return now.toISOString();
100
+ }
101
+ // Try to parse as date
102
+ const parsed = new Date(since);
103
+ if (!isNaN(parsed.getTime())) {
104
+ return parsed.toISOString();
105
+ }
106
+ // Default to 1 week
107
+ return getWeekAgo();
108
+ }
109
+ function getWeekAgo() {
110
+ const now = new Date();
111
+ now.setDate(now.getDate() - 7);
112
+ return now.toISOString();
113
+ }
@@ -0,0 +1,8 @@
1
+ interface ExportOptions {
2
+ format?: string;
3
+ since?: string;
4
+ path?: string;
5
+ output?: string;
6
+ }
7
+ export declare function exportCmd(options: ExportOptions): Promise<void>;
8
+ export {};
@@ -0,0 +1,118 @@
1
+ import chalk from 'chalk';
2
+ import { writeFileSync } from 'fs';
3
+ import { getEntries } from '../lib/database.js';
4
+ export async function exportCmd(options) {
5
+ const format = options.format || 'markdown';
6
+ const since = options.since ? parseSince(options.since) : undefined;
7
+ const entries = getEntries({
8
+ limit: 1000,
9
+ since,
10
+ path: options.path
11
+ });
12
+ if (entries.length === 0) {
13
+ console.log(chalk.yellow('No entries to export.'));
14
+ return;
15
+ }
16
+ let content;
17
+ let extension;
18
+ switch (format.toLowerCase()) {
19
+ case 'json':
20
+ content = JSON.stringify(entries, null, 2);
21
+ extension = 'json';
22
+ break;
23
+ case 'csv':
24
+ content = exportToCsv(entries);
25
+ extension = 'csv';
26
+ break;
27
+ case 'markdown':
28
+ case 'md':
29
+ default:
30
+ content = exportToMarkdown(entries);
31
+ extension = 'md';
32
+ break;
33
+ }
34
+ const outputPath = options.output || `stakeout-export-${Date.now()}.${extension}`;
35
+ writeFileSync(outputPath, content);
36
+ console.log(chalk.green(`✓ Exported ${entries.length} entries to ${outputPath}`));
37
+ }
38
+ function exportToMarkdown(entries) {
39
+ const lines = [
40
+ '# STAKEOUT Export',
41
+ '',
42
+ `Generated: ${new Date().toISOString()}`,
43
+ `Entries: ${entries.length}`,
44
+ '',
45
+ '---',
46
+ ''
47
+ ];
48
+ for (const entry of entries) {
49
+ const date = new Date(entry.timestamp).toLocaleString();
50
+ lines.push(`## Entry #${entry.id}`);
51
+ lines.push('');
52
+ lines.push(`**Date:** ${date}`);
53
+ if (entry.commit_hash) {
54
+ lines.push(`**Commit:** \`${entry.commit_hash.slice(0, 7)}\``);
55
+ }
56
+ if (entry.commit_message) {
57
+ lines.push(`**Message:** ${entry.commit_message}`);
58
+ }
59
+ lines.push('');
60
+ lines.push('### Summary');
61
+ lines.push('');
62
+ lines.push(entry.summary);
63
+ lines.push('');
64
+ if (entry.directories.length > 0) {
65
+ lines.push('### Directories');
66
+ lines.push('');
67
+ lines.push(entry.directories.map((d) => `- \`${d}\``).join('\n'));
68
+ lines.push('');
69
+ }
70
+ if (entry.files_changed.length > 0) {
71
+ lines.push('### Files Changed');
72
+ lines.push('');
73
+ lines.push(entry.files_changed.slice(0, 20).map((f) => `- \`${f}\``).join('\n'));
74
+ if (entry.files_changed.length > 20) {
75
+ lines.push(`- ... and ${entry.files_changed.length - 20} more`);
76
+ }
77
+ lines.push('');
78
+ }
79
+ lines.push('---');
80
+ lines.push('');
81
+ }
82
+ return lines.join('\n');
83
+ }
84
+ function exportToCsv(entries) {
85
+ const headers = ['id', 'timestamp', 'commit_hash', 'summary', 'directories', 'file_count'];
86
+ const rows = entries.map(e => [
87
+ e.id,
88
+ e.timestamp,
89
+ e.commit_hash || '',
90
+ `"${e.summary.replace(/"/g, '""')}"`,
91
+ `"${e.directories.join(', ')}"`,
92
+ e.files_changed.length
93
+ ]);
94
+ return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
95
+ }
96
+ function parseSince(since) {
97
+ const now = new Date();
98
+ const match = since.match(/^(\d+)\s*(day|days|week|weeks|month|months)$/i);
99
+ if (match) {
100
+ const amount = parseInt(match[1], 10);
101
+ const unit = match[2].toLowerCase();
102
+ switch (unit) {
103
+ case 'day':
104
+ case 'days':
105
+ now.setDate(now.getDate() - amount);
106
+ break;
107
+ case 'week':
108
+ case 'weeks':
109
+ now.setDate(now.getDate() - (amount * 7));
110
+ break;
111
+ case 'month':
112
+ case 'months':
113
+ now.setMonth(now.getMonth() - amount);
114
+ break;
115
+ }
116
+ }
117
+ return now.toISOString();
118
+ }
@@ -0,0 +1,6 @@
1
+ interface HookOptions {
2
+ path?: string;
3
+ remove?: boolean;
4
+ }
5
+ export declare function hook(options: HookOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { simpleGit } from 'simple-git';
5
+ const HOOK_SCRIPT = `#!/bin/sh
6
+ # STAKEOUT post-commit hook
7
+ # Records changes after each commit
8
+
9
+ stakeout record --last-commit 2>/dev/null || true
10
+ `;
11
+ export async function hook(options) {
12
+ const repoPath = options.path || process.cwd();
13
+ const git = simpleGit(repoPath);
14
+ const isRepo = await git.checkIsRepo();
15
+ if (!isRepo) {
16
+ console.error(chalk.red('Not a git repository: ' + repoPath));
17
+ process.exit(1);
18
+ }
19
+ const gitDir = join(repoPath, '.git');
20
+ const hooksDir = join(gitDir, 'hooks');
21
+ const hookPath = join(hooksDir, 'post-commit');
22
+ if (options.remove) {
23
+ if (existsSync(hookPath)) {
24
+ const { unlinkSync } = await import('fs');
25
+ unlinkSync(hookPath);
26
+ console.log(chalk.green('✓ Removed post-commit hook'));
27
+ }
28
+ else {
29
+ console.log(chalk.yellow('No hook installed'));
30
+ }
31
+ return;
32
+ }
33
+ // Create hooks directory if it doesn't exist
34
+ if (!existsSync(hooksDir)) {
35
+ mkdirSync(hooksDir, { recursive: true });
36
+ }
37
+ // Check if hook already exists
38
+ if (existsSync(hookPath)) {
39
+ console.log(chalk.yellow('Hook already exists at: ' + hookPath));
40
+ console.log(chalk.dim('Use --remove to uninstall first'));
41
+ return;
42
+ }
43
+ // Write the hook
44
+ writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
45
+ // Make sure it's executable (for Unix systems)
46
+ try {
47
+ chmodSync(hookPath, 0o755);
48
+ }
49
+ catch {
50
+ // Windows doesn't need chmod
51
+ }
52
+ console.log(chalk.green('✓ Installed post-commit hook'));
53
+ console.log(chalk.dim(' Location: ' + hookPath));
54
+ console.log('');
55
+ console.log(chalk.cyan('Now every commit will automatically record a summary!'));
56
+ console.log(chalk.dim('To remove: stakeout hook --remove'));
57
+ }
@@ -0,0 +1,6 @@
1
+ interface InitOptions {
2
+ path?: string;
3
+ hook?: boolean;
4
+ }
5
+ export declare function init(options: InitOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,70 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { simpleGit } from 'simple-git';
5
+ import { loadConfig, saveConfig } from '../lib/config.js';
6
+ import { hook } from './hook.js';
7
+ export async function init(options) {
8
+ const repoPath = options.path || process.cwd();
9
+ console.log(chalk.bold.green('\n🔍 STAKEOUT Setup\n'));
10
+ console.log(chalk.dim('─'.repeat(40)));
11
+ // Check if it's a git repo
12
+ const git = simpleGit(repoPath);
13
+ const isRepo = await git.checkIsRepo();
14
+ if (!isRepo) {
15
+ console.log(chalk.red('✗ Not a git repository'));
16
+ console.log(chalk.dim(` Path: ${repoPath}`));
17
+ console.log(chalk.dim(' Initialize with: git init'));
18
+ return;
19
+ }
20
+ console.log(chalk.green('✓ Git repository detected'));
21
+ console.log(chalk.dim(` Path: ${repoPath}`));
22
+ // Get repo info
23
+ const log = await git.log({ maxCount: 1 }).catch(() => null);
24
+ if (log?.latest) {
25
+ console.log(chalk.dim(` Latest commit: ${log.latest.hash.slice(0, 7)} - ${log.latest.message.slice(0, 40)}`));
26
+ }
27
+ // Update config with this path
28
+ const config = loadConfig();
29
+ config.watched_path = repoPath;
30
+ saveConfig(config);
31
+ console.log(chalk.green('✓ Set as watched path'));
32
+ // Check Ollama
33
+ console.log('');
34
+ console.log(chalk.dim('Checking LLM provider...'));
35
+ try {
36
+ const { Ollama } = await import('ollama');
37
+ const ollama = new Ollama({ host: config.ollama_host });
38
+ await ollama.list();
39
+ console.log(chalk.green(`✓ Ollama connected (${config.ollama_model})`));
40
+ }
41
+ catch {
42
+ console.log(chalk.yellow('⚠ Ollama not available'));
43
+ console.log(chalk.dim(' Start with: ollama serve'));
44
+ console.log(chalk.dim(' Or use: stakeout config --provider openai'));
45
+ }
46
+ // Install hook if requested
47
+ if (options.hook !== false) {
48
+ console.log('');
49
+ console.log(chalk.dim('Installing git hook...'));
50
+ const hooksDir = join(repoPath, '.git', 'hooks');
51
+ const hookPath = join(hooksDir, 'post-commit');
52
+ if (existsSync(hookPath)) {
53
+ console.log(chalk.yellow('⚠ Hook already exists'));
54
+ }
55
+ else {
56
+ await hook({ path: repoPath });
57
+ }
58
+ }
59
+ // Summary
60
+ console.log('');
61
+ console.log(chalk.dim('─'.repeat(40)));
62
+ console.log(chalk.bold.green('\n✓ STAKEOUT is ready!\n'));
63
+ console.log('Quick commands:');
64
+ console.log(chalk.cyan(' stakeout record') + chalk.dim(' # Record current changes'));
65
+ console.log(chalk.cyan(' stakeout record -c') + chalk.dim(' # Record last commit'));
66
+ console.log(chalk.cyan(' stakeout dash') + chalk.dim(' # Open web dashboard'));
67
+ console.log(chalk.cyan(' stakeout ui') + chalk.dim(' # Open terminal UI'));
68
+ console.log(chalk.cyan(' stakeout watch') + chalk.dim(' # Auto-record mode'));
69
+ console.log('');
70
+ }
@@ -0,0 +1,9 @@
1
+ interface LogOptions {
2
+ limit?: string;
3
+ since?: string;
4
+ path?: string;
5
+ favorites?: boolean;
6
+ tag?: string;
7
+ }
8
+ export declare function log(options: LogOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,103 @@
1
+ import chalk from 'chalk';
2
+ import { getEntries } from '../lib/database.js';
3
+ export async function log(options) {
4
+ const limit = options.limit ? parseInt(options.limit, 10) : 10;
5
+ const since = options.since ? parseSince(options.since) : undefined;
6
+ const entries = getEntries({
7
+ limit,
8
+ since,
9
+ path: options.path,
10
+ favorites: options.favorites,
11
+ tag: options.tag
12
+ });
13
+ if (entries.length === 0) {
14
+ console.log(chalk.yellow('No entries found.'));
15
+ console.log(chalk.dim('Record changes with: stakeout record'));
16
+ return;
17
+ }
18
+ let title = 'STAKEOUT LOG';
19
+ if (options.favorites)
20
+ title = 'FAVORITES';
21
+ if (options.tag)
22
+ title = `TAG: ${options.tag}`;
23
+ console.log(chalk.bold(`\n📋 ${title} (${entries.length} entries)\n`));
24
+ console.log(chalk.dim('─'.repeat(60)));
25
+ for (const entry of entries) {
26
+ const date = new Date(entry.timestamp);
27
+ const timeAgo = getTimeAgo(date);
28
+ console.log('');
29
+ // Build status indicators
30
+ const indicators = [];
31
+ if (entry.favorite)
32
+ indicators.push(chalk.yellow('★'));
33
+ if (entry.is_breaking)
34
+ indicators.push(chalk.red('⚠️'));
35
+ console.log(chalk.cyan(`#${entry.id}`) +
36
+ (indicators.length > 0 ? ' ' + indicators.join(' ') : '') +
37
+ chalk.dim(` · ${timeAgo}`) +
38
+ (entry.commit_hash ? chalk.dim(` · ${entry.commit_hash.slice(0, 7)}`) : ''));
39
+ console.log(chalk.white(entry.summary));
40
+ if (entry.tags && entry.tags.length > 0) {
41
+ console.log(chalk.magenta(` 🏷️ ${entry.tags.join(', ')}`));
42
+ }
43
+ if (entry.notes) {
44
+ console.log(chalk.dim(` 📝 ${entry.notes}`));
45
+ }
46
+ if (entry.directories.length > 0) {
47
+ console.log(chalk.dim(` 📁 ${entry.directories.join(', ')}`));
48
+ }
49
+ console.log(chalk.dim('─'.repeat(60)));
50
+ }
51
+ console.log('');
52
+ }
53
+ function parseSince(since) {
54
+ const now = new Date();
55
+ const match = since.match(/^(\d+)\s*(day|days|week|weeks|hour|hours|month|months)$/i);
56
+ if (!match) {
57
+ // Try to parse as date
58
+ const parsed = new Date(since);
59
+ if (!isNaN(parsed.getTime())) {
60
+ return parsed.toISOString();
61
+ }
62
+ console.error(chalk.red(`Invalid --since format: ${since}`));
63
+ console.error(chalk.dim('Examples: "3 days", "1 week", "2 hours"'));
64
+ process.exit(1);
65
+ }
66
+ const amount = parseInt(match[1], 10);
67
+ const unit = match[2].toLowerCase();
68
+ switch (unit) {
69
+ case 'hour':
70
+ case 'hours':
71
+ now.setHours(now.getHours() - amount);
72
+ break;
73
+ case 'day':
74
+ case 'days':
75
+ now.setDate(now.getDate() - amount);
76
+ break;
77
+ case 'week':
78
+ case 'weeks':
79
+ now.setDate(now.getDate() - (amount * 7));
80
+ break;
81
+ case 'month':
82
+ case 'months':
83
+ now.setMonth(now.getMonth() - amount);
84
+ break;
85
+ }
86
+ return now.toISOString();
87
+ }
88
+ function getTimeAgo(date) {
89
+ const now = new Date();
90
+ const diffMs = now.getTime() - date.getTime();
91
+ const diffMins = Math.floor(diffMs / 60000);
92
+ const diffHours = Math.floor(diffMins / 60);
93
+ const diffDays = Math.floor(diffHours / 24);
94
+ if (diffMins < 1)
95
+ return 'just now';
96
+ if (diffMins < 60)
97
+ return `${diffMins}m ago`;
98
+ if (diffHours < 24)
99
+ return `${diffHours}h ago`;
100
+ if (diffDays < 7)
101
+ return `${diffDays}d ago`;
102
+ return date.toLocaleDateString();
103
+ }
@@ -0,0 +1,9 @@
1
+ interface NoteOptions {
2
+ note?: string;
3
+ favorite?: boolean;
4
+ unfavorite?: boolean;
5
+ breaking?: boolean;
6
+ notBreaking?: boolean;
7
+ }
8
+ export declare function note(entryId: string, options: NoteOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk';
2
+ import { getEntry, updateEntry } from '../lib/database.js';
3
+ export async function note(entryId, options) {
4
+ const id = parseInt(entryId, 10);
5
+ if (isNaN(id)) {
6
+ console.error(chalk.red('Invalid entry ID'));
7
+ return;
8
+ }
9
+ const entry = getEntry(id);
10
+ if (!entry) {
11
+ console.error(chalk.red(`Entry #${id} not found`));
12
+ return;
13
+ }
14
+ // If no options, show current state
15
+ if (!options.note && !options.favorite && !options.unfavorite && !options.breaking && !options.notBreaking) {
16
+ console.log(chalk.bold(`\nEntry #${id}\n`));
17
+ console.log(chalk.dim('Summary: ') + entry.summary);
18
+ console.log(chalk.dim('Favorite: ') + (entry.favorite ? chalk.yellow('★ Yes') : 'No'));
19
+ console.log(chalk.dim('Breaking: ') + (entry.is_breaking ? chalk.red('⚠️ Yes') : 'No'));
20
+ console.log(chalk.dim('Notes: ') + (entry.notes || chalk.dim('(none)')));
21
+ console.log('');
22
+ return;
23
+ }
24
+ const updates = {};
25
+ if (options.note !== undefined) {
26
+ updates.notes = options.note;
27
+ console.log(chalk.green(`✓ Updated note for entry #${id}`));
28
+ }
29
+ if (options.favorite) {
30
+ updates.favorite = true;
31
+ console.log(chalk.green(`✓ Marked entry #${id} as favorite ★`));
32
+ }
33
+ if (options.unfavorite) {
34
+ updates.favorite = false;
35
+ console.log(chalk.green(`✓ Removed favorite from entry #${id}`));
36
+ }
37
+ if (options.breaking) {
38
+ updates.is_breaking = true;
39
+ console.log(chalk.green(`✓ Marked entry #${id} as breaking change ⚠️`));
40
+ }
41
+ if (options.notBreaking) {
42
+ updates.is_breaking = false;
43
+ console.log(chalk.green(`✓ Unmarked entry #${id} as breaking change`));
44
+ }
45
+ if (Object.keys(updates).length > 0) {
46
+ updateEntry(id, updates);
47
+ }
48
+ }
@@ -0,0 +1,6 @@
1
+ interface RecordOptions {
2
+ path?: string;
3
+ lastCommit?: boolean;
4
+ }
5
+ export declare function record(options: RecordOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,106 @@
1
+ import chalk from 'chalk';
2
+ import { collectDiff, collectLastCommit } from '../lib/diff.js';
3
+ import { summarize } from '../lib/summarizer.js';
4
+ import { insertEntry, entryExists, updateRepoLastRecorded } from '../lib/database.js';
5
+ import { loadConfig } from '../lib/config.js';
6
+ // Default patterns that indicate potentially breaking changes
7
+ const DEFAULT_BREAKING_PATTERNS = [
8
+ /^src\/api\//i,
9
+ /^api\//i,
10
+ /schema/i,
11
+ /migration/i,
12
+ /\.proto$/i,
13
+ /breaking/i,
14
+ /major/i,
15
+ /deprecated/i,
16
+ /removed?/i,
17
+ /delete/i,
18
+ ];
19
+ export async function record(options) {
20
+ const config = loadConfig();
21
+ const repoPath = options.path || config.watched_path || process.cwd();
22
+ console.log(chalk.dim('🔍 Collecting changes...'));
23
+ const diff = options.lastCommit
24
+ ? await collectLastCommit(repoPath)
25
+ : await collectDiff(repoPath);
26
+ if (!diff) {
27
+ console.log(chalk.yellow('No changes detected.'));
28
+ return;
29
+ }
30
+ // Check for duplicates
31
+ if (entryExists(diff.diff_hash)) {
32
+ console.log(chalk.yellow('These changes have already been recorded.'));
33
+ return;
34
+ }
35
+ console.log(chalk.dim(`📁 ${diff.files.length} files changed`));
36
+ console.log(chalk.dim('🤖 Generating summary...'));
37
+ try {
38
+ const summary = await summarize(diff);
39
+ // Detect breaking changes
40
+ const isBreaking = detectBreakingChange(diff.files, diff.diff_text, summary);
41
+ const entry = {
42
+ timestamp: new Date().toISOString(),
43
+ files_changed: diff.files,
44
+ directories: diff.directories,
45
+ summary,
46
+ diff_hash: diff.diff_hash,
47
+ commit_hash: diff.commit_hash,
48
+ commit_message: diff.commit_message,
49
+ repo_path: repoPath,
50
+ is_breaking: isBreaking,
51
+ tags: [],
52
+ favorite: false,
53
+ notes: ''
54
+ };
55
+ const id = insertEntry(entry);
56
+ // Update repo last recorded time
57
+ updateRepoLastRecorded(repoPath);
58
+ console.log('');
59
+ console.log(chalk.green('✓ Recorded entry #' + id));
60
+ if (isBreaking) {
61
+ console.log(chalk.red.bold('⚠️ POTENTIAL BREAKING CHANGE DETECTED'));
62
+ }
63
+ console.log('');
64
+ console.log(chalk.bold('Summary:'));
65
+ console.log(chalk.white(summary));
66
+ console.log('');
67
+ console.log(chalk.dim(`Files: ${diff.files.slice(0, 5).join(', ')}${diff.files.length > 5 ? ` (+${diff.files.length - 5} more)` : ''}`));
68
+ }
69
+ catch (error) {
70
+ console.error(chalk.red('Error: ' + error.message));
71
+ process.exit(1);
72
+ }
73
+ }
74
+ function detectBreakingChange(files, diffText, summary) {
75
+ // Check files against breaking patterns
76
+ for (const file of files) {
77
+ for (const pattern of DEFAULT_BREAKING_PATTERNS) {
78
+ if (pattern.test(file)) {
79
+ return true;
80
+ }
81
+ }
82
+ }
83
+ // Check summary for breaking keywords
84
+ const breakingKeywords = [
85
+ 'breaking change',
86
+ 'breaking:',
87
+ 'removed',
88
+ 'deleted',
89
+ 'deprecated',
90
+ 'incompatible',
91
+ 'migration required',
92
+ 'schema change',
93
+ 'api change'
94
+ ];
95
+ const lowerSummary = summary.toLowerCase();
96
+ for (const keyword of breakingKeywords) {
97
+ if (lowerSummary.includes(keyword)) {
98
+ return true;
99
+ }
100
+ }
101
+ // Check diff for removal of exports or public APIs
102
+ if (diffText.includes('-export ') || diffText.includes('-public ')) {
103
+ return true;
104
+ }
105
+ return false;
106
+ }
@@ -0,0 +1,7 @@
1
+ interface RepoOptions {
2
+ add?: string;
3
+ remove?: string;
4
+ list?: boolean;
5
+ }
6
+ export declare function repo(options: RepoOptions): Promise<void>;
7
+ export {};