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,402 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { getEntries, insertEntry, getDb, getEntry, updateEntry, getRepos, addChatMessage } from '../lib/database.js';
|
|
6
|
+
import { collectDiff, collectLastCommit } from '../lib/diff.js';
|
|
7
|
+
import { summarize } from '../lib/summarizer.js';
|
|
8
|
+
import { loadConfig, saveConfig } from '../lib/config.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
export function createServer(port = 3333) {
|
|
12
|
+
const app = express();
|
|
13
|
+
app.use(cors());
|
|
14
|
+
app.use(express.json());
|
|
15
|
+
app.use(express.static(join(__dirname, 'public')));
|
|
16
|
+
// API Routes
|
|
17
|
+
// Get entries
|
|
18
|
+
app.get('/api/entries', (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
|
|
21
|
+
const since = req.query.since;
|
|
22
|
+
const path = req.query.path;
|
|
23
|
+
const entries = getEntries({ limit, since, path });
|
|
24
|
+
res.json({ entries });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
res.status(500).json({ error: error.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Get stats
|
|
31
|
+
app.get('/api/stats', (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const db = getDb();
|
|
34
|
+
const totalEntries = db.prepare('SELECT COUNT(*) as count FROM entries').get();
|
|
35
|
+
const today = new Date();
|
|
36
|
+
today.setHours(0, 0, 0, 0);
|
|
37
|
+
const todayEntries = db.prepare('SELECT COUNT(*) as count FROM entries WHERE timestamp >= ?').get(today.toISOString());
|
|
38
|
+
const weekAgo = new Date();
|
|
39
|
+
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
40
|
+
const weekEntries = db.prepare('SELECT COUNT(*) as count FROM entries WHERE timestamp >= ?').get(weekAgo.toISOString());
|
|
41
|
+
// Activity by day (last 14 days)
|
|
42
|
+
const activity = db.prepare(`
|
|
43
|
+
SELECT DATE(timestamp) as date, COUNT(*) as count
|
|
44
|
+
FROM entries
|
|
45
|
+
WHERE timestamp >= datetime('now', '-14 days')
|
|
46
|
+
GROUP BY DATE(timestamp)
|
|
47
|
+
ORDER BY date ASC
|
|
48
|
+
`).all();
|
|
49
|
+
res.json({
|
|
50
|
+
total: totalEntries.count,
|
|
51
|
+
today: todayEntries.count,
|
|
52
|
+
thisWeek: weekEntries.count,
|
|
53
|
+
activity
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
res.status(500).json({ error: error.message });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// Record changes
|
|
61
|
+
app.post('/api/record', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const { path, lastCommit } = req.body;
|
|
64
|
+
const diff = lastCommit
|
|
65
|
+
? await collectLastCommit(path)
|
|
66
|
+
: await collectDiff(path);
|
|
67
|
+
if (!diff) {
|
|
68
|
+
res.json({ success: false, message: 'No changes detected' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const summary = await summarize(diff);
|
|
72
|
+
const entry = {
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
files_changed: diff.files,
|
|
75
|
+
directories: diff.directories,
|
|
76
|
+
summary,
|
|
77
|
+
diff_hash: diff.diff_hash,
|
|
78
|
+
commit_hash: diff.commit_hash,
|
|
79
|
+
commit_message: diff.commit_message
|
|
80
|
+
};
|
|
81
|
+
const id = insertEntry(entry);
|
|
82
|
+
res.json({
|
|
83
|
+
success: true,
|
|
84
|
+
entry: { ...entry, id }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
res.status(500).json({ error: error.message });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Get config
|
|
92
|
+
app.get('/api/config', (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const config = loadConfig();
|
|
95
|
+
// Don't expose full API key
|
|
96
|
+
if (config.openai_api_key) {
|
|
97
|
+
config.openai_api_key = '****' + config.openai_api_key.slice(-4);
|
|
98
|
+
}
|
|
99
|
+
res.json(config);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
res.status(500).json({ error: error.message });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Update config
|
|
106
|
+
app.patch('/api/config', (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
const current = loadConfig();
|
|
109
|
+
const updated = { ...current, ...req.body };
|
|
110
|
+
saveConfig(updated);
|
|
111
|
+
res.json({ success: true });
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
res.status(500).json({ error: error.message });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// Search entries
|
|
118
|
+
app.get('/api/search', (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const query = req.query.q;
|
|
121
|
+
if (!query) {
|
|
122
|
+
res.json({ entries: [] });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const db = getDb();
|
|
126
|
+
const pattern = `%${query}%`;
|
|
127
|
+
const stmt = db.prepare(`
|
|
128
|
+
SELECT * FROM entries
|
|
129
|
+
WHERE summary LIKE ?
|
|
130
|
+
OR commit_message LIKE ?
|
|
131
|
+
OR files_changed LIKE ?
|
|
132
|
+
OR directories LIKE ?
|
|
133
|
+
ORDER BY timestamp DESC
|
|
134
|
+
LIMIT 50
|
|
135
|
+
`);
|
|
136
|
+
const rows = stmt.all(pattern, pattern, pattern, pattern);
|
|
137
|
+
const entries = rows.map(row => ({
|
|
138
|
+
id: row.id,
|
|
139
|
+
timestamp: row.timestamp,
|
|
140
|
+
files_changed: JSON.parse(row.files_changed),
|
|
141
|
+
directories: JSON.parse(row.directories),
|
|
142
|
+
summary: row.summary,
|
|
143
|
+
diff_hash: row.diff_hash,
|
|
144
|
+
commit_hash: row.commit_hash,
|
|
145
|
+
commit_message: row.commit_message
|
|
146
|
+
}));
|
|
147
|
+
res.json({ entries });
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
res.status(500).json({ error: error.message });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// Detailed stats
|
|
154
|
+
app.get('/api/stats/detailed', (req, res) => {
|
|
155
|
+
try {
|
|
156
|
+
const db = getDb();
|
|
157
|
+
const total = db.prepare('SELECT COUNT(*) as count FROM entries').get().count;
|
|
158
|
+
const today = new Date();
|
|
159
|
+
today.setHours(0, 0, 0, 0);
|
|
160
|
+
const todayCount = db.prepare('SELECT COUNT(*) as count FROM entries WHERE timestamp >= ?').get(today.toISOString()).count;
|
|
161
|
+
const weekAgo = new Date();
|
|
162
|
+
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
163
|
+
const thisWeek = db.prepare('SELECT COUNT(*) as count FROM entries WHERE timestamp >= ?').get(weekAgo.toISOString()).count;
|
|
164
|
+
const monthAgo = new Date();
|
|
165
|
+
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
|
166
|
+
const thisMonth = db.prepare('SELECT COUNT(*) as count FROM entries WHERE timestamp >= ?').get(monthAgo.toISOString()).count;
|
|
167
|
+
const first = db.prepare('SELECT timestamp FROM entries ORDER BY timestamp ASC LIMIT 1').get();
|
|
168
|
+
// Top directories
|
|
169
|
+
const allDirs = db.prepare('SELECT directories FROM entries').all();
|
|
170
|
+
const dirCounts = {};
|
|
171
|
+
for (const row of allDirs) {
|
|
172
|
+
const dirs = JSON.parse(row.directories);
|
|
173
|
+
for (const dir of dirs) {
|
|
174
|
+
dirCounts[dir] = (dirCounts[dir] || 0) + 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const topDirs = Object.entries(dirCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
178
|
+
// Activity by day of week
|
|
179
|
+
const byDayOfWeek = db.prepare(`
|
|
180
|
+
SELECT
|
|
181
|
+
CASE CAST(strftime('%w', timestamp) AS INTEGER)
|
|
182
|
+
WHEN 0 THEN 'Sun'
|
|
183
|
+
WHEN 1 THEN 'Mon'
|
|
184
|
+
WHEN 2 THEN 'Tue'
|
|
185
|
+
WHEN 3 THEN 'Wed'
|
|
186
|
+
WHEN 4 THEN 'Thu'
|
|
187
|
+
WHEN 5 THEN 'Fri'
|
|
188
|
+
WHEN 6 THEN 'Sat'
|
|
189
|
+
END as day,
|
|
190
|
+
COUNT(*) as count
|
|
191
|
+
FROM entries
|
|
192
|
+
GROUP BY strftime('%w', timestamp)
|
|
193
|
+
ORDER BY CAST(strftime('%w', timestamp) AS INTEGER)
|
|
194
|
+
`).all();
|
|
195
|
+
// Peak hours
|
|
196
|
+
const peakHours = db.prepare(`
|
|
197
|
+
SELECT CAST(strftime('%H', timestamp) AS INTEGER) as hour, COUNT(*) as count
|
|
198
|
+
FROM entries
|
|
199
|
+
GROUP BY strftime('%H', timestamp)
|
|
200
|
+
ORDER BY count DESC
|
|
201
|
+
`).all();
|
|
202
|
+
res.json({
|
|
203
|
+
total,
|
|
204
|
+
today: todayCount,
|
|
205
|
+
thisWeek,
|
|
206
|
+
thisMonth,
|
|
207
|
+
firstEntry: first?.timestamp,
|
|
208
|
+
topDirs,
|
|
209
|
+
byDayOfWeek,
|
|
210
|
+
peakHours
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
res.status(500).json({ error: error.message });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// Export entries
|
|
218
|
+
app.get('/api/export', (req, res) => {
|
|
219
|
+
try {
|
|
220
|
+
const format = req.query.format || 'markdown';
|
|
221
|
+
const since = req.query.since;
|
|
222
|
+
const entries = getEntries({ limit: 1000, since });
|
|
223
|
+
if (format === 'json') {
|
|
224
|
+
res.setHeader('Content-Type', 'application/json');
|
|
225
|
+
res.setHeader('Content-Disposition', 'attachment; filename=stakeout-export.json');
|
|
226
|
+
res.send(JSON.stringify(entries, null, 2));
|
|
227
|
+
}
|
|
228
|
+
else if (format === 'csv') {
|
|
229
|
+
res.setHeader('Content-Type', 'text/csv');
|
|
230
|
+
res.setHeader('Content-Disposition', 'attachment; filename=stakeout-export.csv');
|
|
231
|
+
const headers = ['id', 'timestamp', 'commit_hash', 'summary', 'directories', 'file_count'];
|
|
232
|
+
const rows = entries.map(e => [
|
|
233
|
+
e.id,
|
|
234
|
+
e.timestamp,
|
|
235
|
+
e.commit_hash || '',
|
|
236
|
+
`"${e.summary.replace(/"/g, '""')}"`,
|
|
237
|
+
`"${e.directories.join(', ')}"`,
|
|
238
|
+
e.files_changed.length
|
|
239
|
+
]);
|
|
240
|
+
res.send([headers.join(','), ...rows.map(r => r.join(','))].join('\n'));
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
res.setHeader('Content-Type', 'text/markdown');
|
|
244
|
+
res.setHeader('Content-Disposition', 'attachment; filename=stakeout-export.md');
|
|
245
|
+
let md = `# STAKEOUT Export\n\nGenerated: ${new Date().toISOString()}\nEntries: ${entries.length}\n\n---\n\n`;
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
md += `## Entry #${entry.id}\n\n`;
|
|
248
|
+
md += `**Date:** ${new Date(entry.timestamp).toLocaleString()}\n`;
|
|
249
|
+
if (entry.commit_hash)
|
|
250
|
+
md += `**Commit:** \`${entry.commit_hash.slice(0, 7)}\`\n`;
|
|
251
|
+
md += `\n### Summary\n\n${entry.summary}\n\n`;
|
|
252
|
+
if (entry.directories.length > 0) {
|
|
253
|
+
md += `### Directories\n\n${entry.directories.map(d => `- \`${d}\``).join('\n')}\n\n`;
|
|
254
|
+
}
|
|
255
|
+
md += `---\n\n`;
|
|
256
|
+
}
|
|
257
|
+
res.send(md);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
res.status(500).json({ error: error.message });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
// Get single entry
|
|
265
|
+
app.get('/api/entries/:id', (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const id = parseInt(req.params.id, 10);
|
|
268
|
+
const entry = getEntry(id);
|
|
269
|
+
if (!entry) {
|
|
270
|
+
res.status(404).json({ error: 'Entry not found' });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
res.json(entry);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
res.status(500).json({ error: error.message });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// Update entry (tags, favorite, notes, breaking)
|
|
280
|
+
app.patch('/api/entries/:id', (req, res) => {
|
|
281
|
+
try {
|
|
282
|
+
const id = parseInt(req.params.id, 10);
|
|
283
|
+
const { tags, favorite, notes, is_breaking } = req.body;
|
|
284
|
+
updateEntry(id, { tags, favorite, notes, is_breaking });
|
|
285
|
+
res.json({ success: true });
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
res.status(500).json({ error: error.message });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
// Get velocity data
|
|
292
|
+
app.get('/api/velocity', (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const db = getDb();
|
|
295
|
+
const days = parseInt(req.query.days || '30', 10);
|
|
296
|
+
const velocity = db.prepare(`
|
|
297
|
+
SELECT DATE(timestamp) as date, COUNT(*) as count
|
|
298
|
+
FROM entries
|
|
299
|
+
WHERE timestamp >= datetime('now', '-' || ? || ' days')
|
|
300
|
+
GROUP BY DATE(timestamp)
|
|
301
|
+
ORDER BY date ASC
|
|
302
|
+
`).all(days);
|
|
303
|
+
const counts = velocity.map(v => v.count);
|
|
304
|
+
const avg = counts.length > 0 ? counts.reduce((a, b) => a + b, 0) / counts.length : 0;
|
|
305
|
+
const max = counts.length > 0 ? Math.max(...counts) : 0;
|
|
306
|
+
const recent = velocity.slice(-7);
|
|
307
|
+
const previous = velocity.slice(-14, -7);
|
|
308
|
+
const recentAvg = recent.length > 0 ? recent.reduce((a, b) => a + b.count, 0) / recent.length : 0;
|
|
309
|
+
const previousAvg = previous.length > 0 ? previous.reduce((a, b) => a + b.count, 0) / previous.length : 0;
|
|
310
|
+
const trend = previousAvg > 0 ? ((recentAvg - previousAvg) / previousAvg) * 100 : 0;
|
|
311
|
+
res.json({ velocity, average: Math.round(avg * 10) / 10, max, trend: Math.round(trend) });
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
res.status(500).json({ error: error.message });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
// Get repos
|
|
318
|
+
app.get('/api/repos', (req, res) => {
|
|
319
|
+
try {
|
|
320
|
+
const repos = getRepos();
|
|
321
|
+
res.json({ repos });
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
res.status(500).json({ error: error.message });
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
// Chat API
|
|
328
|
+
app.post('/api/chat', async (req, res) => {
|
|
329
|
+
try {
|
|
330
|
+
const { message } = req.body;
|
|
331
|
+
if (!message) {
|
|
332
|
+
res.status(400).json({ error: 'Message required' });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const db = getDb();
|
|
336
|
+
const recentEntries = getEntries({ limit: 10 });
|
|
337
|
+
const total = db.prepare('SELECT COUNT(*) as count FROM entries').get().count;
|
|
338
|
+
const keywords = message.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
339
|
+
let relevantEntries = [];
|
|
340
|
+
if (keywords.length > 0) {
|
|
341
|
+
const conditions = keywords.map(() => '(summary LIKE ? OR directories LIKE ?)').join(' OR ');
|
|
342
|
+
const params = keywords.flatMap((k) => [`%${k}%`, `%${k}%`]);
|
|
343
|
+
relevantEntries = db.prepare(`SELECT * FROM entries WHERE ${conditions} ORDER BY timestamp DESC LIMIT 5`).all(...params);
|
|
344
|
+
}
|
|
345
|
+
let context = `Total entries: ${total}\n\n`;
|
|
346
|
+
if (relevantEntries.length > 0) {
|
|
347
|
+
context += 'Relevant entries:\n';
|
|
348
|
+
for (const e of relevantEntries)
|
|
349
|
+
context += `[#${e.id}] ${e.summary}\n`;
|
|
350
|
+
context += '\n';
|
|
351
|
+
}
|
|
352
|
+
context += 'Recent entries:\n';
|
|
353
|
+
for (const e of recentEntries.slice(0, 5))
|
|
354
|
+
context += `[#${e.id}] ${e.summary}\n`;
|
|
355
|
+
const config = loadConfig();
|
|
356
|
+
let response;
|
|
357
|
+
const systemPrompt = `You are STAKEOUT AI. Answer questions about the codebase history. Be concise.`;
|
|
358
|
+
if (config.llm_provider === 'ollama') {
|
|
359
|
+
const { Ollama } = await import('ollama');
|
|
360
|
+
const ollama = new Ollama({ host: config.ollama_host });
|
|
361
|
+
const result = await ollama.chat({
|
|
362
|
+
model: config.ollama_model,
|
|
363
|
+
messages: [
|
|
364
|
+
{ role: 'system', content: systemPrompt },
|
|
365
|
+
{ role: 'system', content: `Context:\n${context}` },
|
|
366
|
+
{ role: 'user', content: message }
|
|
367
|
+
]
|
|
368
|
+
});
|
|
369
|
+
response = result.message.content;
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const OpenAI = (await import('openai')).default;
|
|
373
|
+
if (!config.openai_api_key)
|
|
374
|
+
throw new Error('OpenAI key not configured');
|
|
375
|
+
const openai = new OpenAI({ apiKey: config.openai_api_key });
|
|
376
|
+
const result = await openai.chat.completions.create({
|
|
377
|
+
model: config.openai_model,
|
|
378
|
+
messages: [
|
|
379
|
+
{ role: 'system', content: systemPrompt },
|
|
380
|
+
{ role: 'system', content: `Context:\n${context}` },
|
|
381
|
+
{ role: 'user', content: message }
|
|
382
|
+
]
|
|
383
|
+
});
|
|
384
|
+
response = result.choices[0]?.message?.content || 'Unable to generate response';
|
|
385
|
+
}
|
|
386
|
+
addChatMessage('user', message);
|
|
387
|
+
addChatMessage('assistant', response);
|
|
388
|
+
res.json({ response });
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
res.status(500).json({ error: error.message });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
// Serve the dashboard for the root route
|
|
395
|
+
app.get('/', (req, res) => {
|
|
396
|
+
res.sendFile(join(__dirname, 'public', 'index.html'));
|
|
397
|
+
});
|
|
398
|
+
const server = app.listen(port, () => {
|
|
399
|
+
console.log(`\n🔍 STAKEOUT Dashboard running at http://localhost:${port}\n`);
|
|
400
|
+
});
|
|
401
|
+
return server;
|
|
402
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stakeout-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Surveillance ops for your codebase - AI-powered change tracking",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
8
|
+
"author": "Casey",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"git",
|
|
11
|
+
"changelog",
|
|
12
|
+
"ai",
|
|
13
|
+
"llm",
|
|
14
|
+
"ollama",
|
|
15
|
+
"openai",
|
|
16
|
+
"diff",
|
|
17
|
+
"summary",
|
|
18
|
+
"developer-tools",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Kaidorespy/stakeout.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Kaidorespy/stakeout/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/Kaidorespy/stakeout#readme",
|
|
29
|
+
"bin": {
|
|
30
|
+
"stakeout": "./dist/index.js",
|
|
31
|
+
"sto": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc && cp -r src/web/public dist/web/",
|
|
43
|
+
"dev": "tsc --watch",
|
|
44
|
+
"start": "node dist/index.js",
|
|
45
|
+
"dashboard": "node dist/index.js dashboard"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"better-sqlite3": "^11.0.0",
|
|
49
|
+
"chalk": "^5.3.0",
|
|
50
|
+
"commander": "^12.0.0",
|
|
51
|
+
"cors": "^2.8.6",
|
|
52
|
+
"express": "^5.2.1",
|
|
53
|
+
"ink": "^6.6.0",
|
|
54
|
+
"ink-spinner": "^5.0.0",
|
|
55
|
+
"ink-text-input": "^6.0.0",
|
|
56
|
+
"ollama": "^0.5.0",
|
|
57
|
+
"openai": "^4.0.0",
|
|
58
|
+
"react": "^19.2.4",
|
|
59
|
+
"simple-git": "^3.22.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/better-sqlite3": "^7.6.9",
|
|
63
|
+
"@types/cors": "^2.8.19",
|
|
64
|
+
"@types/express": "^5.0.6",
|
|
65
|
+
"@types/node": "^20.11.0",
|
|
66
|
+
"@types/react": "^19.2.13",
|
|
67
|
+
"typescript": "^5.3.0"
|
|
68
|
+
}
|
|
69
|
+
}
|