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.
- package/LICENSE +131 -0
- package/README.md +152 -0
- package/dist/commands/chat.d.ts +5 -0
- package/dist/commands/chat.js +162 -0
- package/dist/commands/clear.d.ts +7 -0
- package/dist/commands/clear.js +89 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +64 -0
- package/dist/commands/dashboard.d.ts +5 -0
- package/dist/commands/dashboard.js +9 -0
- package/dist/commands/digest.d.ts +6 -0
- package/dist/commands/digest.js +113 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +118 -0
- package/dist/commands/hook.d.ts +6 -0
- package/dist/commands/hook.js +57 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/log.d.ts +9 -0
- package/dist/commands/log.js +103 -0
- package/dist/commands/note.d.ts +9 -0
- package/dist/commands/note.js +48 -0
- package/dist/commands/record.d.ts +6 -0
- package/dist/commands/record.js +106 -0
- package/dist/commands/repo.d.ts +7 -0
- package/dist/commands/repo.js +60 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +69 -0
- package/dist/commands/stats.d.ts +1 -0
- package/dist/commands/stats.js +99 -0
- package/dist/commands/tag.d.ts +8 -0
- package/dist/commands/tag.js +61 -0
- package/dist/commands/tui.d.ts +1 -0
- package/dist/commands/tui.js +5 -0
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +101 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +195 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.js +51 -0
- package/dist/lib/database.d.ts +31 -0
- package/dist/lib/database.js +222 -0
- package/dist/lib/diff.d.ts +3 -0
- package/dist/lib/diff.js +118 -0
- package/dist/lib/summarizer.d.ts +2 -0
- package/dist/lib/summarizer.js +90 -0
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +125 -0
- package/dist/types/index.d.ts +38 -0
- package/dist/types/index.js +1 -0
- package/dist/web/public/app.js +387 -0
- package/dist/web/public/index.html +131 -0
- package/dist/web/public/styles.css +571 -0
- package/dist/web/server.d.ts +1 -0
- package/dist/web/server.js +402 -0
- 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,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,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,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
|
+
}
|
package/dist/index.d.ts
ADDED
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;
|