@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.
Files changed (31) hide show
  1. package/README.md +234 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  18. package/package.json +47 -0
  19. package/src/aggregator.ts +245 -0
  20. package/src/cache.ts +94 -0
  21. package/src/extractor.ts +189 -0
  22. package/src/generator.ts +395 -0
  23. package/src/html.ts +481 -0
  24. package/src/index.ts +1 -0
  25. package/src/insights.ts +416 -0
  26. package/src/parser.ts +373 -0
  27. package/src/report.css +411 -0
  28. package/src/report.js +35 -0
  29. package/src/scanner.ts +13 -0
  30. package/src/types.ts +114 -0
  31. package/src/utils.ts +265 -0
package/src/html.ts ADDED
@@ -0,0 +1,481 @@
1
+ // HTML report renderer — generate a shareable HTML insights report.
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import type { AggregatedData, InsightResults } from "./types.ts";
6
+ import {
7
+ emptyHtml,
8
+ escapeHtmlWithBold,
9
+ escapeXmlAttr,
10
+ generateBarChartHtml,
11
+ generateResponseTimeHistogramHtml,
12
+ generateTimeOfDayChartHtml,
13
+ getHourCountsJson,
14
+ OUTCOME_ORDER,
15
+ SATISFACTION_ORDER,
16
+ } from "./utils.ts";
17
+
18
+ const REPORT_CSS = readFileSync(join(__dirname, "report.css"), "utf-8");
19
+ const REPORT_JS_TEMPLATE = readFileSync(join(__dirname, "report.js"), "utf-8");
20
+
21
+ function generateReportJs(hourCountsJson: string): string {
22
+ return REPORT_JS_TEMPLATE.replace("__HOUR_COUNTS_JSON__", hourCountsJson);
23
+ }
24
+
25
+ function renderAtAGlanceHtml(insights: InsightResults): string {
26
+ const atAGlance = insights.atAGlance as
27
+ | {
28
+ whatsWorking?: string;
29
+ whatsHindering?: string;
30
+ quickWins?: string;
31
+ ambitiousWorkflows?: string;
32
+ }
33
+ | undefined;
34
+
35
+ if (!atAGlance) return "";
36
+
37
+ return `
38
+ <div class="at-a-glance">
39
+ <div class="glance-title">At a Glance</div>
40
+ <div class="glance-sections">
41
+ ${atAGlance.whatsWorking ? `<div class="glance-section"><strong>What's working:</strong> ${escapeHtmlWithBold(atAGlance.whatsWorking)}</div>` : ""}
42
+ ${atAGlance.whatsHindering ? `<div class="glance-section"><strong>What's hindering you:</strong> ${escapeHtmlWithBold(atAGlance.whatsHindering)}</div>` : ""}
43
+ ${atAGlance.quickWins ? `<div class="glance-section"><strong>Quick wins to try:</strong> ${escapeHtmlWithBold(atAGlance.quickWins)}</div>` : ""}
44
+ ${atAGlance.ambitiousWorkflows ? `<div class="glance-section"><strong>Ambitious workflows:</strong> ${escapeHtmlWithBold(atAGlance.ambitiousWorkflows)}</div>` : ""}
45
+ </div>
46
+ </div>
47
+ `;
48
+ }
49
+
50
+ function renderProjectAreasHtml(insights: InsightResults): string {
51
+ const projectAreas =
52
+ (
53
+ insights.projectAreas as
54
+ | { areas?: Array<{ name: string; sessionCount: number; description: string }> }
55
+ | undefined
56
+ )?.areas || [];
57
+
58
+ if (projectAreas.length === 0) return "";
59
+
60
+ return `
61
+ <h2>What You Work On</h2>
62
+ <div class="project-areas">
63
+ ${projectAreas
64
+ .map(
65
+ (area) => `
66
+ <div class="project-area">
67
+ <div class="area-header">
68
+ <span class="area-name">${escapeXmlAttr(area.name)}</span>
69
+ <span class="area-count">~${area.sessionCount} sessions</span>
70
+ </div>
71
+ <div class="area-desc">${escapeXmlAttr(area.description)}</div>
72
+ </div>
73
+ `,
74
+ )
75
+ .join("")}
76
+ </div>
77
+ `;
78
+ }
79
+
80
+ function renderInteractionHtml(insights: InsightResults): string {
81
+ const interactionStyle = insights.interactionStyle as
82
+ | { narrative?: string; keyPattern?: string }
83
+ | undefined;
84
+
85
+ if (!interactionStyle?.narrative) return "";
86
+
87
+ return `
88
+ <h2>How You Use PI</h2>
89
+ <div class="narrative">
90
+ ${markdownToHtml(interactionStyle.narrative)}
91
+ ${interactionStyle.keyPattern ? `<div class="key-insight"><strong>Key pattern:</strong> ${escapeXmlAttr(interactionStyle.keyPattern)}</div>` : ""}
92
+ </div>
93
+ `;
94
+ }
95
+
96
+ function renderWhatWorksHtml(insights: InsightResults): string {
97
+ const whatWorks = insights.whatWorks as
98
+ | { intro?: string; impressiveWorkflows?: Array<{ title: string; description: string }> }
99
+ | undefined;
100
+
101
+ if (!whatWorks?.impressiveWorkflows?.length) return "";
102
+
103
+ return `
104
+ <h2>Impressive Things You Did</h2>
105
+ ${whatWorks.intro ? `<p class="section-intro">${escapeXmlAttr(whatWorks.intro)}</p>` : ""}
106
+ <div class="big-wins">
107
+ ${whatWorks.impressiveWorkflows
108
+ .map(
109
+ (wf) => `
110
+ <div class="big-win">
111
+ <div class="big-win-title">${escapeXmlAttr(wf.title || "")}</div>
112
+ <div class="big-win-desc">${escapeXmlAttr(wf.description || "")}</div>
113
+ </div>
114
+ `,
115
+ )
116
+ .join("")}
117
+ </div>
118
+ `;
119
+ }
120
+
121
+ function renderFrictionHtml(insights: InsightResults): string {
122
+ const frictionAnalysis = insights.frictionAnalysis as
123
+ | {
124
+ intro?: string;
125
+ categories?: Array<{ category: string; description: string; examples?: string[] }>;
126
+ }
127
+ | undefined;
128
+
129
+ if (!frictionAnalysis?.categories?.length) return "";
130
+
131
+ return `
132
+ <h2>Where Things Go Wrong</h2>
133
+ ${frictionAnalysis.intro ? `<p class="section-intro">${escapeXmlAttr(frictionAnalysis.intro)}</p>` : ""}
134
+ <div class="friction-categories">
135
+ ${frictionAnalysis.categories
136
+ .map(
137
+ (cat) => `
138
+ <div class="friction-category">
139
+ <div class="friction-title">${escapeXmlAttr(cat.category || "")}</div>
140
+ <div class="friction-desc">${escapeXmlAttr(cat.description || "")}</div>
141
+ ${cat.examples ? `<ul class="friction-examples">${cat.examples.map((ex) => `<li>${escapeXmlAttr(ex)}</li>`).join("")}</ul>` : ""}
142
+ </div>
143
+ `,
144
+ )
145
+ .join("")}
146
+ </div>
147
+ `;
148
+ }
149
+
150
+ function renderSuggestionsHtml(insights: InsightResults): string {
151
+ const suggestions = insights.suggestions as
152
+ | {
153
+ claudeMdAdditions?: Array<{ addition: string; why: string; promptScaffold?: string }>;
154
+ featuresToTry?: Array<{
155
+ feature: string;
156
+ oneLiner: string;
157
+ whyForYou: string;
158
+ exampleCode?: string;
159
+ }>;
160
+ usagePatterns?: Array<{
161
+ title: string;
162
+ suggestion: string;
163
+ detail?: string;
164
+ copyablePrompt?: string;
165
+ }>;
166
+ }
167
+ | undefined;
168
+
169
+ if (!suggestions) return "";
170
+
171
+ return `
172
+ ${
173
+ suggestions.claudeMdAdditions?.length
174
+ ? `
175
+ <h2>Suggested CLAUDE.md Additions</h2>
176
+ <div class="claude-md-section">
177
+ ${suggestions.claudeMdAdditions
178
+ .map(
179
+ (add) => `
180
+ <div class="claude-md-item">
181
+ <code class="cmd-code">${escapeXmlAttr(add.addition)}</code>
182
+ <div class="cmd-why">${escapeXmlAttr(add.why)}</div>
183
+ </div>
184
+ `,
185
+ )
186
+ .join("")}
187
+ </div>
188
+ `
189
+ : ""
190
+ }
191
+ ${
192
+ suggestions.featuresToTry?.length
193
+ ? `
194
+ <h2>Features to Try</h2>
195
+ <div class="features-section">
196
+ ${suggestions.featuresToTry
197
+ .map(
198
+ (feat) => `
199
+ <div class="feature-card">
200
+ <div class="feature-title">${escapeXmlAttr(feat.feature || "")}</div>
201
+ <div class="feature-oneliner">${escapeXmlAttr(feat.oneLiner || "")}</div>
202
+ <div class="feature-why"><strong>Why for you:</strong> ${escapeXmlAttr(feat.whyForYou || "")}</div>
203
+ ${feat.exampleCode ? `<code class="example-code">${escapeXmlAttr(feat.exampleCode)}</code>` : ""}
204
+ </div>
205
+ `,
206
+ )
207
+ .join("")}
208
+ </div>
209
+ `
210
+ : ""
211
+ }
212
+ ${
213
+ suggestions.usagePatterns?.length
214
+ ? `
215
+ <h2>New Ways to Use PI</h2>
216
+ <div class="patterns-section">
217
+ ${suggestions.usagePatterns
218
+ .map(
219
+ (pat) => `
220
+ <div class="pattern-card">
221
+ <div class="pattern-title">${escapeXmlAttr(pat.title || "")}</div>
222
+ <div class="pattern-summary">${escapeXmlAttr(pat.suggestion || "")}</div>
223
+ ${pat.detail ? `<div class="pattern-detail">${escapeXmlAttr(pat.detail)}</div>` : ""}
224
+ ${pat.copyablePrompt ? `<code class="copyable-prompt">${escapeXmlAttr(pat.copyablePrompt)}</code>` : ""}
225
+ </div>
226
+ `,
227
+ )
228
+ .join("")}
229
+ </div>
230
+ `
231
+ : ""
232
+ }
233
+ `;
234
+ }
235
+
236
+ function renderHorizonHtml(insights: InsightResults): string {
237
+ const horizonData = insights.onTheHorizon as
238
+ | {
239
+ intro?: string;
240
+ opportunities?: Array<{
241
+ title: string;
242
+ whatsPossible: string;
243
+ howToTry?: string;
244
+ copyablePrompt?: string;
245
+ }>;
246
+ }
247
+ | undefined;
248
+
249
+ if (!horizonData?.opportunities?.length) return "";
250
+
251
+ return `
252
+ <h2>On the Horizon</h2>
253
+ ${horizonData.intro ? `<p class="section-intro">${escapeXmlAttr(horizonData.intro)}</p>` : ""}
254
+ <div class="horizon-section">
255
+ ${horizonData.opportunities
256
+ .map(
257
+ (opp) => `
258
+ <div class="horizon-card">
259
+ <div class="horizon-title">${escapeXmlAttr(opp.title || "")}</div>
260
+ <div class="horizon-possible">${escapeXmlAttr(opp.whatsPossible || "")}</div>
261
+ ${opp.howToTry ? `<div class="horizon-tip"><strong>Getting started:</strong> ${escapeXmlAttr(opp.howToTry)}</div>` : ""}
262
+ ${opp.copyablePrompt ? `<code class="copyable-prompt">${escapeXmlAttr(opp.copyablePrompt)}</code>` : ""}
263
+ </div>
264
+ `,
265
+ )
266
+ .join("")}
267
+ </div>
268
+ `;
269
+ }
270
+
271
+ function renderFunEndingHtml(insights: InsightResults): string {
272
+ const funEnding = insights.funEnding as { headline?: string; detail?: string } | undefined;
273
+
274
+ if (!funEnding?.headline) return "";
275
+
276
+ return `
277
+ <div class="fun-ending">
278
+ <div class="fun-headline">"${escapeXmlAttr(funEnding.headline)}"</div>
279
+ ${funEnding.detail ? `<div class="fun-detail">${escapeXmlAttr(funEnding.detail)}</div>` : ""}
280
+ </div>
281
+ `;
282
+ }
283
+
284
+ function renderGoalToolChartRow(data: AggregatedData): string {
285
+ return `
286
+ <div class="charts-row">
287
+ <div class="chart-card">
288
+ <div class="chart-title">What You Wanted</div>
289
+ ${generateBarChartHtml(data.goalCategories, "#2563eb")}
290
+ </div>
291
+ <div class="chart-card">
292
+ <div class="chart-title">Top Tools Used</div>
293
+ ${generateBarChartHtml(data.toolCounts, "#0891b2")}
294
+ </div>
295
+ </div>`;
296
+ }
297
+
298
+ function renderLanguageSessionChartRow(data: AggregatedData): string {
299
+ return `
300
+ <div class="charts-row">
301
+ <div class="chart-card">
302
+ <div class="chart-title">Languages</div>
303
+ ${generateBarChartHtml(data.languages, "#10b981")}
304
+ </div>
305
+ <div class="chart-card">
306
+ <div class="chart-title">Session Types</div>
307
+ ${generateBarChartHtml(data.sessionTypes || {}, "#8b5cf6")}
308
+ </div>
309
+ </div>`;
310
+ }
311
+
312
+ function renderResponseTimeCard(data: AggregatedData): string {
313
+ return `
314
+ <div class="chart-card" style="margin: 24px 0;">
315
+ <div class="chart-title">User Response Time Distribution</div>
316
+ ${generateResponseTimeHistogramHtml(data.userResponseTimes)}
317
+ <div style="font-size: 12px; color: #64748b; margin-top: 8px;">
318
+ Median: ${data.medianResponseTime.toFixed(1)}s · Average: ${data.avgResponseTime.toFixed(1)}s
319
+ </div>
320
+ </div>`;
321
+ }
322
+
323
+ function renderMultiClaudingCard(data: AggregatedData): string {
324
+ return `
325
+ <div class="chart-card" style="margin: 24px 0;">
326
+ <div class="chart-title">Multi-PI (Parallel Sessions)</div>
327
+ ${
328
+ data.multiClauding.overlapEvents === 0
329
+ ? `<p style="font-size: 14px; color: #64748b; padding: 8px 0;">No parallel session usage detected. You typically work with one PI session at a time.</p>`
330
+ : `
331
+ <div style="display: flex; gap: 24px; margin: 12px 0;">
332
+ <div style="text-align: center;"><div style="font-size: 24px; font-weight: 700; color: #7c3aed;">${data.multiClauding.overlapEvents}</div><div style="font-size: 11px; color: #64748b; text-transform: uppercase;">Overlap Events</div></div>
333
+ <div style="text-align: center;"><div style="font-size: 24px; font-weight: 700; color: #7c3aed;">${data.multiClauding.sessionsInvolved}</div><div style="font-size: 11px; color: #64748b; text-transform: uppercase;">Sessions Involved</div></div>
334
+ </div>
335
+ `
336
+ }
337
+ </div>`;
338
+ }
339
+
340
+ function renderTimeOfDayChartRow(data: AggregatedData): string {
341
+ return `
342
+ <div class="charts-row">
343
+ <div class="chart-card">
344
+ <div class="chart-title" style="display: flex; align-items: center; gap: 12px;">
345
+ User Messages by Time of Day
346
+ <select id="timezone-select" style="font-size: 12px; padding: 4px 8px; border-radius: 4px; border: 1px solid #e2e8f0;">
347
+ <option value="-8">PT (UTC-8)</option>
348
+ <option value="-5">ET (UTC-5)</option>
349
+ <option value="0">London (UTC)</option>
350
+ <option value="1">CET (UTC+1)</option>
351
+ <option value="9">Tokyo (UTC+9)</option>
352
+ </select>
353
+ </div>
354
+ <div id="hour-histogram">${generateTimeOfDayChartHtml(data.messageHours, -8)}</div>
355
+ </div>
356
+ <div class="chart-card">
357
+ <div class="chart-title">Tool Errors Encountered</div>
358
+ ${Object.keys(data.toolErrorCategories).length > 0 ? generateBarChartHtml(data.toolErrorCategories, "#dc2626") : emptyHtml("No tool errors")}
359
+ </div>
360
+ </div>`;
361
+ }
362
+
363
+ function renderOutcomeChartRow(data: AggregatedData): string {
364
+ return `
365
+ <div class="charts-row">
366
+ <div class="chart-card">
367
+ <div class="chart-title">What Helped Most</div>
368
+ ${generateBarChartHtml(data.success, "#16a34a")}
369
+ </div>
370
+ <div class="chart-card">
371
+ <div class="chart-title">Outcomes</div>
372
+ ${generateBarChartHtml(data.outcomes, "#8b5cf6", 6, OUTCOME_ORDER)}
373
+ </div>
374
+ </div>`;
375
+ }
376
+
377
+ function renderFrictionChartRow(data: AggregatedData): string {
378
+ return `
379
+ <div class="charts-row">
380
+ <div class="chart-card">
381
+ <div class="chart-title">Primary Friction Types</div>
382
+ ${generateBarChartHtml(data.friction, "#dc2626")}
383
+ </div>
384
+ <div class="chart-card">
385
+ <div class="chart-title">Inferred Satisfaction</div>
386
+ ${generateBarChartHtml(data.satisfaction, "#eab308", 6, SATISFACTION_ORDER)}
387
+ </div>
388
+ </div>`;
389
+ }
390
+
391
+ export function generateHtmlReport(data: AggregatedData, insights: InsightResults): string {
392
+ const atAGlanceHtml = renderAtAGlanceHtml(insights);
393
+ const projectAreasHtml = renderProjectAreasHtml(insights);
394
+ const interactionHtml = renderInteractionHtml(insights);
395
+ const whatWorksHtml = renderWhatWorksHtml(insights);
396
+ const frictionHtml = renderFrictionHtml(insights);
397
+ const suggestionsHtml = renderSuggestionsHtml(insights);
398
+ const horizonHtml = renderHorizonHtml(insights);
399
+ const funEndingHtml = renderFunEndingHtml(insights);
400
+ const hourCountsJson = getHourCountsJson(data.messageHours);
401
+
402
+ const sessionLabel =
403
+ data.totalSessionsScanned && data.totalSessionsScanned > data.totalSessions
404
+ ? `${data.totalSessionsScanned.toLocaleString()} sessions total \u00b7 ${data.totalSessions} analyzed`
405
+ : `${data.totalSessions} sessions`;
406
+
407
+ const failureBanner =
408
+ data.facetExtractionFailed > 0 || data.insightSectionsFailed.length > 0
409
+ ? `<div class="failure-banner">
410
+ ${data.facetExtractionFailed > 0 ? `<span>⚠ ${data.facetExtractionFailed} of ${data.facetExtractionAttempted} LLM facet extractions failed</span>` : ""}
411
+ ${data.insightSectionsFailed.length > 0 ? `<span>⚠ ${data.insightSectionsFailed.length} insight section(s) unavailable: ${escapeXmlAttr(data.insightSectionsFailed.join(", "))}</span>` : ""}
412
+ <span class="failure-hint">Run \`/supi-insights\` again later to retry.</span>
413
+ </div>`
414
+ : "";
415
+
416
+ return `<!DOCTYPE html>
417
+ <html>
418
+ <head>
419
+ <meta charset="utf-8">
420
+ <title>PI Insights</title>
421
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
422
+ <style>${REPORT_CSS}</style>
423
+ </head>
424
+ <body>
425
+ <div class="container">
426
+ <h1>PI Insights</h1>
427
+ <p class="subtitle">${data.totalMessages.toLocaleString()} messages across ${sessionLabel} | ${data.dateRange.start} to ${data.dateRange.end}</p>
428
+
429
+ ${failureBanner}
430
+
431
+ ${atAGlanceHtml}
432
+
433
+ <div class="stats-row">
434
+ <div class="stat"><div class="stat-value">${data.totalMessages.toLocaleString()}</div><div class="stat-label">Messages</div></div>
435
+ <div class="stat"><div class="stat-value">+${data.totalLinesAdded.toLocaleString()}/-${data.totalLinesRemoved.toLocaleString()}</div><div class="stat-label">Lines</div></div>
436
+ <div class="stat"><div class="stat-value">${data.totalFilesModified}</div><div class="stat-label">Files</div></div>
437
+ <div class="stat"><div class="stat-value">${data.daysActive}</div><div class="stat-label">Days</div></div>
438
+ <div class="stat"><div class="stat-value">${data.messagesPerDay}</div><div class="stat-label">Msgs/Day</div></div>
439
+ </div>
440
+
441
+ ${projectAreasHtml}
442
+
443
+ ${renderGoalToolChartRow(data)}
444
+ ${renderLanguageSessionChartRow(data)}
445
+
446
+ ${interactionHtml}
447
+
448
+ ${renderResponseTimeCard(data)}
449
+ ${renderMultiClaudingCard(data)}
450
+ ${renderTimeOfDayChartRow(data)}
451
+
452
+ ${whatWorksHtml}
453
+
454
+ ${renderOutcomeChartRow(data)}
455
+
456
+ ${frictionHtml}
457
+
458
+ ${renderFrictionChartRow(data)}
459
+
460
+ ${suggestionsHtml}
461
+ ${horizonHtml}
462
+ ${funEndingHtml}
463
+ </div>
464
+ <script>${generateReportJs(hourCountsJson)}</script>
465
+ </body>
466
+ </html>`;
467
+ }
468
+
469
+ function markdownToHtml(md: string): string {
470
+ if (!md) return "";
471
+ return md
472
+ .split("\n\n")
473
+ .map((p) => {
474
+ let html = escapeXmlAttr(p);
475
+ html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
476
+ html = html.replace(/^- /gm, "• ");
477
+ html = html.replace(/\n/g, "<br>");
478
+ return `<p>${html}</p>`;
479
+ })
480
+ .join("\n");
481
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./insights.ts";