@simonfestl/husky-cli 1.36.0 → 1.37.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/dist/commands/config.js +12 -4
- package/dist/commands/insights.d.ts +12 -0
- package/dist/commands/insights.js +397 -0
- package/dist/commands/llm-context.js +38 -0
- package/dist/commands/session.d.ts +12 -0
- package/dist/commands/session.js +665 -0
- package/dist/index.js +4 -0
- package/dist/types/session.d.ts +80 -0
- package/dist/types/session.js +4 -0
- package/package.json +1 -1
package/dist/commands/config.js
CHANGED
|
@@ -41,11 +41,19 @@ function validateApiUrl(url) {
|
|
|
41
41
|
}
|
|
42
42
|
export function getConfig() {
|
|
43
43
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
let config = {};
|
|
45
|
+
if (existsSync(CONFIG_FILE)) {
|
|
46
|
+
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
47
|
+
config = JSON.parse(content);
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
// Environment variable overrides (useful for testing)
|
|
50
|
+
if (process.env.HUSKY_API_URL) {
|
|
51
|
+
config.apiUrl = process.env.HUSKY_API_URL;
|
|
52
|
+
}
|
|
53
|
+
if (process.env.HUSKY_API_KEY) {
|
|
54
|
+
config.apiKey = process.env.HUSKY_API_KEY;
|
|
55
|
+
}
|
|
56
|
+
return config;
|
|
49
57
|
}
|
|
50
58
|
catch {
|
|
51
59
|
return {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insights Command - Aggregated Session Analytics
|
|
3
|
+
*
|
|
4
|
+
* Part of the self-improving system. Provides commands for:
|
|
5
|
+
* - Viewing recent errors across sessions
|
|
6
|
+
* - Listing learnings
|
|
7
|
+
* - Showing suggested improvements
|
|
8
|
+
* - Aggregated statistics
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
export declare const insightsCommand: Command;
|
|
12
|
+
export default insightsCommand;
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insights Command - Aggregated Session Analytics
|
|
3
|
+
*
|
|
4
|
+
* Part of the self-improving system. Provides commands for:
|
|
5
|
+
* - Viewing recent errors across sessions
|
|
6
|
+
* - Listing learnings
|
|
7
|
+
* - Showing suggested improvements
|
|
8
|
+
* - Aggregated statistics
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { getConfig } from "./config.js";
|
|
12
|
+
import { getApiClient } from "../lib/api-client.js";
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Helpers
|
|
15
|
+
// ============================================================================
|
|
16
|
+
function truncate(text, maxLen) {
|
|
17
|
+
if (text.length <= maxLen)
|
|
18
|
+
return text;
|
|
19
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
20
|
+
}
|
|
21
|
+
function formatDate(date) {
|
|
22
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
23
|
+
return d.toLocaleString("de-DE", {
|
|
24
|
+
day: "2-digit",
|
|
25
|
+
month: "2-digit",
|
|
26
|
+
hour: "2-digit",
|
|
27
|
+
minute: "2-digit",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function parseDuration(since) {
|
|
31
|
+
const match = since.match(/^(\d+)([dhwm])$/);
|
|
32
|
+
if (!match) {
|
|
33
|
+
throw new Error("Invalid duration format. Use: 7d, 24h, 2w, 1m");
|
|
34
|
+
}
|
|
35
|
+
const value = parseInt(match[1], 10);
|
|
36
|
+
const unit = match[2];
|
|
37
|
+
const now = new Date();
|
|
38
|
+
switch (unit) {
|
|
39
|
+
case "h":
|
|
40
|
+
return new Date(now.getTime() - value * 60 * 60 * 1000);
|
|
41
|
+
case "d":
|
|
42
|
+
return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
|
|
43
|
+
case "w":
|
|
44
|
+
return new Date(now.getTime() - value * 7 * 24 * 60 * 60 * 1000);
|
|
45
|
+
case "m":
|
|
46
|
+
return new Date(now.getTime() - value * 30 * 24 * 60 * 60 * 1000);
|
|
47
|
+
default:
|
|
48
|
+
throw new Error("Invalid duration unit. Use: h, d, w, m");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function ensureConfig() {
|
|
52
|
+
const config = getConfig();
|
|
53
|
+
if (!config.apiUrl) {
|
|
54
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Command Definition
|
|
61
|
+
// ============================================================================
|
|
62
|
+
export const insightsCommand = new Command("insights")
|
|
63
|
+
.description("View aggregated session insights and analytics");
|
|
64
|
+
// -------------------------------------------------------------------------
|
|
65
|
+
// Errors
|
|
66
|
+
// -------------------------------------------------------------------------
|
|
67
|
+
insightsCommand
|
|
68
|
+
.command("errors")
|
|
69
|
+
.description("List recent errors from analyzed sessions")
|
|
70
|
+
.option("--since <duration>", "Time window (e.g., 7d, 24h, 2w)", "7d")
|
|
71
|
+
.option("-l, --limit <num>", "Max errors to show", "20")
|
|
72
|
+
.option("--type <type>", "Filter by error type")
|
|
73
|
+
.option("--preventable", "Show only preventable errors")
|
|
74
|
+
.option("--json", "Output as JSON")
|
|
75
|
+
.action(async (options) => {
|
|
76
|
+
ensureConfig();
|
|
77
|
+
const api = getApiClient();
|
|
78
|
+
try {
|
|
79
|
+
// Get analyzed sessions
|
|
80
|
+
const result = await api.get("/api/sessions?status=analyzed&limit=50");
|
|
81
|
+
const sinceDate = parseDuration(options.since);
|
|
82
|
+
const limit = parseInt(options.limit, 10);
|
|
83
|
+
// Extract errors from summaries
|
|
84
|
+
const allErrors = [];
|
|
85
|
+
for (const session of result.sessions) {
|
|
86
|
+
if (!session.summary?.errors)
|
|
87
|
+
continue;
|
|
88
|
+
const analyzedAt = new Date(session.analyzedAt || session.createdAt);
|
|
89
|
+
if (analyzedAt < sinceDate)
|
|
90
|
+
continue;
|
|
91
|
+
for (const error of session.summary.errors) {
|
|
92
|
+
if (options.type && error.type !== options.type)
|
|
93
|
+
continue;
|
|
94
|
+
if (options.preventable && !error.preventable)
|
|
95
|
+
continue;
|
|
96
|
+
allErrors.push({
|
|
97
|
+
sessionId: session.id,
|
|
98
|
+
analyzedAt,
|
|
99
|
+
error,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Sort by date, most recent first
|
|
104
|
+
allErrors.sort((a, b) => b.analyzedAt.getTime() - a.analyzedAt.getTime());
|
|
105
|
+
const errors = allErrors.slice(0, limit);
|
|
106
|
+
if (options.json) {
|
|
107
|
+
console.log(JSON.stringify({ success: true, errors, count: errors.length }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(`\n Errors (last ${options.since}, ${errors.length} found)\n`);
|
|
111
|
+
if (errors.length === 0) {
|
|
112
|
+
console.log(" No errors found.");
|
|
113
|
+
console.log("");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Group by error type
|
|
117
|
+
const byType = new Map();
|
|
118
|
+
for (const e of errors) {
|
|
119
|
+
const list = byType.get(e.error.type) || [];
|
|
120
|
+
list.push(e);
|
|
121
|
+
byType.set(e.error.type, list);
|
|
122
|
+
}
|
|
123
|
+
for (const [type, typeErrors] of byType) {
|
|
124
|
+
console.log(` [${type}] (${typeErrors.length})`);
|
|
125
|
+
for (const e of typeErrors.slice(0, 5)) {
|
|
126
|
+
const preventable = e.error.preventable ? " (preventable)" : "";
|
|
127
|
+
console.log(` ${formatDate(e.analyzedAt)} | ${truncate(e.error.description, 50)}${preventable}`);
|
|
128
|
+
console.log(` ${truncate(e.error.solution, 55)}`);
|
|
129
|
+
}
|
|
130
|
+
if (typeErrors.length > 5) {
|
|
131
|
+
console.log(` ... and ${typeErrors.length - 5} more`);
|
|
132
|
+
}
|
|
133
|
+
console.log("");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error("Error:", error.message);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// Learnings
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
insightsCommand
|
|
145
|
+
.command("learnings")
|
|
146
|
+
.description("List learnings from analyzed sessions")
|
|
147
|
+
.option("--since <duration>", "Time window (e.g., 7d, 24h, 2w)", "7d")
|
|
148
|
+
.option("-l, --limit <num>", "Max learnings to show", "20")
|
|
149
|
+
.option("-t, --tags <tags>", "Filter by tags (comma-separated)")
|
|
150
|
+
.option("--confidence <level>", "Filter by confidence (low, medium, high)")
|
|
151
|
+
.option("--saved-only", "Show only learnings saved to brain")
|
|
152
|
+
.option("--json", "Output as JSON")
|
|
153
|
+
.action(async (options) => {
|
|
154
|
+
ensureConfig();
|
|
155
|
+
const api = getApiClient();
|
|
156
|
+
try {
|
|
157
|
+
const result = await api.get("/api/sessions?status=analyzed&limit=50");
|
|
158
|
+
const sinceDate = parseDuration(options.since);
|
|
159
|
+
const limit = parseInt(options.limit, 10);
|
|
160
|
+
const filterTags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
|
|
161
|
+
const allLearnings = [];
|
|
162
|
+
for (const session of result.sessions) {
|
|
163
|
+
if (!session.summary?.learnings)
|
|
164
|
+
continue;
|
|
165
|
+
const analyzedAt = new Date(session.analyzedAt || session.createdAt);
|
|
166
|
+
if (analyzedAt < sinceDate)
|
|
167
|
+
continue;
|
|
168
|
+
for (const learning of session.summary.learnings) {
|
|
169
|
+
if (options.confidence && learning.confidence !== options.confidence)
|
|
170
|
+
continue;
|
|
171
|
+
if (options.savedOnly && !learning.savedToBrain)
|
|
172
|
+
continue;
|
|
173
|
+
if (filterTags.length > 0) {
|
|
174
|
+
const hasTag = filterTags.some((t) => learning.tags.includes(t));
|
|
175
|
+
if (!hasTag)
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
allLearnings.push({
|
|
179
|
+
sessionId: session.id,
|
|
180
|
+
analyzedAt,
|
|
181
|
+
learning,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
allLearnings.sort((a, b) => b.analyzedAt.getTime() - a.analyzedAt.getTime());
|
|
186
|
+
const learnings = allLearnings.slice(0, limit);
|
|
187
|
+
if (options.json) {
|
|
188
|
+
console.log(JSON.stringify({ success: true, learnings, count: learnings.length }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(`\n Learnings (last ${options.since}, ${learnings.length} found)\n`);
|
|
192
|
+
if (learnings.length === 0) {
|
|
193
|
+
console.log(" No learnings found.");
|
|
194
|
+
console.log("");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (const l of learnings) {
|
|
198
|
+
const tags = l.learning.tags.length > 0 ? ` [${l.learning.tags.join(", ")}]` : "";
|
|
199
|
+
const confidence = l.learning.confidence;
|
|
200
|
+
const saved = l.learning.savedToBrain ? " " : "";
|
|
201
|
+
console.log(` ${formatDate(l.analyzedAt)} | [${confidence}]${saved} ${truncate(l.learning.content, 60)}${tags}`);
|
|
202
|
+
}
|
|
203
|
+
console.log("");
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error("Error:", error.message);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
// Improvements
|
|
212
|
+
// -------------------------------------------------------------------------
|
|
213
|
+
insightsCommand
|
|
214
|
+
.command("improvements")
|
|
215
|
+
.description("List suggested improvements from analyzed sessions")
|
|
216
|
+
.option("--since <duration>", "Time window (e.g., 7d, 24h, 2w)", "30d")
|
|
217
|
+
.option("-l, --limit <num>", "Max improvements to show", "20")
|
|
218
|
+
.option("--priority <priority>", "Filter by priority (low, medium, high)")
|
|
219
|
+
.option("--area <area>", "Filter by area")
|
|
220
|
+
.option("--json", "Output as JSON")
|
|
221
|
+
.action(async (options) => {
|
|
222
|
+
ensureConfig();
|
|
223
|
+
const api = getApiClient();
|
|
224
|
+
try {
|
|
225
|
+
const result = await api.get("/api/sessions?status=analyzed&limit=100");
|
|
226
|
+
const sinceDate = parseDuration(options.since);
|
|
227
|
+
const limit = parseInt(options.limit, 10);
|
|
228
|
+
const allImprovements = [];
|
|
229
|
+
for (const session of result.sessions) {
|
|
230
|
+
if (!session.summary?.improvements)
|
|
231
|
+
continue;
|
|
232
|
+
const analyzedAt = new Date(session.analyzedAt || session.createdAt);
|
|
233
|
+
if (analyzedAt < sinceDate)
|
|
234
|
+
continue;
|
|
235
|
+
for (const improvement of session.summary.improvements) {
|
|
236
|
+
if (options.priority && improvement.priority !== options.priority)
|
|
237
|
+
continue;
|
|
238
|
+
if (options.area && !improvement.area.toLowerCase().includes(options.area.toLowerCase()))
|
|
239
|
+
continue;
|
|
240
|
+
allImprovements.push({
|
|
241
|
+
sessionId: session.id,
|
|
242
|
+
analyzedAt,
|
|
243
|
+
improvement,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Sort by priority (high first), then by date
|
|
248
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
249
|
+
allImprovements.sort((a, b) => {
|
|
250
|
+
const pA = priorityOrder[a.improvement.priority] ?? 3;
|
|
251
|
+
const pB = priorityOrder[b.improvement.priority] ?? 3;
|
|
252
|
+
if (pA !== pB)
|
|
253
|
+
return pA - pB;
|
|
254
|
+
return b.analyzedAt.getTime() - a.analyzedAt.getTime();
|
|
255
|
+
});
|
|
256
|
+
const improvements = allImprovements.slice(0, limit);
|
|
257
|
+
if (options.json) {
|
|
258
|
+
console.log(JSON.stringify({ success: true, improvements, count: improvements.length }));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
console.log(`\n Suggested Improvements (last ${options.since}, ${improvements.length} found)\n`);
|
|
262
|
+
if (improvements.length === 0) {
|
|
263
|
+
console.log(" No improvements found.");
|
|
264
|
+
console.log("");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Group by area
|
|
268
|
+
const byArea = new Map();
|
|
269
|
+
for (const i of improvements) {
|
|
270
|
+
const list = byArea.get(i.improvement.area) || [];
|
|
271
|
+
list.push(i);
|
|
272
|
+
byArea.set(i.improvement.area, list);
|
|
273
|
+
}
|
|
274
|
+
for (const [area, areaImprovements] of byArea) {
|
|
275
|
+
console.log(` ${area}`);
|
|
276
|
+
for (const i of areaImprovements) {
|
|
277
|
+
const priority = i.improvement.priority.toUpperCase().padEnd(6);
|
|
278
|
+
console.log(` [${priority}] ${truncate(i.improvement.suggestion, 55)}`);
|
|
279
|
+
}
|
|
280
|
+
console.log("");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
console.error("Error:", error.message);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
// -------------------------------------------------------------------------
|
|
289
|
+
// Stats
|
|
290
|
+
// -------------------------------------------------------------------------
|
|
291
|
+
insightsCommand
|
|
292
|
+
.command("stats")
|
|
293
|
+
.description("Show aggregated statistics across sessions")
|
|
294
|
+
.option("--since <duration>", "Time window (e.g., 7d, 24h, 2w)", "7d")
|
|
295
|
+
.option("--json", "Output as JSON")
|
|
296
|
+
.action(async (options) => {
|
|
297
|
+
ensureConfig();
|
|
298
|
+
const api = getApiClient();
|
|
299
|
+
try {
|
|
300
|
+
// Get all sessions (not just analyzed)
|
|
301
|
+
const result = await api.get("/api/sessions?limit=100");
|
|
302
|
+
const sinceDate = parseDuration(options.since);
|
|
303
|
+
// Filter by date
|
|
304
|
+
const sessions = result.sessions.filter((s) => {
|
|
305
|
+
const createdAt = new Date(s.createdAt);
|
|
306
|
+
return createdAt >= sinceDate;
|
|
307
|
+
});
|
|
308
|
+
// Calculate stats
|
|
309
|
+
const totalSessions = sessions.length;
|
|
310
|
+
const analyzedSessions = sessions.filter((s) => s.status === "analyzed").length;
|
|
311
|
+
const activeSessions = sessions.filter((s) => s.status === "active").length;
|
|
312
|
+
let totalLogs = 0;
|
|
313
|
+
let totalErrors = 0;
|
|
314
|
+
let totalDurationMinutes = 0;
|
|
315
|
+
const toolUsage = {};
|
|
316
|
+
const errorTypes = {};
|
|
317
|
+
let totalLearnings = 0;
|
|
318
|
+
let totalImprovements = 0;
|
|
319
|
+
for (const session of sessions) {
|
|
320
|
+
totalLogs += session.logCount || 0;
|
|
321
|
+
totalErrors += session.errorCount || 0;
|
|
322
|
+
if (session.summary) {
|
|
323
|
+
totalDurationMinutes += session.summary.stats.durationMinutes || 0;
|
|
324
|
+
totalLearnings += session.summary.learnings.length;
|
|
325
|
+
totalImprovements += session.summary.improvements.length;
|
|
326
|
+
// Aggregate tool usage
|
|
327
|
+
for (const [tool, count] of Object.entries(session.summary.stats.toolUsage || {})) {
|
|
328
|
+
toolUsage[tool] = (toolUsage[tool] || 0) + count;
|
|
329
|
+
}
|
|
330
|
+
// Aggregate error types
|
|
331
|
+
for (const error of session.summary.errors) {
|
|
332
|
+
errorTypes[error.type] = (errorTypes[error.type] || 0) + 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const stats = {
|
|
337
|
+
period: options.since,
|
|
338
|
+
totalSessions,
|
|
339
|
+
analyzedSessions,
|
|
340
|
+
activeSessions,
|
|
341
|
+
totalLogs,
|
|
342
|
+
totalErrors,
|
|
343
|
+
totalDurationMinutes,
|
|
344
|
+
totalLearnings,
|
|
345
|
+
totalImprovements,
|
|
346
|
+
toolUsage,
|
|
347
|
+
errorTypes,
|
|
348
|
+
};
|
|
349
|
+
if (options.json) {
|
|
350
|
+
console.log(JSON.stringify({ success: true, stats }));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
console.log(`\n Session Stats (last ${options.since})`);
|
|
354
|
+
console.log(` ${"".repeat(40)}`);
|
|
355
|
+
console.log("");
|
|
356
|
+
console.log(` Sessions: ${totalSessions}`);
|
|
357
|
+
console.log(` Active: ${activeSessions}`);
|
|
358
|
+
console.log(` Analyzed: ${analyzedSessions}`);
|
|
359
|
+
console.log("");
|
|
360
|
+
console.log(` Activity:`);
|
|
361
|
+
console.log(` Total logs: ${totalLogs.toLocaleString()}`);
|
|
362
|
+
console.log(` Total errors: ${totalErrors.toLocaleString()}`);
|
|
363
|
+
console.log(` Duration: ${Math.round(totalDurationMinutes / 60)} hours`);
|
|
364
|
+
console.log("");
|
|
365
|
+
console.log(` Analysis:`);
|
|
366
|
+
console.log(` Learnings: ${totalLearnings}`);
|
|
367
|
+
console.log(` Improvements: ${totalImprovements}`);
|
|
368
|
+
console.log("");
|
|
369
|
+
// Top tools
|
|
370
|
+
const sortedTools = Object.entries(toolUsage)
|
|
371
|
+
.sort((a, b) => b[1] - a[1])
|
|
372
|
+
.slice(0, 5);
|
|
373
|
+
if (sortedTools.length > 0) {
|
|
374
|
+
console.log(` Top Tools:`);
|
|
375
|
+
for (const [tool, count] of sortedTools) {
|
|
376
|
+
console.log(` ${tool.padEnd(20)} ${count.toLocaleString()}`);
|
|
377
|
+
}
|
|
378
|
+
console.log("");
|
|
379
|
+
}
|
|
380
|
+
// Error types
|
|
381
|
+
const sortedErrors = Object.entries(errorTypes)
|
|
382
|
+
.sort((a, b) => b[1] - a[1])
|
|
383
|
+
.slice(0, 5);
|
|
384
|
+
if (sortedErrors.length > 0) {
|
|
385
|
+
console.log(` Error Types:`);
|
|
386
|
+
for (const [type, count] of sortedErrors) {
|
|
387
|
+
console.log(` ${type.padEnd(20)} ${count}`);
|
|
388
|
+
}
|
|
389
|
+
console.log("");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.error("Error:", error.message);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
export default insightsCommand;
|
|
@@ -289,6 +289,42 @@ husky brain stats # Statistiken
|
|
|
289
289
|
\`\`\`
|
|
290
290
|
`;
|
|
291
291
|
}
|
|
292
|
+
function getSessionSection() {
|
|
293
|
+
return `
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Session Logging (Self-Improving System)
|
|
297
|
+
|
|
298
|
+
> [!NOTE]
|
|
299
|
+
> Sessions werden automatisch geloggt und bei VM-Shutdown exportiert.
|
|
300
|
+
> Analysierte Sessions ermoeglichen semantische Suche nach aehnlichen Problemen/Loesungen.
|
|
301
|
+
|
|
302
|
+
\`\`\`bash
|
|
303
|
+
# Session Management
|
|
304
|
+
husky session current # Zeigt aktuelle Session
|
|
305
|
+
husky session list --limit 10 # Sessions auflisten
|
|
306
|
+
husky session log --tool "Bash" ... # Tool-Call loggen (via Hook)
|
|
307
|
+
husky session export --to-firestore # Logs exportieren
|
|
308
|
+
|
|
309
|
+
# Analyse & Embeddings
|
|
310
|
+
husky session analyze # Analyse triggern (async)
|
|
311
|
+
husky session embed # Embeddings generieren
|
|
312
|
+
husky session summary # Analyse-Summary anzeigen
|
|
313
|
+
|
|
314
|
+
# Semantische Suche (mit Vector Embeddings)
|
|
315
|
+
husky session search "trigger fehler" # Nach aehnlichen Problemen suchen
|
|
316
|
+
husky session search "RLS" --type errors # Nur Fehler
|
|
317
|
+
husky session search "fix" --type learnings # Nur Learnings
|
|
318
|
+
\`\`\`
|
|
319
|
+
|
|
320
|
+
### Insights (Aggregierte Analytics)
|
|
321
|
+
\`\`\`bash
|
|
322
|
+
husky insights errors --since 7d # Fehler der letzten 7 Tage
|
|
323
|
+
husky insights learnings --since 7d # Learnings anzeigen
|
|
324
|
+
husky insights stats --since 7d # Aggregierte Statistiken
|
|
325
|
+
\`\`\`
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
292
328
|
function getVMSection() {
|
|
293
329
|
return `
|
|
294
330
|
---
|
|
@@ -428,6 +464,8 @@ export function generateLLMContext(role, permissions) {
|
|
|
428
464
|
}
|
|
429
465
|
// Brain - always included
|
|
430
466
|
context += getBrainSection(effectiveRole);
|
|
467
|
+
// Session Logging - always included
|
|
468
|
+
context += getSessionSection();
|
|
431
469
|
// Tips - always included
|
|
432
470
|
context += getTips();
|
|
433
471
|
return context;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Command - Agent Session Logging & Analysis
|
|
3
|
+
*
|
|
4
|
+
* Part of the self-improving system. Provides commands for:
|
|
5
|
+
* - Logging tool calls during a session
|
|
6
|
+
* - Exporting session data before VM shutdown
|
|
7
|
+
* - Triggering analysis
|
|
8
|
+
* - Searching for similar problems/solutions
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
export declare const sessionCommand: Command;
|
|
12
|
+
export default sessionCommand;
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Command - Agent Session Logging & Analysis
|
|
3
|
+
*
|
|
4
|
+
* Part of the self-improving system. Provides commands for:
|
|
5
|
+
* - Logging tool calls during a session
|
|
6
|
+
* - Exporting session data before VM shutdown
|
|
7
|
+
* - Triggering analysis
|
|
8
|
+
* - Searching for similar problems/solutions
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { getConfig } from "./config.js";
|
|
12
|
+
import { getApiClient } from "../lib/api-client.js";
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Helpers
|
|
17
|
+
// ============================================================================
|
|
18
|
+
const DEFAULT_AGENT_ID = process.env.HUSKY_AGENT_ID || "default";
|
|
19
|
+
const DEFAULT_VM_NAME = process.env.HUSKY_VM_NAME || process.env.HOSTNAME || "local";
|
|
20
|
+
// Local session state file for tracking current session
|
|
21
|
+
const SESSION_STATE_FILE = path.join(process.env.HOME || "/tmp", ".husky", "current-session.json");
|
|
22
|
+
function loadLocalState() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(SESSION_STATE_FILE)) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(SESSION_STATE_FILE, "utf-8"));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore errors
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function saveLocalState(state) {
|
|
34
|
+
const dir = path.dirname(SESSION_STATE_FILE);
|
|
35
|
+
if (!fs.existsSync(dir)) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
37
|
+
}
|
|
38
|
+
fs.writeFileSync(SESSION_STATE_FILE, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
39
|
+
}
|
|
40
|
+
function clearLocalState() {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(SESSION_STATE_FILE)) {
|
|
43
|
+
fs.unlinkSync(SESSION_STATE_FILE);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Ignore errors
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function truncate(text, maxLen) {
|
|
51
|
+
if (text.length <= maxLen)
|
|
52
|
+
return text;
|
|
53
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
54
|
+
}
|
|
55
|
+
function formatDate(date) {
|
|
56
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
57
|
+
return d.toLocaleString("de-DE", {
|
|
58
|
+
day: "2-digit",
|
|
59
|
+
month: "2-digit",
|
|
60
|
+
hour: "2-digit",
|
|
61
|
+
minute: "2-digit",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function ensureConfig() {
|
|
65
|
+
const config = getConfig();
|
|
66
|
+
if (!config.apiUrl) {
|
|
67
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Command Definition
|
|
74
|
+
// ============================================================================
|
|
75
|
+
export const sessionCommand = new Command("session")
|
|
76
|
+
.description("Agent session logging and analysis (self-improving system)");
|
|
77
|
+
// -------------------------------------------------------------------------
|
|
78
|
+
// Session Management
|
|
79
|
+
// -------------------------------------------------------------------------
|
|
80
|
+
sessionCommand
|
|
81
|
+
.command("init")
|
|
82
|
+
.description("Initialize a new session (typically called at VM startup)")
|
|
83
|
+
.option("--vm-name <name>", "VM name", DEFAULT_VM_NAME)
|
|
84
|
+
.option("--agent-id <id>", "Agent ID", DEFAULT_AGENT_ID)
|
|
85
|
+
.option("--agent-name <name>", "Agent display name")
|
|
86
|
+
.option("--task-id <id>", "Associated task ID")
|
|
87
|
+
.option("--json", "Output as JSON")
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
ensureConfig();
|
|
90
|
+
const api = getApiClient();
|
|
91
|
+
try {
|
|
92
|
+
const session = await api.post("/api/sessions", {
|
|
93
|
+
vmName: options.vmName,
|
|
94
|
+
agentId: options.agentId,
|
|
95
|
+
agentName: options.agentName,
|
|
96
|
+
taskId: options.taskId,
|
|
97
|
+
});
|
|
98
|
+
// Save local state
|
|
99
|
+
saveLocalState({
|
|
100
|
+
sessionId: session.id,
|
|
101
|
+
vmName: options.vmName,
|
|
102
|
+
agentId: options.agentId,
|
|
103
|
+
startedAt: new Date().toISOString(),
|
|
104
|
+
pendingLogs: [],
|
|
105
|
+
});
|
|
106
|
+
// Set environment variable for hooks
|
|
107
|
+
console.log(`export HUSKY_SESSION_ID="${session.id}"`);
|
|
108
|
+
if (options.json) {
|
|
109
|
+
console.log(JSON.stringify({ success: true, session }));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(` Session initialized: ${session.id}`);
|
|
113
|
+
console.log(` VM: ${options.vmName}`);
|
|
114
|
+
console.log(` Agent: ${options.agentId}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error("Error:", error.message);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
sessionCommand
|
|
123
|
+
.command("current")
|
|
124
|
+
.description("Show current session info")
|
|
125
|
+
.option("--vm-name <name>", "VM name filter", DEFAULT_VM_NAME)
|
|
126
|
+
.option("--agent-id <id>", "Agent ID filter")
|
|
127
|
+
.option("--json", "Output as JSON")
|
|
128
|
+
.action(async (options) => {
|
|
129
|
+
// First check local state
|
|
130
|
+
const localState = loadLocalState();
|
|
131
|
+
if (localState) {
|
|
132
|
+
if (options.json) {
|
|
133
|
+
console.log(JSON.stringify({
|
|
134
|
+
success: true,
|
|
135
|
+
source: "local",
|
|
136
|
+
sessionId: localState.sessionId,
|
|
137
|
+
vmName: localState.vmName,
|
|
138
|
+
agentId: localState.agentId,
|
|
139
|
+
startedAt: localState.startedAt,
|
|
140
|
+
pendingLogs: localState.pendingLogs.length,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(`\n Current Session (local state)`);
|
|
145
|
+
console.log(` ID: ${localState.sessionId}`);
|
|
146
|
+
console.log(` VM: ${localState.vmName}`);
|
|
147
|
+
console.log(` Agent: ${localState.agentId}`);
|
|
148
|
+
console.log(` Started: ${formatDate(localState.startedAt)}`);
|
|
149
|
+
console.log(` Pending logs: ${localState.pendingLogs.length}`);
|
|
150
|
+
console.log("");
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Fall back to API
|
|
155
|
+
ensureConfig();
|
|
156
|
+
const api = getApiClient();
|
|
157
|
+
try {
|
|
158
|
+
const params = new URLSearchParams();
|
|
159
|
+
if (options.vmName)
|
|
160
|
+
params.set("vmName", options.vmName);
|
|
161
|
+
if (options.agentId)
|
|
162
|
+
params.set("agentId", options.agentId);
|
|
163
|
+
const result = await api.get(`/api/sessions/current?${params.toString()}`);
|
|
164
|
+
if (options.json) {
|
|
165
|
+
console.log(JSON.stringify({ success: true, source: "api", session: result.session }));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const s = result.session;
|
|
169
|
+
console.log(`\n Current Session`);
|
|
170
|
+
console.log(` ID: ${s.id}`);
|
|
171
|
+
console.log(` VM: ${s.vmName}`);
|
|
172
|
+
console.log(` Agent: ${s.agentId}`);
|
|
173
|
+
console.log(` Status: ${s.status}`);
|
|
174
|
+
console.log(` Logs: ${s.logCount} (${s.errorCount} errors)`);
|
|
175
|
+
console.log(` Started: ${formatDate(s.startedAt)}`);
|
|
176
|
+
console.log("");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (options.json) {
|
|
181
|
+
console.log(JSON.stringify({ success: false, error: error.message }));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(" No active session found");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
sessionCommand
|
|
189
|
+
.command("list")
|
|
190
|
+
.description("List recent sessions")
|
|
191
|
+
.option("-s, --status <status>", "Filter by status (active, exported, analyzed)")
|
|
192
|
+
.option("--vm-name <name>", "Filter by VM name")
|
|
193
|
+
.option("--agent-id <id>", "Filter by agent ID")
|
|
194
|
+
.option("-l, --limit <num>", "Max results", "20")
|
|
195
|
+
.option("--json", "Output as JSON")
|
|
196
|
+
.action(async (options) => {
|
|
197
|
+
ensureConfig();
|
|
198
|
+
const api = getApiClient();
|
|
199
|
+
try {
|
|
200
|
+
const params = new URLSearchParams();
|
|
201
|
+
if (options.status)
|
|
202
|
+
params.set("status", options.status);
|
|
203
|
+
if (options.vmName)
|
|
204
|
+
params.set("vmName", options.vmName);
|
|
205
|
+
if (options.agentId)
|
|
206
|
+
params.set("agentId", options.agentId);
|
|
207
|
+
params.set("limit", options.limit);
|
|
208
|
+
const result = await api.get(`/api/sessions?${params.toString()}`);
|
|
209
|
+
if (options.json) {
|
|
210
|
+
console.log(JSON.stringify({ success: true, sessions: result.sessions }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.log(`\n Sessions (${result.sessions.length})\n`);
|
|
214
|
+
if (result.sessions.length === 0) {
|
|
215
|
+
console.log(" No sessions found.");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
for (const s of result.sessions) {
|
|
219
|
+
const date = formatDate(s.createdAt);
|
|
220
|
+
const status = s.status.padEnd(10);
|
|
221
|
+
const logs = `${s.logCount} logs`.padEnd(12);
|
|
222
|
+
console.log(` ${date} | ${status} | ${logs} | ${s.vmName}/${s.agentId}`);
|
|
223
|
+
}
|
|
224
|
+
console.log("");
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
console.error("Error:", error.message);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
// -------------------------------------------------------------------------
|
|
232
|
+
// Logging
|
|
233
|
+
// -------------------------------------------------------------------------
|
|
234
|
+
sessionCommand
|
|
235
|
+
.command("log")
|
|
236
|
+
.description("Add a log entry to current session (for PostToolUse hook)")
|
|
237
|
+
.option("--session-id <id>", "Session ID (or uses current)")
|
|
238
|
+
.option("--tool <name>", "Tool name", "unknown")
|
|
239
|
+
.option("--input <text>", "Tool input (truncated to 1KB)")
|
|
240
|
+
.option("--output <text>", "Tool output (truncated to 5KB)")
|
|
241
|
+
.option("--status <status>", "Status: success, error, warning", "success")
|
|
242
|
+
.option("--error-type <type>", "Error type if status is error")
|
|
243
|
+
.option("--duration <ms>", "Duration in milliseconds")
|
|
244
|
+
.option("--buffer", "Buffer locally instead of sending immediately")
|
|
245
|
+
.option("--json", "Output as JSON")
|
|
246
|
+
.action(async (options) => {
|
|
247
|
+
const localState = loadLocalState();
|
|
248
|
+
const sessionId = options.sessionId || localState?.sessionId || process.env.HUSKY_SESSION_ID;
|
|
249
|
+
if (!sessionId) {
|
|
250
|
+
if (options.json) {
|
|
251
|
+
console.log(JSON.stringify({ success: false, error: "No session ID" }));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
console.error("Error: No session ID. Run 'husky session init' first.");
|
|
255
|
+
}
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
const logEntry = {
|
|
259
|
+
tool: options.tool,
|
|
260
|
+
toolInput: truncate(options.input || "", 1024),
|
|
261
|
+
toolOutput: truncate(options.output || "", 5120),
|
|
262
|
+
status: options.status,
|
|
263
|
+
errorType: options.errorType,
|
|
264
|
+
duration: options.duration ? parseInt(options.duration, 10) : undefined,
|
|
265
|
+
timestamp: new Date().toISOString(),
|
|
266
|
+
};
|
|
267
|
+
// Buffer mode: store locally
|
|
268
|
+
if (options.buffer && localState) {
|
|
269
|
+
localState.pendingLogs.push(logEntry);
|
|
270
|
+
saveLocalState(localState);
|
|
271
|
+
if (options.json) {
|
|
272
|
+
console.log(JSON.stringify({ success: true, buffered: true, pending: localState.pendingLogs.length }));
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Send to API
|
|
277
|
+
ensureConfig();
|
|
278
|
+
const api = getApiClient();
|
|
279
|
+
try {
|
|
280
|
+
const vmName = localState?.vmName || DEFAULT_VM_NAME;
|
|
281
|
+
const agentId = localState?.agentId || DEFAULT_AGENT_ID;
|
|
282
|
+
await api.post(`/api/sessions/${sessionId}/logs`, {
|
|
283
|
+
vmName,
|
|
284
|
+
agentId,
|
|
285
|
+
...logEntry,
|
|
286
|
+
});
|
|
287
|
+
if (options.json) {
|
|
288
|
+
console.log(JSON.stringify({ success: true, sessionId }));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
// Fail silently in non-json mode (don't disrupt main workflow)
|
|
293
|
+
if (options.json) {
|
|
294
|
+
console.log(JSON.stringify({ success: false, error: error.message }));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
// -------------------------------------------------------------------------
|
|
299
|
+
// Export (for VM shutdown)
|
|
300
|
+
// -------------------------------------------------------------------------
|
|
301
|
+
sessionCommand
|
|
302
|
+
.command("export")
|
|
303
|
+
.description("Export buffered logs to Firestore (for VM shutdown)")
|
|
304
|
+
.option("--session-id <id>", "Session ID (or uses current)")
|
|
305
|
+
.option("--to-firestore", "Export to Firestore (default)")
|
|
306
|
+
.option("--force", "Export even if no pending logs")
|
|
307
|
+
.option("--json", "Output as JSON")
|
|
308
|
+
.action(async (options) => {
|
|
309
|
+
const localState = loadLocalState();
|
|
310
|
+
const sessionId = options.sessionId || localState?.sessionId || process.env.HUSKY_SESSION_ID;
|
|
311
|
+
if (!sessionId) {
|
|
312
|
+
if (options.json) {
|
|
313
|
+
console.log(JSON.stringify({ success: false, error: "No session ID" }));
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
console.error("Error: No session ID. Run 'husky session init' first.");
|
|
317
|
+
}
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
if (!localState?.pendingLogs.length && !options.force) {
|
|
321
|
+
if (options.json) {
|
|
322
|
+
console.log(JSON.stringify({ success: true, logsExported: 0, message: "No pending logs" }));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
console.log(" No pending logs to export.");
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
ensureConfig();
|
|
330
|
+
const api = getApiClient();
|
|
331
|
+
try {
|
|
332
|
+
const vmName = localState?.vmName || DEFAULT_VM_NAME;
|
|
333
|
+
const agentId = localState?.agentId || DEFAULT_AGENT_ID;
|
|
334
|
+
const logs = (localState?.pendingLogs || []).map((log) => ({
|
|
335
|
+
vmName,
|
|
336
|
+
agentId,
|
|
337
|
+
...log,
|
|
338
|
+
}));
|
|
339
|
+
if (logs.length > 0) {
|
|
340
|
+
const result = await api.post(`/api/sessions/${sessionId}/export`, { logs });
|
|
341
|
+
// Clear local state after successful export
|
|
342
|
+
clearLocalState();
|
|
343
|
+
if (options.json) {
|
|
344
|
+
console.log(JSON.stringify(result));
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
console.log(` Exported ${result.logsImported} logs to session ${sessionId}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Just update status
|
|
352
|
+
await api.patch(`/api/sessions/${sessionId}`, { status: "exported" });
|
|
353
|
+
if (options.json) {
|
|
354
|
+
console.log(JSON.stringify({ success: true, logsExported: 0 }));
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
console.log(` Session ${sessionId} marked as exported`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
console.error("Error:", error.message);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
// -------------------------------------------------------------------------
|
|
367
|
+
// Analysis
|
|
368
|
+
// -------------------------------------------------------------------------
|
|
369
|
+
sessionCommand
|
|
370
|
+
.command("analyze")
|
|
371
|
+
.description("Trigger session analysis")
|
|
372
|
+
.option("--session-id <id>", "Session ID (or uses current)")
|
|
373
|
+
.option("--sync", "Wait for analysis to complete (not recommended)")
|
|
374
|
+
.option("--json", "Output as JSON")
|
|
375
|
+
.action(async (options) => {
|
|
376
|
+
const localState = loadLocalState();
|
|
377
|
+
const sessionId = options.sessionId || localState?.sessionId || process.env.HUSKY_SESSION_ID;
|
|
378
|
+
if (!sessionId) {
|
|
379
|
+
if (options.json) {
|
|
380
|
+
console.log(JSON.stringify({ success: false, error: "No session ID" }));
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
console.error("Error: No session ID.");
|
|
384
|
+
}
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
ensureConfig();
|
|
388
|
+
const api = getApiClient();
|
|
389
|
+
try {
|
|
390
|
+
const result = await api.post(`/api/sessions/${sessionId}/analyze`, { async: !options.sync });
|
|
391
|
+
if (options.json) {
|
|
392
|
+
console.log(JSON.stringify(result));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
if (result.taskId) {
|
|
396
|
+
console.log(` Analysis task created: ${result.taskId}`);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
console.log(` ${result.message || "Analysis triggered"}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
console.error("Error:", error.message);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
// -------------------------------------------------------------------------
|
|
409
|
+
// Get Logs (for analyzer worker)
|
|
410
|
+
// -------------------------------------------------------------------------
|
|
411
|
+
sessionCommand
|
|
412
|
+
.command("get-logs")
|
|
413
|
+
.description("Get logs for a session (for analyzer worker)")
|
|
414
|
+
.option("--session-id <id>", "Session ID", "")
|
|
415
|
+
.option("-l, --limit <num>", "Max results", "500")
|
|
416
|
+
.option("--offset <num>", "Offset", "0")
|
|
417
|
+
.option("--status <status>", "Filter by status")
|
|
418
|
+
.option("--json", "Output as JSON")
|
|
419
|
+
.option("--format <format>", "Output format (json, text)", "text")
|
|
420
|
+
.action(async (options) => {
|
|
421
|
+
if (!options.sessionId) {
|
|
422
|
+
console.error("Error: --session-id is required");
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
ensureConfig();
|
|
426
|
+
const api = getApiClient();
|
|
427
|
+
try {
|
|
428
|
+
const params = new URLSearchParams();
|
|
429
|
+
params.set("limit", options.limit);
|
|
430
|
+
params.set("offset", options.offset);
|
|
431
|
+
if (options.status)
|
|
432
|
+
params.set("status", options.status);
|
|
433
|
+
const result = await api.get(`/api/sessions/${options.sessionId}/logs?${params.toString()}`);
|
|
434
|
+
if (options.json || options.format === "json") {
|
|
435
|
+
console.log(JSON.stringify(result));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
console.log(`\n Logs for session ${options.sessionId} (${result.count})\n`);
|
|
439
|
+
for (const log of result.logs) {
|
|
440
|
+
const time = formatDate(log.timestamp);
|
|
441
|
+
const status = log.status === "error" ? "" : log.status === "warning" ? "" : "";
|
|
442
|
+
console.log(` ${time} | ${status} ${log.tool.padEnd(20)} | ${truncate(log.toolInput, 50)}`);
|
|
443
|
+
}
|
|
444
|
+
console.log("");
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
console.error("Error:", error.message);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
// -------------------------------------------------------------------------
|
|
452
|
+
// Save Summary (for analyzer worker)
|
|
453
|
+
// -------------------------------------------------------------------------
|
|
454
|
+
sessionCommand
|
|
455
|
+
.command("save-summary")
|
|
456
|
+
.description("Save analysis summary (for analyzer worker)")
|
|
457
|
+
.option("--session-id <id>", "Session ID", "")
|
|
458
|
+
.option("--file <path>", "JSON file with summary data")
|
|
459
|
+
.option("--json", "Output as JSON")
|
|
460
|
+
.action(async (options) => {
|
|
461
|
+
if (!options.sessionId) {
|
|
462
|
+
console.error("Error: --session-id is required");
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
if (!options.file) {
|
|
466
|
+
console.error("Error: --file is required");
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
let summaryData;
|
|
470
|
+
try {
|
|
471
|
+
const content = fs.readFileSync(options.file, "utf-8");
|
|
472
|
+
summaryData = JSON.parse(content);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
console.error("Error reading summary file:", error.message);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
ensureConfig();
|
|
479
|
+
const api = getApiClient();
|
|
480
|
+
try {
|
|
481
|
+
const result = await api.post(`/api/sessions/${options.sessionId}/summary`, summaryData);
|
|
482
|
+
if (options.json) {
|
|
483
|
+
console.log(JSON.stringify(result));
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
console.log(` Summary saved for session ${options.sessionId}`);
|
|
487
|
+
console.log(` Learnings: ${result.summary.learnings.length}`);
|
|
488
|
+
console.log(` Errors: ${result.summary.errors.length}`);
|
|
489
|
+
console.log(` Improvements: ${result.summary.improvements.length}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
console.error("Error:", error.message);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
// -------------------------------------------------------------------------
|
|
498
|
+
// Search
|
|
499
|
+
// -------------------------------------------------------------------------
|
|
500
|
+
sessionCommand
|
|
501
|
+
.command("search <query>")
|
|
502
|
+
.description("Search for similar problems/solutions across sessions")
|
|
503
|
+
.option("--type <type>", "Search type: all, errors, learnings", "all")
|
|
504
|
+
.option("-l, --limit <num>", "Max results", "10")
|
|
505
|
+
.option("--json", "Output as JSON")
|
|
506
|
+
.action(async (query, options) => {
|
|
507
|
+
ensureConfig();
|
|
508
|
+
const api = getApiClient();
|
|
509
|
+
try {
|
|
510
|
+
const result = await api.post("/api/sessions/search", {
|
|
511
|
+
query,
|
|
512
|
+
type: options.type,
|
|
513
|
+
limit: parseInt(options.limit, 10),
|
|
514
|
+
});
|
|
515
|
+
if (options.json) {
|
|
516
|
+
console.log(JSON.stringify(result));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
console.log(`\n Search: "${query}" (${result.count} results)\n`);
|
|
520
|
+
if (result.results.length === 0) {
|
|
521
|
+
console.log(" No matching sessions found.");
|
|
522
|
+
console.log("");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
for (const r of result.results) {
|
|
526
|
+
const dateInfo = r.analyzedAt ? formatDate(r.analyzedAt) : "not analyzed";
|
|
527
|
+
const similarity = r.similarity ? ` ${r.similarity}% match` : "";
|
|
528
|
+
console.log(` Session ${r.sessionId.slice(0, 8)}... (${dateInfo})${similarity}`);
|
|
529
|
+
if (r.match.learnings.length > 0) {
|
|
530
|
+
for (const l of r.match.learnings.slice(0, 2)) {
|
|
531
|
+
console.log(` ${truncate(l.content, 70)}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (r.match.errors.length > 0) {
|
|
535
|
+
for (const e of r.match.errors.slice(0, 2)) {
|
|
536
|
+
console.log(` ${truncate(e.description, 70)}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
console.log("");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
console.error("Error:", error.message);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
// -------------------------------------------------------------------------
|
|
548
|
+
// Summary
|
|
549
|
+
// -------------------------------------------------------------------------
|
|
550
|
+
sessionCommand
|
|
551
|
+
.command("summary")
|
|
552
|
+
.description("Get analysis summary for a session")
|
|
553
|
+
.option("--session-id <id>", "Session ID (or uses current)")
|
|
554
|
+
.option("--json", "Output as JSON")
|
|
555
|
+
.action(async (options) => {
|
|
556
|
+
const localState = loadLocalState();
|
|
557
|
+
const sessionId = options.sessionId || localState?.sessionId || process.env.HUSKY_SESSION_ID;
|
|
558
|
+
if (!sessionId) {
|
|
559
|
+
console.error("Error: No session ID.");
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
ensureConfig();
|
|
563
|
+
const api = getApiClient();
|
|
564
|
+
try {
|
|
565
|
+
const result = await api.get(`/api/sessions/${sessionId}/summary`);
|
|
566
|
+
if (options.json) {
|
|
567
|
+
console.log(JSON.stringify(result));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const s = result.summary;
|
|
571
|
+
console.log(`\n Session Summary: ${sessionId.slice(0, 8)}...`);
|
|
572
|
+
console.log(` Analyzed: ${formatDate(s.analyzedAt)} by ${s.analyzedBy}`);
|
|
573
|
+
console.log("");
|
|
574
|
+
console.log(` Stats:`);
|
|
575
|
+
console.log(` Total logs: ${s.stats.totalLogs}`);
|
|
576
|
+
console.log(` Errors: ${s.stats.errorCount}`);
|
|
577
|
+
console.log(` Duration: ${s.stats.durationMinutes} min`);
|
|
578
|
+
console.log("");
|
|
579
|
+
if (s.learnings.length > 0) {
|
|
580
|
+
console.log(` Learnings (${s.learnings.length}):`);
|
|
581
|
+
for (const l of s.learnings) {
|
|
582
|
+
const saved = l.savedToBrain ? " [saved to brain]" : "";
|
|
583
|
+
console.log(` - ${truncate(l.content, 70)}${saved}`);
|
|
584
|
+
}
|
|
585
|
+
console.log("");
|
|
586
|
+
}
|
|
587
|
+
if (s.errors.length > 0) {
|
|
588
|
+
console.log(` Errors (${s.errors.length}):`);
|
|
589
|
+
for (const e of s.errors) {
|
|
590
|
+
console.log(` - [${e.type}] ${truncate(e.description, 60)}`);
|
|
591
|
+
console.log(` ${truncate(e.solution, 60)}`);
|
|
592
|
+
}
|
|
593
|
+
console.log("");
|
|
594
|
+
}
|
|
595
|
+
if (s.improvements.length > 0) {
|
|
596
|
+
console.log(` Improvements (${s.improvements.length}):`);
|
|
597
|
+
for (const i of s.improvements) {
|
|
598
|
+
console.log(` - [${i.priority}] ${i.area}: ${truncate(i.suggestion, 50)}`);
|
|
599
|
+
}
|
|
600
|
+
console.log("");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
if (error.message.includes("not found")) {
|
|
605
|
+
console.log(" Session has not been analyzed yet.");
|
|
606
|
+
console.log(" Run: husky session analyze --session-id " + sessionId);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
console.error("Error:", error.message);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
// -------------------------------------------------------------------------
|
|
615
|
+
// Embed
|
|
616
|
+
// -------------------------------------------------------------------------
|
|
617
|
+
sessionCommand
|
|
618
|
+
.command("embed")
|
|
619
|
+
.description("Generate vector embeddings for a session (requires analysis)")
|
|
620
|
+
.option("--session-id <id>", "Session ID (or uses current)")
|
|
621
|
+
.option("--json", "Output as JSON")
|
|
622
|
+
.action(async (options) => {
|
|
623
|
+
const localState = loadLocalState();
|
|
624
|
+
const sessionId = options.sessionId || localState?.sessionId || process.env.HUSKY_SESSION_ID;
|
|
625
|
+
if (!sessionId) {
|
|
626
|
+
console.error("Error: No session ID. Use --session-id or set HUSKY_SESSION_ID.");
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
ensureConfig();
|
|
630
|
+
const api = getApiClient();
|
|
631
|
+
try {
|
|
632
|
+
if (!options.json) {
|
|
633
|
+
console.log(`Generating embeddings for session ${sessionId.slice(0, 8)}...`);
|
|
634
|
+
}
|
|
635
|
+
const result = await api.post(`/api/sessions/${sessionId}/embed`);
|
|
636
|
+
if (options.json) {
|
|
637
|
+
console.log(JSON.stringify(result));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
console.log(`✓ Generated ${result.embeddingsGenerated} embeddings`);
|
|
641
|
+
console.log("");
|
|
642
|
+
const learnings = result.embeddings.filter(e => e.type === "learning");
|
|
643
|
+
const errors = result.embeddings.filter(e => e.type === "error");
|
|
644
|
+
if (learnings.length > 0) {
|
|
645
|
+
console.log(` Learnings embedded: ${learnings.length}`);
|
|
646
|
+
}
|
|
647
|
+
if (errors.length > 0) {
|
|
648
|
+
console.log(` Errors embedded: ${errors.length}`);
|
|
649
|
+
}
|
|
650
|
+
console.log("");
|
|
651
|
+
console.log(" Session can now be found via semantic search:");
|
|
652
|
+
console.log(" husky session search \"your query\"");
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
if (error.message.includes("must be analyzed")) {
|
|
656
|
+
console.error("Error: Session must be analyzed before embedding.");
|
|
657
|
+
console.error("Run: husky session analyze --session-id " + sessionId);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
console.error("Error:", error.message);
|
|
661
|
+
}
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
export default sessionCommand;
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,8 @@ import { businessCommand } from "./commands/business.js";
|
|
|
39
39
|
import { planCommand } from "./commands/plan.js";
|
|
40
40
|
import { diagramsCommand } from "./commands/diagrams.js";
|
|
41
41
|
import { supervisorCommand } from "./commands/supervisor.js";
|
|
42
|
+
import { sessionCommand } from "./commands/session.js";
|
|
43
|
+
import { insightsCommand } from "./commands/insights.js";
|
|
42
44
|
import { checkVersion } from "./lib/version-check.js";
|
|
43
45
|
// Read version from package.json
|
|
44
46
|
const require = createRequire(import.meta.url);
|
|
@@ -86,6 +88,8 @@ program.addCommand(businessCommand);
|
|
|
86
88
|
program.addCommand(planCommand);
|
|
87
89
|
program.addCommand(diagramsCommand);
|
|
88
90
|
program.addCommand(supervisorCommand);
|
|
91
|
+
program.addCommand(sessionCommand);
|
|
92
|
+
program.addCommand(insightsCommand);
|
|
89
93
|
// Handle --llm flag specially
|
|
90
94
|
if (process.argv.includes("--llm")) {
|
|
91
95
|
printLLMContext();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Types - Shared between session.ts and insights.ts
|
|
3
|
+
*/
|
|
4
|
+
export interface SessionSummary {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
analyzedAt: Date;
|
|
7
|
+
analyzedBy: string;
|
|
8
|
+
learnings: Array<{
|
|
9
|
+
content: string;
|
|
10
|
+
tags: string[];
|
|
11
|
+
confidence: string;
|
|
12
|
+
savedToBrain: boolean;
|
|
13
|
+
brainMemoryId?: string;
|
|
14
|
+
}>;
|
|
15
|
+
errors: Array<{
|
|
16
|
+
type: string;
|
|
17
|
+
description: string;
|
|
18
|
+
solution: string;
|
|
19
|
+
preventable: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
improvements: Array<{
|
|
22
|
+
area: string;
|
|
23
|
+
suggestion: string;
|
|
24
|
+
priority: string;
|
|
25
|
+
}>;
|
|
26
|
+
stats: {
|
|
27
|
+
totalLogs: number;
|
|
28
|
+
errorCount: number;
|
|
29
|
+
durationMinutes: number;
|
|
30
|
+
toolUsage: Record<string, number>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface AgentSession {
|
|
34
|
+
id: string;
|
|
35
|
+
vmName: string;
|
|
36
|
+
agentId: string;
|
|
37
|
+
agentName?: string;
|
|
38
|
+
taskId?: string;
|
|
39
|
+
status: string;
|
|
40
|
+
logCount: number;
|
|
41
|
+
errorCount: number;
|
|
42
|
+
startedAt: Date;
|
|
43
|
+
lastActivityAt: Date;
|
|
44
|
+
exportedAt?: Date;
|
|
45
|
+
analyzedAt?: Date;
|
|
46
|
+
embeddedAt?: Date;
|
|
47
|
+
embeddingCount?: number;
|
|
48
|
+
summary?: SessionSummary;
|
|
49
|
+
createdAt: Date;
|
|
50
|
+
}
|
|
51
|
+
export interface SessionLogEntry {
|
|
52
|
+
id: string;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
vmName: string;
|
|
55
|
+
agentId: string;
|
|
56
|
+
timestamp: Date;
|
|
57
|
+
tool: string;
|
|
58
|
+
toolInput: string;
|
|
59
|
+
toolOutput: string;
|
|
60
|
+
status: "success" | "error" | "warning";
|
|
61
|
+
errorType?: string;
|
|
62
|
+
duration?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface SearchResult {
|
|
65
|
+
sessionId: string;
|
|
66
|
+
similarity?: number;
|
|
67
|
+
analyzedAt?: Date;
|
|
68
|
+
match: {
|
|
69
|
+
learnings: Array<{
|
|
70
|
+
content: string;
|
|
71
|
+
tags: string[];
|
|
72
|
+
score?: number;
|
|
73
|
+
}>;
|
|
74
|
+
errors: Array<{
|
|
75
|
+
description: string;
|
|
76
|
+
solution: string;
|
|
77
|
+
score?: number;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
}
|