@mrclrchtr/supi-insights 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/README.md +234 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +47 -0
- package/src/aggregator.ts +245 -0
- package/src/cache.ts +94 -0
- package/src/extractor.ts +189 -0
- package/src/generator.ts +395 -0
- package/src/html.ts +481 -0
- package/src/index.ts +1 -0
- package/src/insights.ts +416 -0
- package/src/parser.ts +373 -0
- package/src/report.css +411 -0
- package/src/report.js +35 -0
- package/src/scanner.ts +13 -0
- package/src/types.ts +114 -0
- package/src/utils.ts +265 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// Utility helpers for supi-insights
|
|
2
|
+
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
|
|
5
|
+
// ── Retry helper for LLM calls ────────────────────────────
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Attempt an async operation with retries and exponential backoff.
|
|
9
|
+
* Returns the result on success, or null if all attempts fail.
|
|
10
|
+
*/
|
|
11
|
+
export async function withRetry<T>(
|
|
12
|
+
fn: () => Promise<T>,
|
|
13
|
+
retries = 2,
|
|
14
|
+
baseDelayMs = 1000,
|
|
15
|
+
): Promise<T | null> {
|
|
16
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
return await fn();
|
|
19
|
+
} catch (_err) {
|
|
20
|
+
if (attempt < retries) {
|
|
21
|
+
await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** attempt));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
29
|
+
".ts": "TypeScript",
|
|
30
|
+
".tsx": "TypeScript",
|
|
31
|
+
".js": "JavaScript",
|
|
32
|
+
".jsx": "JavaScript",
|
|
33
|
+
".py": "Python",
|
|
34
|
+
".rb": "Ruby",
|
|
35
|
+
".go": "Go",
|
|
36
|
+
".rs": "Rust",
|
|
37
|
+
".java": "Java",
|
|
38
|
+
".md": "Markdown",
|
|
39
|
+
".json": "JSON",
|
|
40
|
+
".yaml": "YAML",
|
|
41
|
+
".yml": "YAML",
|
|
42
|
+
".sh": "Shell",
|
|
43
|
+
".css": "CSS",
|
|
44
|
+
".html": "HTML",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function getLanguageFromPath(filePath: string): string | null {
|
|
48
|
+
const ext = extname(filePath).toLowerCase();
|
|
49
|
+
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function countCharInString(str: string, char: string): number {
|
|
53
|
+
let count = 0;
|
|
54
|
+
for (const c of str) {
|
|
55
|
+
if (c === char) count++;
|
|
56
|
+
}
|
|
57
|
+
return count;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function escapeXmlAttr(text: string): string {
|
|
61
|
+
return text
|
|
62
|
+
.replace(/&/g, "&")
|
|
63
|
+
.replace(/</g, "<")
|
|
64
|
+
.replace(/>/g, ">")
|
|
65
|
+
.replace(/"/g, """)
|
|
66
|
+
.replace(/'/g, "'");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Escape HTML but render **bold** as <strong>
|
|
70
|
+
export function escapeHtmlWithBold(text: string): string {
|
|
71
|
+
const escaped = escapeXmlAttr(text);
|
|
72
|
+
return escaped.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function emptyHtml(message: string): string {
|
|
76
|
+
const tag = "p";
|
|
77
|
+
return `<${tag} class="empty">${escapeXmlAttr(message)}</${tag}>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function generateBarChartHtml(
|
|
81
|
+
data: Record<string, number>,
|
|
82
|
+
color: string,
|
|
83
|
+
maxItems = 6,
|
|
84
|
+
fixedOrder?: string[],
|
|
85
|
+
): string {
|
|
86
|
+
let entries: [string, number][];
|
|
87
|
+
|
|
88
|
+
if (fixedOrder) {
|
|
89
|
+
entries = fixedOrder
|
|
90
|
+
.filter((key) => key in data && (data[key] ?? 0) > 0)
|
|
91
|
+
.map((key) => [key, data[key] ?? 0] as [string, number]);
|
|
92
|
+
} else {
|
|
93
|
+
entries = Object.entries(data)
|
|
94
|
+
.sort((a, b) => b[1] - a[1])
|
|
95
|
+
.slice(0, maxItems);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (entries.length === 0) return emptyHtml("No data");
|
|
99
|
+
|
|
100
|
+
const maxVal = Math.max(...entries.map((e) => e[1]));
|
|
101
|
+
return entries
|
|
102
|
+
.map(([label, count]) => {
|
|
103
|
+
const pct = maxVal > 0 ? (count / maxVal) * 100 : 0;
|
|
104
|
+
const cleanLabel =
|
|
105
|
+
LABEL_MAP[label] ?? label.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
106
|
+
return `<div class="bar-row">
|
|
107
|
+
<div class="bar-label">${escapeXmlAttr(cleanLabel)}</div>
|
|
108
|
+
<div class="bar-track"><div class="bar-fill" style="width:${pct.toFixed(1)}%;background:${color}"></div></div>
|
|
109
|
+
<div class="bar-value">${count}</div>
|
|
110
|
+
</div>`;
|
|
111
|
+
})
|
|
112
|
+
.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function generateResponseTimeHistogramHtml(times: number[]): string {
|
|
116
|
+
if (times.length === 0) return emptyHtml("No response time data");
|
|
117
|
+
|
|
118
|
+
const buckets = RESPONSE_TIME_BUCKETS.map((bucket) => ({ ...bucket, count: 0 }));
|
|
119
|
+
for (const t of times) {
|
|
120
|
+
const bucket = buckets.find((item) => t < item.lessThan) ?? buckets.at(-1);
|
|
121
|
+
if (bucket) bucket.count++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const maxVal = Math.max(...buckets.map((bucket) => bucket.count));
|
|
125
|
+
if (maxVal === 0) return emptyHtml("No response time data");
|
|
126
|
+
|
|
127
|
+
return buckets
|
|
128
|
+
.map(({ label, count }) => {
|
|
129
|
+
const pct = maxVal > 0 ? (count / maxVal) * 100 : 0;
|
|
130
|
+
return `<div class="bar-row">
|
|
131
|
+
<div class="bar-label">${label}</div>
|
|
132
|
+
<div class="bar-track"><div class="bar-fill" style="width:${pct.toFixed(1)}%;background:#6366f1"></div></div>
|
|
133
|
+
<div class="bar-value">${count}</div>
|
|
134
|
+
</div>`;
|
|
135
|
+
})
|
|
136
|
+
.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const RESPONSE_TIME_BUCKETS = [
|
|
140
|
+
{ label: "2-10s", lessThan: 10 },
|
|
141
|
+
{ label: "10-30s", lessThan: 30 },
|
|
142
|
+
{ label: "30s-1m", lessThan: 60 },
|
|
143
|
+
{ label: "1-2m", lessThan: 120 },
|
|
144
|
+
{ label: "2-5m", lessThan: 300 },
|
|
145
|
+
{ label: "5-15m", lessThan: 900 },
|
|
146
|
+
{ label: ">15m", lessThan: Number.POSITIVE_INFINITY },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
export function generateTimeOfDayChartHtml(messageHours: number[], utcOffset = 0): string {
|
|
150
|
+
if (messageHours.length === 0) return emptyHtml("No time data");
|
|
151
|
+
|
|
152
|
+
const periods = [
|
|
153
|
+
{ label: "Morning (6-12)", range: [6, 7, 8, 9, 10, 11] },
|
|
154
|
+
{ label: "Afternoon (12-18)", range: [12, 13, 14, 15, 16, 17] },
|
|
155
|
+
{ label: "Evening (18-24)", range: [18, 19, 20, 21, 22, 23] },
|
|
156
|
+
{ label: "Night (0-6)", range: [0, 1, 2, 3, 4, 5] },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const hourCounts: Record<number, number> = {};
|
|
160
|
+
for (const h of messageHours) {
|
|
161
|
+
const localHour = (h + utcOffset + 24) % 24;
|
|
162
|
+
hourCounts[localHour] = (hourCounts[localHour] ?? 0) + 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const periodCounts = periods.map((p) => ({
|
|
166
|
+
label: p.label,
|
|
167
|
+
count: p.range.reduce((sum, h) => sum + (hourCounts[h] ?? 0), 0),
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
const maxVal = Math.max(...periodCounts.map((p) => p.count)) || 1;
|
|
171
|
+
|
|
172
|
+
return periodCounts
|
|
173
|
+
.map(
|
|
174
|
+
(p) => `
|
|
175
|
+
<div class="bar-row">
|
|
176
|
+
<div class="bar-label">${p.label}</div>
|
|
177
|
+
<div class="bar-track"><div class="bar-fill" style="width:${((p.count / maxVal) * 100).toFixed(1)}%;background:#8b5cf6"></div></div>
|
|
178
|
+
<div class="bar-value">${p.count}</div>
|
|
179
|
+
</div>`,
|
|
180
|
+
)
|
|
181
|
+
.join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getHourCountsJson(messageHours: number[]): string {
|
|
185
|
+
const hourCounts: Record<number, number> = {};
|
|
186
|
+
for (const h of messageHours) {
|
|
187
|
+
hourCounts[h] = (hourCounts[h] ?? 0) + 1;
|
|
188
|
+
}
|
|
189
|
+
return JSON.stringify(hourCounts);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const LABEL_MAP: Record<string, string> = {
|
|
193
|
+
debug_investigate: "Debug/Investigate",
|
|
194
|
+
implement_feature: "Implement Feature",
|
|
195
|
+
fix_bug: "Fix Bug",
|
|
196
|
+
write_script_tool: "Write Script/Tool",
|
|
197
|
+
refactor_code: "Refactor Code",
|
|
198
|
+
configure_system: "Configure System",
|
|
199
|
+
create_pr_commit: "Create PR/Commit",
|
|
200
|
+
analyze_data: "Analyze Data",
|
|
201
|
+
understand_codebase: "Understand Codebase",
|
|
202
|
+
write_tests: "Write Tests",
|
|
203
|
+
write_docs: "Write Docs",
|
|
204
|
+
deploy_infra: "Deploy/Infra",
|
|
205
|
+
warmup_minimal: "Cache Warmup",
|
|
206
|
+
fast_accurate_search: "Fast/Accurate Search",
|
|
207
|
+
correct_code_edits: "Correct Code Edits",
|
|
208
|
+
good_explanations: "Good Explanations",
|
|
209
|
+
proactive_help: "Proactive Help",
|
|
210
|
+
multi_file_changes: "Multi-file Changes",
|
|
211
|
+
handled_complexity: "Multi-file Changes",
|
|
212
|
+
good_debugging: "Good Debugging",
|
|
213
|
+
misunderstood_request: "Misunderstood Request",
|
|
214
|
+
wrong_approach: "Wrong Approach",
|
|
215
|
+
buggy_code: "Buggy Code",
|
|
216
|
+
user_rejected_action: "User Rejected Action",
|
|
217
|
+
claude_got_blocked: "Claude Got Blocked",
|
|
218
|
+
user_stopped_early: "User Stopped Early",
|
|
219
|
+
wrong_file_or_location: "Wrong File/Location",
|
|
220
|
+
excessive_changes: "Excessive Changes",
|
|
221
|
+
slow_or_verbose: "Slow/Verbose",
|
|
222
|
+
tool_failed: "Tool Failed",
|
|
223
|
+
user_unclear: "User Unclear",
|
|
224
|
+
external_issue: "External Issue",
|
|
225
|
+
frustrated: "Frustrated",
|
|
226
|
+
dissatisfied: "Dissatisfied",
|
|
227
|
+
likely_satisfied: "Likely Satisfied",
|
|
228
|
+
satisfied: "Satisfied",
|
|
229
|
+
happy: "Happy",
|
|
230
|
+
unsure: "Unsure",
|
|
231
|
+
neutral: "Neutral",
|
|
232
|
+
delighted: "Delighted",
|
|
233
|
+
single_task: "Single Task",
|
|
234
|
+
multi_task: "Multi Task",
|
|
235
|
+
iterative_refinement: "Iterative Refinement",
|
|
236
|
+
exploration: "Exploration",
|
|
237
|
+
quick_question: "Quick Question",
|
|
238
|
+
fully_achieved: "Fully Achieved",
|
|
239
|
+
mostly_achieved: "Mostly Achieved",
|
|
240
|
+
partially_achieved: "Partially Achieved",
|
|
241
|
+
not_achieved: "Not Achieved",
|
|
242
|
+
unclear_from_transcript: "Unclear",
|
|
243
|
+
unhelpful: "Unhelpful",
|
|
244
|
+
slightly_helpful: "Slightly Helpful",
|
|
245
|
+
moderately_helpful: "Moderately Helpful",
|
|
246
|
+
very_helpful: "Very Helpful",
|
|
247
|
+
essential: "Essential",
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const SATISFACTION_ORDER = [
|
|
251
|
+
"frustrated",
|
|
252
|
+
"dissatisfied",
|
|
253
|
+
"likely_satisfied",
|
|
254
|
+
"satisfied",
|
|
255
|
+
"happy",
|
|
256
|
+
"unsure",
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
export const OUTCOME_ORDER = [
|
|
260
|
+
"not_achieved",
|
|
261
|
+
"partially_achieved",
|
|
262
|
+
"mostly_achieved",
|
|
263
|
+
"fully_achieved",
|
|
264
|
+
"unclear_from_transcript",
|
|
265
|
+
];
|