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,60 @@
1
+ import chalk from 'chalk';
2
+ import { basename } from 'path';
3
+ import { simpleGit } from 'simple-git';
4
+ import { addRepo, getRepos, getDb } from '../lib/database.js';
5
+ export async function repo(options) {
6
+ if (options.add) {
7
+ await addRepoCmd(options.add);
8
+ return;
9
+ }
10
+ if (options.remove) {
11
+ await removeRepoCmd(options.remove);
12
+ return;
13
+ }
14
+ // Default: list repos
15
+ await listReposCmd();
16
+ }
17
+ async function addRepoCmd(path) {
18
+ const git = simpleGit(path);
19
+ const isRepo = await git.checkIsRepo();
20
+ if (!isRepo) {
21
+ console.error(chalk.red('Not a git repository: ' + path));
22
+ return;
23
+ }
24
+ const name = basename(path);
25
+ addRepo(path, name);
26
+ console.log(chalk.green(`āœ“ Added repository: ${name}`));
27
+ console.log(chalk.dim(` Path: ${path}`));
28
+ }
29
+ async function removeRepoCmd(pathOrName) {
30
+ const db = getDb();
31
+ const result = db.prepare('DELETE FROM repos WHERE path = ? OR name = ?').run(pathOrName, pathOrName);
32
+ if (result.changes > 0) {
33
+ console.log(chalk.green(`āœ“ Removed repository: ${pathOrName}`));
34
+ }
35
+ else {
36
+ console.log(chalk.yellow(`Repository not found: ${pathOrName}`));
37
+ }
38
+ }
39
+ async function listReposCmd() {
40
+ const repos = getRepos();
41
+ const db = getDb();
42
+ console.log(chalk.bold('\nšŸ“¦ Tracked Repositories\n'));
43
+ if (repos.length === 0) {
44
+ console.log(chalk.dim('No repositories tracked.'));
45
+ console.log(chalk.dim('Add one with: stakeout repo --add /path/to/repo'));
46
+ console.log('');
47
+ return;
48
+ }
49
+ for (const repo of repos) {
50
+ // Get entry count for this repo
51
+ const count = db.prepare('SELECT COUNT(*) as count FROM entries WHERE repo_path = ?').get(repo.path).count;
52
+ console.log(chalk.cyan(repo.name));
53
+ console.log(chalk.dim(` Path: ${repo.path}`));
54
+ console.log(chalk.dim(` Entries: ${count}`));
55
+ if (repo.last_recorded) {
56
+ console.log(chalk.dim(` Last recorded: ${new Date(repo.last_recorded).toLocaleString()}`));
57
+ }
58
+ console.log('');
59
+ }
60
+ }
@@ -0,0 +1,5 @@
1
+ interface SearchOptions {
2
+ limit?: string;
3
+ }
4
+ export declare function search(query: string, options: SearchOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import { getDb } from '../lib/database.js';
3
+ export async function search(query, options) {
4
+ const limit = options.limit ? parseInt(options.limit, 10) : 20;
5
+ const db = getDb();
6
+ // Search in summaries, commit messages, files, and directories
7
+ const stmt = db.prepare(`
8
+ SELECT * FROM entries
9
+ WHERE summary LIKE ?
10
+ OR commit_message LIKE ?
11
+ OR files_changed LIKE ?
12
+ OR directories LIKE ?
13
+ ORDER BY timestamp DESC
14
+ LIMIT ?
15
+ `);
16
+ const pattern = `%${query}%`;
17
+ const rows = stmt.all(pattern, pattern, pattern, pattern, limit);
18
+ if (rows.length === 0) {
19
+ console.log(chalk.yellow(`No results for "${query}"`));
20
+ return;
21
+ }
22
+ console.log(chalk.bold(`\nšŸ” Search results for "${query}" (${rows.length} found)\n`));
23
+ console.log(chalk.dim('─'.repeat(60)));
24
+ for (const row of rows) {
25
+ const entry = {
26
+ id: row.id,
27
+ timestamp: row.timestamp,
28
+ files_changed: JSON.parse(row.files_changed),
29
+ directories: JSON.parse(row.directories),
30
+ summary: row.summary,
31
+ diff_hash: row.diff_hash,
32
+ commit_hash: row.commit_hash,
33
+ commit_message: row.commit_message
34
+ };
35
+ const date = new Date(entry.timestamp);
36
+ const timeAgo = getTimeAgo(date);
37
+ console.log('');
38
+ console.log(chalk.cyan(`#${entry.id}`) +
39
+ chalk.dim(` Ā· ${timeAgo}`) +
40
+ (entry.commit_hash ? chalk.dim(` Ā· ${entry.commit_hash.slice(0, 7)}`) : ''));
41
+ // Highlight the query in the summary
42
+ const highlightedSummary = entry.summary.replace(new RegExp(`(${escapeRegex(query)})`, 'gi'), chalk.bgYellow.black('$1'));
43
+ console.log(chalk.white(highlightedSummary));
44
+ if (entry.directories.length > 0) {
45
+ console.log(chalk.dim(` šŸ“ ${entry.directories.join(', ')}`));
46
+ }
47
+ console.log(chalk.dim('─'.repeat(60)));
48
+ }
49
+ console.log('');
50
+ }
51
+ function getTimeAgo(date) {
52
+ const now = new Date();
53
+ const diffMs = now.getTime() - date.getTime();
54
+ const diffMins = Math.floor(diffMs / 60000);
55
+ const diffHours = Math.floor(diffMins / 60);
56
+ const diffDays = Math.floor(diffHours / 24);
57
+ if (diffMins < 1)
58
+ return 'just now';
59
+ if (diffMins < 60)
60
+ return `${diffMins}m ago`;
61
+ if (diffHours < 24)
62
+ return `${diffHours}h ago`;
63
+ if (diffDays < 7)
64
+ return `${diffDays}d ago`;
65
+ return date.toLocaleDateString();
66
+ }
67
+ function escapeRegex(str) {
68
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
+ }
@@ -0,0 +1 @@
1
+ export declare function stats(): Promise<void>;
@@ -0,0 +1,99 @@
1
+ import chalk from 'chalk';
2
+ import { getDb } from '../lib/database.js';
3
+ export async function stats() {
4
+ const db = getDb();
5
+ // Total entries
6
+ const total = db.prepare('SELECT COUNT(*) as count FROM entries').get();
7
+ // Today
8
+ const today = new Date();
9
+ today.setHours(0, 0, 0, 0);
10
+ const todayCount = db.prepare('SELECT COUNT(*) as count FROM entries WHERE timestamp >= ?').get(today.toISOString());
11
+ // This week
12
+ const weekAgo = new Date();
13
+ weekAgo.setDate(weekAgo.getDate() - 7);
14
+ const weekCount = db.prepare('SELECT COUNT(*) as count FROM entries WHERE timestamp >= ?').get(weekAgo.toISOString());
15
+ // This month
16
+ const monthAgo = new Date();
17
+ monthAgo.setMonth(monthAgo.getMonth() - 1);
18
+ const monthCount = db.prepare('SELECT COUNT(*) as count FROM entries WHERE timestamp >= ?').get(monthAgo.toISOString());
19
+ // First and last entry
20
+ const first = db.prepare('SELECT timestamp FROM entries ORDER BY timestamp ASC LIMIT 1').get();
21
+ const last = db.prepare('SELECT timestamp FROM entries ORDER BY timestamp DESC LIMIT 1').get();
22
+ // Most active directories
23
+ const allDirs = db.prepare('SELECT directories FROM entries').all();
24
+ const dirCounts = {};
25
+ for (const row of allDirs) {
26
+ const dirs = JSON.parse(row.directories);
27
+ for (const dir of dirs) {
28
+ dirCounts[dir] = (dirCounts[dir] || 0) + 1;
29
+ }
30
+ }
31
+ const topDirs = Object.entries(dirCounts)
32
+ .sort((a, b) => b[1] - a[1])
33
+ .slice(0, 5);
34
+ // Activity by day of week
35
+ const dayOfWeekCounts = db.prepare(`
36
+ SELECT
37
+ CASE CAST(strftime('%w', timestamp) AS INTEGER)
38
+ WHEN 0 THEN 'Sun'
39
+ WHEN 1 THEN 'Mon'
40
+ WHEN 2 THEN 'Tue'
41
+ WHEN 3 THEN 'Wed'
42
+ WHEN 4 THEN 'Thu'
43
+ WHEN 5 THEN 'Fri'
44
+ WHEN 6 THEN 'Sat'
45
+ END as day,
46
+ COUNT(*) as count
47
+ FROM entries
48
+ GROUP BY strftime('%w', timestamp)
49
+ ORDER BY CAST(strftime('%w', timestamp) AS INTEGER)
50
+ `).all();
51
+ // Activity by hour
52
+ const hourCounts = db.prepare(`
53
+ SELECT
54
+ CAST(strftime('%H', timestamp) AS INTEGER) as hour,
55
+ COUNT(*) as count
56
+ FROM entries
57
+ GROUP BY strftime('%H', timestamp)
58
+ ORDER BY hour
59
+ `).all();
60
+ // Average summary length
61
+ const avgLength = db.prepare('SELECT AVG(LENGTH(summary)) as avg FROM entries').get();
62
+ // Print stats
63
+ console.log(chalk.bold.green('\nšŸ“Š STAKEOUT STATISTICS\n'));
64
+ console.log(chalk.dim('─'.repeat(50)));
65
+ console.log(chalk.bold('\nOverview'));
66
+ console.log(` Total entries: ${chalk.cyan(total.count)}`);
67
+ console.log(` Today: ${chalk.cyan(todayCount.count)}`);
68
+ console.log(` This week: ${chalk.cyan(weekCount.count)}`);
69
+ console.log(` This month: ${chalk.cyan(monthCount.count)}`);
70
+ if (first && last) {
71
+ console.log(` First entry: ${chalk.dim(new Date(first.timestamp).toLocaleDateString())}`);
72
+ console.log(` Latest entry: ${chalk.dim(new Date(last.timestamp).toLocaleDateString())}`);
73
+ }
74
+ console.log(` Avg summary: ${chalk.dim(Math.round(avgLength.avg || 0) + ' chars')}`);
75
+ if (topDirs.length > 0) {
76
+ console.log(chalk.bold('\nMost Active Directories'));
77
+ for (const [dir, count] of topDirs) {
78
+ const bar = 'ā–ˆ'.repeat(Math.min(20, Math.round((count / topDirs[0][1]) * 20)));
79
+ console.log(` ${chalk.yellow(dir.padEnd(20))} ${chalk.green(bar)} ${count}`);
80
+ }
81
+ }
82
+ if (dayOfWeekCounts.length > 0) {
83
+ console.log(chalk.bold('\nActivity by Day'));
84
+ const maxDayCount = Math.max(...dayOfWeekCounts.map(d => d.count));
85
+ for (const { day, count } of dayOfWeekCounts) {
86
+ const bar = 'ā–ˆ'.repeat(Math.min(15, Math.round((count / maxDayCount) * 15)));
87
+ console.log(` ${chalk.cyan(day)} ${chalk.green(bar)} ${count}`);
88
+ }
89
+ }
90
+ if (hourCounts.length > 0) {
91
+ console.log(chalk.bold('\nPeak Hours'));
92
+ const sorted = [...hourCounts].sort((a, b) => b.count - a.count).slice(0, 5);
93
+ for (const { hour, count } of sorted) {
94
+ const time = `${hour.toString().padStart(2, '0')}:00`;
95
+ console.log(` ${chalk.magenta(time)} ${count} entries`);
96
+ }
97
+ }
98
+ console.log('');
99
+ }
@@ -0,0 +1,8 @@
1
+ interface TagOptions {
2
+ add?: string;
3
+ remove?: string;
4
+ list?: boolean;
5
+ }
6
+ export declare function tag(entryId: string, options: TagOptions): Promise<void>;
7
+ export declare function listTags(): Promise<void>;
8
+ export {};
@@ -0,0 +1,61 @@
1
+ import chalk from 'chalk';
2
+ import { getEntry, updateEntry, getDb } from '../lib/database.js';
3
+ export async function tag(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
+ const currentTags = entry.tags || [];
15
+ if (options.list || (!options.add && !options.remove)) {
16
+ console.log(chalk.bold(`\nTags for entry #${id}:\n`));
17
+ if (currentTags.length === 0) {
18
+ console.log(chalk.dim(' No tags'));
19
+ }
20
+ else {
21
+ currentTags.forEach(t => console.log(chalk.cyan(` • ${t}`)));
22
+ }
23
+ console.log('');
24
+ return;
25
+ }
26
+ if (options.add) {
27
+ const newTags = options.add.split(',').map(t => t.trim()).filter(t => t);
28
+ const updated = [...new Set([...currentTags, ...newTags])];
29
+ updateEntry(id, { tags: updated });
30
+ console.log(chalk.green(`āœ“ Added tags to entry #${id}: ${newTags.join(', ')}`));
31
+ }
32
+ if (options.remove) {
33
+ const toRemove = options.remove.split(',').map(t => t.trim().toLowerCase());
34
+ const updated = currentTags.filter(t => !toRemove.includes(t.toLowerCase()));
35
+ updateEntry(id, { tags: updated });
36
+ console.log(chalk.green(`āœ“ Removed tags from entry #${id}: ${options.remove}`));
37
+ }
38
+ }
39
+ export async function listTags() {
40
+ const db = getDb();
41
+ const rows = db.prepare('SELECT tags FROM entries WHERE tags != "[]"').all();
42
+ const tagCounts = {};
43
+ for (const row of rows) {
44
+ const tags = JSON.parse(row.tags);
45
+ for (const t of tags) {
46
+ tagCounts[t] = (tagCounts[t] || 0) + 1;
47
+ }
48
+ }
49
+ const sorted = Object.entries(tagCounts).sort((a, b) => b[1] - a[1]);
50
+ console.log(chalk.bold('\nšŸ“ All Tags\n'));
51
+ if (sorted.length === 0) {
52
+ console.log(chalk.dim('No tags found.'));
53
+ console.log(chalk.dim('Add tags with: stakeout tag <id> --add "feature,bugfix"'));
54
+ }
55
+ else {
56
+ for (const [tag, count] of sorted) {
57
+ console.log(` ${chalk.cyan(tag.padEnd(20))} ${chalk.dim(count + ' entries')}`);
58
+ }
59
+ }
60
+ console.log('');
61
+ }
@@ -0,0 +1 @@
1
+ export declare function tui(): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import chalk from 'chalk';
2
+ export async function tui() {
3
+ console.log(chalk.yellow('\nTUI mode not yet implemented.'));
4
+ console.log(chalk.dim('Use the web dashboard instead: stakeout dashboard\n'));
5
+ }
@@ -0,0 +1,6 @@
1
+ interface WatchOptions {
2
+ path?: string;
3
+ debounce?: string;
4
+ }
5
+ export declare function watch(options: WatchOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,101 @@
1
+ import chalk from 'chalk';
2
+ import { watch as fsWatch } from 'fs';
3
+ import { simpleGit } from 'simple-git';
4
+ import { loadConfig } from '../lib/config.js';
5
+ import { collectDiff } from '../lib/diff.js';
6
+ import { summarize } from '../lib/summarizer.js';
7
+ import { insertEntry, entryExists } from '../lib/database.js';
8
+ export async function watch(options) {
9
+ const config = loadConfig();
10
+ const watchPath = options.path || config.watched_path || process.cwd();
11
+ const debounceMs = options.debounce ? parseInt(options.debounce, 10) * 1000 : 30000; // Default 30s
12
+ const git = simpleGit(watchPath);
13
+ const isRepo = await git.checkIsRepo();
14
+ if (!isRepo) {
15
+ console.error(chalk.red('Not a git repository: ' + watchPath));
16
+ process.exit(1);
17
+ }
18
+ console.log(chalk.green('šŸ” STAKEOUT Watch Mode'));
19
+ console.log(chalk.dim('─'.repeat(40)));
20
+ console.log(chalk.dim(`Watching: ${watchPath}`));
21
+ console.log(chalk.dim(`Debounce: ${debounceMs / 1000}s`));
22
+ console.log(chalk.dim('Press Ctrl+C to stop'));
23
+ console.log(chalk.dim('─'.repeat(40)));
24
+ console.log('');
25
+ let timeout = null;
26
+ let lastRecordTime = 0;
27
+ const shouldIgnore = (filename) => {
28
+ const ignorePatterns = [
29
+ /node_modules/,
30
+ /\.git/,
31
+ /dist\//,
32
+ /build\//,
33
+ /\.lock$/,
34
+ /\.log$/,
35
+ /\.tmp$/,
36
+ /\.swp$/,
37
+ ];
38
+ return ignorePatterns.some(p => p.test(filename));
39
+ };
40
+ const tryRecord = async () => {
41
+ const now = Date.now();
42
+ // Don't record more often than debounce interval
43
+ if (now - lastRecordTime < debounceMs) {
44
+ return;
45
+ }
46
+ console.log(chalk.dim(`[${new Date().toLocaleTimeString()}] Checking for changes...`));
47
+ try {
48
+ const diff = await collectDiff(watchPath);
49
+ if (!diff || diff.files.length === 0) {
50
+ console.log(chalk.dim(' No significant changes'));
51
+ return;
52
+ }
53
+ if (entryExists(diff.diff_hash)) {
54
+ console.log(chalk.dim(' Already recorded'));
55
+ return;
56
+ }
57
+ console.log(chalk.yellow(` Found ${diff.files.length} changed files`));
58
+ console.log(chalk.dim(' Generating summary...'));
59
+ const summary = await summarize(diff);
60
+ const entry = {
61
+ timestamp: new Date().toISOString(),
62
+ files_changed: diff.files,
63
+ directories: diff.directories,
64
+ summary,
65
+ diff_hash: diff.diff_hash,
66
+ commit_hash: diff.commit_hash,
67
+ commit_message: diff.commit_message
68
+ };
69
+ const id = insertEntry(entry);
70
+ lastRecordTime = now;
71
+ console.log(chalk.green(` āœ“ Recorded entry #${id}`));
72
+ console.log(chalk.cyan(` ${summary.slice(0, 60)}${summary.length > 60 ? '...' : ''}`));
73
+ console.log('');
74
+ }
75
+ catch (err) {
76
+ console.error(chalk.red(` Error: ${err.message}`));
77
+ }
78
+ };
79
+ const scheduleRecord = () => {
80
+ if (timeout) {
81
+ clearTimeout(timeout);
82
+ }
83
+ timeout = setTimeout(tryRecord, 2000); // Wait 2s after last change
84
+ };
85
+ // Watch for changes
86
+ const watcher = fsWatch(watchPath, { recursive: true }, (eventType, filename) => {
87
+ if (!filename || shouldIgnore(filename)) {
88
+ return;
89
+ }
90
+ console.log(chalk.dim(`[${new Date().toLocaleTimeString()}] ${eventType}: ${filename}`));
91
+ scheduleRecord();
92
+ });
93
+ // Handle graceful shutdown
94
+ process.on('SIGINT', () => {
95
+ console.log(chalk.dim('\nStopping watch...'));
96
+ watcher.close();
97
+ process.exit(0);
98
+ });
99
+ // Keep the process running
100
+ await new Promise(() => { });
101
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { record } from './commands/record.js';
4
+ import { log } from './commands/log.js';
5
+ import { config } from './commands/config.js';
6
+ import { dashboard } from './commands/dashboard.js';
7
+ import { tui } from './commands/tui.js';
8
+ import { hook } from './commands/hook.js';
9
+ import { digest } from './commands/digest.js';
10
+ import { watch } from './commands/watch.js';
11
+ import { search } from './commands/search.js';
12
+ import { exportCmd } from './commands/export.js';
13
+ import { stats } from './commands/stats.js';
14
+ import { clear } from './commands/clear.js';
15
+ import { init } from './commands/init.js';
16
+ import { chat } from './commands/chat.js';
17
+ import { tag, listTags } from './commands/tag.js';
18
+ import { note } from './commands/note.js';
19
+ import { repo } from './commands/repo.js';
20
+ import { closeDb } from './lib/database.js';
21
+ const program = new Command();
22
+ program
23
+ .name('stakeout')
24
+ .description('šŸ” Surveillance ops for your codebase - AI-powered change tracking')
25
+ .version('0.1.0');
26
+ program
27
+ .command('init')
28
+ .description('Initialize STAKEOUT for a repository')
29
+ .option('-p, --path <path>', 'Path to repository (default: current directory)')
30
+ .option('--no-hook', 'Skip installing git hook')
31
+ .action(async (options) => {
32
+ await init(options);
33
+ closeDb();
34
+ });
35
+ program
36
+ .command('record')
37
+ .description('Record and summarize current changes')
38
+ .option('-p, --path <path>', 'Path to git repository (default: current directory)')
39
+ .option('-c, --last-commit', 'Summarize the last commit instead of working changes')
40
+ .action(async (options) => {
41
+ await record({
42
+ path: options.path,
43
+ lastCommit: options.lastCommit
44
+ });
45
+ closeDb();
46
+ });
47
+ program
48
+ .command('log')
49
+ .description('View recorded change summaries')
50
+ .option('-n, --limit <number>', 'Number of entries to show (default: 10)')
51
+ .option('-s, --since <time>', 'Show entries since (e.g. "3 days", "1 week")')
52
+ .option('-p, --path <path>', 'Filter by file/directory path')
53
+ .option('-f, --favorites', 'Show only favorites')
54
+ .option('-t, --tag <tag>', 'Filter by tag')
55
+ .action(async (options) => {
56
+ await log(options);
57
+ closeDb();
58
+ });
59
+ program
60
+ .command('search <query>')
61
+ .description('Search through recorded summaries')
62
+ .option('-n, --limit <number>', 'Max results to show (default: 20)')
63
+ .action(async (query, options) => {
64
+ await search(query, options);
65
+ closeDb();
66
+ });
67
+ program
68
+ .command('chat')
69
+ .description('AI chat - ask questions about your codebase history')
70
+ .option('--clear', 'Clear chat history')
71
+ .action(async (options) => {
72
+ await chat(options);
73
+ closeDb();
74
+ });
75
+ program
76
+ .command('stats')
77
+ .description('Show detailed statistics and analytics')
78
+ .action(async () => {
79
+ await stats();
80
+ closeDb();
81
+ });
82
+ program
83
+ .command('digest')
84
+ .description('Generate a high-level digest of recent activity')
85
+ .option('-s, --since <time>', 'Time period (e.g. "1 week", "2 weeks", "1 month")')
86
+ .option('-p, --path <path>', 'Filter by file/directory path')
87
+ .action(async (options) => {
88
+ await digest(options);
89
+ closeDb();
90
+ });
91
+ program
92
+ .command('tag <id>')
93
+ .description('Manage tags for an entry')
94
+ .option('-a, --add <tags>', 'Add tags (comma-separated)')
95
+ .option('-r, --remove <tags>', 'Remove tags (comma-separated)')
96
+ .option('-l, --list', 'List tags on entry')
97
+ .action(async (id, options) => {
98
+ await tag(id, options);
99
+ closeDb();
100
+ });
101
+ program
102
+ .command('tags')
103
+ .description('List all tags')
104
+ .action(async () => {
105
+ await listTags();
106
+ closeDb();
107
+ });
108
+ program
109
+ .command('note <id>')
110
+ .description('Add notes or mark entries')
111
+ .option('-n, --note <text>', 'Set note text')
112
+ .option('-f, --favorite', 'Mark as favorite')
113
+ .option('-u, --unfavorite', 'Remove favorite')
114
+ .option('-b, --breaking', 'Mark as breaking change')
115
+ .option('--not-breaking', 'Unmark as breaking change')
116
+ .action(async (id, options) => {
117
+ await note(id, options);
118
+ closeDb();
119
+ });
120
+ program
121
+ .command('repo')
122
+ .description('Manage tracked repositories')
123
+ .option('-a, --add <path>', 'Add a repository')
124
+ .option('-r, --remove <name>', 'Remove a repository')
125
+ .option('-l, --list', 'List repositories')
126
+ .action(async (options) => {
127
+ await repo(options);
128
+ closeDb();
129
+ });
130
+ program
131
+ .command('export')
132
+ .description('Export entries to file')
133
+ .option('-f, --format <format>', 'Format: markdown, json, csv (default: markdown)')
134
+ .option('-s, --since <time>', 'Export entries since (e.g. "1 month")')
135
+ .option('-p, --path <path>', 'Filter by file/directory path')
136
+ .option('-o, --output <file>', 'Output filename')
137
+ .action(async (options) => {
138
+ await exportCmd(options);
139
+ closeDb();
140
+ });
141
+ program
142
+ .command('clear')
143
+ .description('Delete old entries')
144
+ .option('--before <time>', 'Delete entries before (e.g. "30 days", "2024-01-01")')
145
+ .option('--all', 'Delete ALL entries')
146
+ .option('-y, --yes', 'Skip confirmation')
147
+ .action(async (options) => {
148
+ await clear(options);
149
+ closeDb();
150
+ });
151
+ program
152
+ .command('config')
153
+ .description('View or update configuration')
154
+ .option('--show', 'Show current configuration')
155
+ .option('--provider <provider>', 'Set LLM provider (ollama or openai)')
156
+ .option('--ollama-model <model>', 'Set Ollama model (e.g. llama3, mistral, codellama)')
157
+ .option('--ollama-host <url>', 'Set Ollama host URL')
158
+ .option('--openai-key <key>', 'Set OpenAI API key')
159
+ .option('--openai-model <model>', 'Set OpenAI model (e.g. gpt-4o-mini, gpt-4o)')
160
+ .action(async (options) => {
161
+ await config(options);
162
+ });
163
+ program
164
+ .command('dashboard')
165
+ .alias('dash')
166
+ .description('Open the STAKEOUT Command Center (web UI)')
167
+ .option('-p, --port <port>', 'Port to run on (default: 3333)')
168
+ .action(async (options) => {
169
+ await dashboard(options);
170
+ });
171
+ program
172
+ .command('ui')
173
+ .alias('tui')
174
+ .description('Open the terminal UI (TUI mode)')
175
+ .action(async () => {
176
+ await tui();
177
+ closeDb();
178
+ });
179
+ program
180
+ .command('hook')
181
+ .description('Install/remove git post-commit hook for auto-recording')
182
+ .option('-p, --path <path>', 'Path to git repository (default: current directory)')
183
+ .option('-r, --remove', 'Remove the hook instead of installing')
184
+ .action(async (options) => {
185
+ await hook(options);
186
+ });
187
+ program
188
+ .command('watch')
189
+ .description('Watch for changes and record automatically')
190
+ .option('-p, --path <path>', 'Path to watch (default: current directory)')
191
+ .option('-d, --debounce <seconds>', 'Minimum seconds between recordings (default: 30)')
192
+ .action(async (options) => {
193
+ await watch(options);
194
+ });
195
+ program.parse();
@@ -0,0 +1,5 @@
1
+ import type { StakeoutConfig } from '../types/index.js';
2
+ export declare function ensureConfigDir(): void;
3
+ export declare function loadConfig(): StakeoutConfig;
4
+ export declare function saveConfig(config: StakeoutConfig): void;
5
+ export declare function getConfigPath(): string;