@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
@@ -0,0 +1,395 @@
1
+ // Insight generator — produce narrative insights from aggregated data via LLM calls.
2
+
3
+ import { complete } from "@earendil-works/pi-ai";
4
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import type { AggregatedData, InsightResults, SessionFacets } from "./types.ts";
6
+ import { withRetry } from "./utils.ts";
7
+
8
+ type InsightSection = {
9
+ name: keyof InsightResults;
10
+ prompt: string;
11
+ maxTokens: number;
12
+ };
13
+
14
+ const INSIGHT_SECTIONS: InsightSection[] = [
15
+ {
16
+ name: "projectAreas",
17
+ prompt: `Analyze this PI usage data and identify project areas.
18
+
19
+ RESPOND WITH ONLY A VALID JSON OBJECT:
20
+ {
21
+ "areas": [
22
+ {"name": "Area name", "sessionCount": N, "description": "2-3 sentences about what was worked on and how PI was used."}
23
+ ]
24
+ }
25
+
26
+ Include 4-5 areas.`,
27
+ maxTokens: 4096,
28
+ },
29
+ {
30
+ name: "interactionStyle",
31
+ prompt: `Analyze this PI usage data and describe the user's interaction style.
32
+
33
+ RESPOND WITH ONLY A VALID JSON OBJECT:
34
+ {
35
+ "narrative": "2-3 paragraphs analyzing HOW the user interacts with PI. Use second person 'you'. Describe patterns: iterate quickly vs detailed upfront specs? Interrupt often or let the agent run? Include specific examples. Use **bold** for key insights.",
36
+ "keyPattern": "One sentence summary of most distinctive interaction style"
37
+ }`,
38
+ maxTokens: 4096,
39
+ },
40
+ {
41
+ name: "whatWorks",
42
+ prompt: `Analyze this PI usage data and identify what's working well for this user. Use second person ("you").
43
+
44
+ RESPOND WITH ONLY A VALID JSON OBJECT:
45
+ {
46
+ "intro": "1 sentence of context",
47
+ "impressiveWorkflows": [
48
+ {"title": "Short title (3-6 words)", "description": "2-3 sentences describing the impressive workflow or approach. Use 'you' not 'the user'."}
49
+ ]
50
+ }
51
+
52
+ Include 3 impressive workflows.`,
53
+ maxTokens: 4096,
54
+ },
55
+ {
56
+ name: "frictionAnalysis",
57
+ prompt: `Analyze this PI usage data and identify friction points for this user. Use second person ("you").
58
+
59
+ RESPOND WITH ONLY A VALID JSON OBJECT:
60
+ {
61
+ "intro": "1 sentence summarizing friction patterns",
62
+ "categories": [
63
+ {"category": "Concrete category name", "description": "1-2 sentences explaining this category and what could be done differently. Use 'you' not 'the user'.", "examples": ["Specific example with consequence", "Another example"]}
64
+ ]
65
+ }
66
+
67
+ Include 3 friction categories with 2 examples each.`,
68
+ maxTokens: 4096,
69
+ },
70
+ {
71
+ name: "suggestions",
72
+ prompt: `Analyze this PI usage data and suggest improvements.
73
+
74
+ RESPOND WITH ONLY A VALID JSON OBJECT:
75
+ {
76
+ "claudeMdAdditions": [
77
+ {"addition": "A specific line or block to add to CLAUDE.md based on workflow patterns.", "why": "1 sentence explaining why this would help based on actual sessions", "promptScaffold": "Instructions for where to add this in CLAUDE.md"}
78
+ ],
79
+ "featuresToTry": [
80
+ {"feature": "Feature name", "oneLiner": "What it does", "whyForYou": "Why this would help YOU based on your sessions", "exampleCode": "Actual command or config to copy"}
81
+ ],
82
+ "usagePatterns": [
83
+ {"title": "Short title", "suggestion": "1-2 sentence summary", "detail": "3-4 sentences explaining how this applies to YOUR work", "copyablePrompt": "A specific prompt to copy and try"}
84
+ ]
85
+ }
86
+
87
+ IMPORTANT: PRIORITIZE instructions that appear MULTIPLE TIMES in the user data.`,
88
+ maxTokens: 4096,
89
+ },
90
+ {
91
+ name: "onTheHorizon",
92
+ prompt: `Analyze this PI usage data and identify future opportunities.
93
+
94
+ RESPOND WITH ONLY A VALID JSON OBJECT:
95
+ {
96
+ "intro": "1 sentence about evolving AI-assisted development",
97
+ "opportunities": [
98
+ {"title": "Short title (4-8 words)", "whatsPossible": "2-3 ambitious sentences about autonomous workflows", "howToTry": "1-2 sentences mentioning relevant tooling", "copyablePrompt": "Detailed prompt to try"}
99
+ ]
100
+ }
101
+
102
+ Include 3 opportunities. Think BIG - autonomous workflows, parallel agents, iterating against tests.`,
103
+ maxTokens: 4096,
104
+ },
105
+ {
106
+ name: "funEnding",
107
+ prompt: `Analyze this PI usage data and find a memorable moment.
108
+
109
+ RESPOND WITH ONLY A VALID JSON OBJECT:
110
+ {
111
+ "headline": "A memorable QUALITATIVE moment from the transcripts - not a statistic. Something human, funny, or surprising.",
112
+ "detail": "Brief context about when/where this happened"
113
+ }
114
+
115
+ Find something genuinely interesting or amusing from the session summaries.`,
116
+ maxTokens: 2048,
117
+ },
118
+ ];
119
+
120
+ export async function generateInsights(
121
+ data: AggregatedData,
122
+ facets: Map<string, SessionFacets>,
123
+ ctx: ExtensionContext,
124
+ ): Promise<InsightResults> {
125
+ const dataContext = buildDataContext(data, facets);
126
+
127
+ // Run sections in parallel
128
+ const results = await Promise.all(
129
+ INSIGHT_SECTIONS.map((section) => generateSectionInsight(section, dataContext, ctx)),
130
+ );
131
+
132
+ const insights: InsightResults = {};
133
+ for (const { name, result } of results) {
134
+ if (result) {
135
+ insights[name] = result;
136
+ }
137
+ }
138
+
139
+ // Generate at_a_glance sequentially (needs other sections)
140
+ const atAGlance = await generateAtAGlance(data, insights, dataContext, ctx);
141
+ if (atAGlance) {
142
+ insights.atAGlance = atAGlance;
143
+ }
144
+
145
+ return insights;
146
+ }
147
+
148
+ async function resolveModel(ctx: ExtensionContext): Promise<{
149
+ model: NonNullable<ExtensionContext["model"]>;
150
+ apiKey: string;
151
+ headers: Record<string, string>;
152
+ } | null> {
153
+ const model = ctx.model ?? ctx.modelRegistry.getAvailable()[0] ?? null;
154
+ if (!model) return null;
155
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
156
+ if (!auth.ok || !auth.apiKey) return null;
157
+ return { model, apiKey: auth.apiKey, headers: auth.headers ?? {} };
158
+ }
159
+
160
+ async function generateSectionInsight(
161
+ section: InsightSection,
162
+ dataContext: string,
163
+ ctx: ExtensionContext,
164
+ ): Promise<{ name: keyof InsightResults; result: unknown }> {
165
+ const resolved = await resolveModel(ctx);
166
+ if (!resolved) return { name: section.name, result: null };
167
+
168
+ const { model, apiKey, headers } = resolved;
169
+
170
+ const response = await withRetry(
171
+ async () => {
172
+ const res = await complete(
173
+ model,
174
+ {
175
+ systemPrompt: "",
176
+ messages: [
177
+ {
178
+ role: "user",
179
+ content: [{ type: "text", text: `${section.prompt}\n\nDATA:\n${dataContext}` }],
180
+ timestamp: Date.now(),
181
+ },
182
+ ],
183
+ },
184
+ {
185
+ apiKey,
186
+ headers,
187
+ signal: ctx.signal,
188
+ maxTokens: section.maxTokens,
189
+ },
190
+ );
191
+ return res;
192
+ },
193
+ 2,
194
+ 1000,
195
+ );
196
+
197
+ if (!response) return { name: section.name, result: null };
198
+
199
+ const text = response.content
200
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
201
+ .map((c) => c.text)
202
+ .join("");
203
+
204
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
205
+ if (!jsonMatch) return { name: section.name, result: null };
206
+
207
+ try {
208
+ return { name: section.name, result: JSON.parse(jsonMatch[0]) };
209
+ } catch {
210
+ return { name: section.name, result: null };
211
+ }
212
+ }
213
+
214
+ async function generateAtAGlance(
215
+ _data: AggregatedData,
216
+ insights: InsightResults,
217
+ dataContext: string,
218
+ ctx: ExtensionContext,
219
+ ): Promise<unknown> {
220
+ const projectAreasText =
221
+ (
222
+ insights.projectAreas as {
223
+ areas?: Array<{ name: string; description: string }>;
224
+ }
225
+ )?.areas
226
+ ?.map((a) => `- ${a.name}: ${a.description}`)
227
+ .join("\n") || "";
228
+
229
+ const bigWinsText =
230
+ (
231
+ insights.whatWorks as {
232
+ impressiveWorkflows?: Array<{ title: string; description: string }>;
233
+ }
234
+ )?.impressiveWorkflows
235
+ ?.map((w) => `- ${w.title}: ${w.description}`)
236
+ .join("\n") || "";
237
+
238
+ const frictionText =
239
+ (
240
+ insights.frictionAnalysis as {
241
+ categories?: Array<{ category: string; description: string }>;
242
+ }
243
+ )?.categories
244
+ ?.map((c) => `- ${c.category}: ${c.description}`)
245
+ .join("\n") || "";
246
+
247
+ const featuresText =
248
+ (
249
+ insights.suggestions as {
250
+ featuresToTry?: Array<{ feature: string; oneLiner: string }>;
251
+ }
252
+ )?.featuresToTry
253
+ ?.map((f) => `- ${f.feature}: ${f.oneLiner}`)
254
+ .join("\n") || "";
255
+
256
+ const horizonText =
257
+ (
258
+ insights.onTheHorizon as {
259
+ opportunities?: Array<{ title: string; whatsPossible: string }>;
260
+ }
261
+ )?.opportunities
262
+ ?.map((o) => `- ${o.title}: ${o.whatsPossible}`)
263
+ .join("\n") || "";
264
+
265
+ const prompt = `You're writing an "At a Glance" summary for a PI usage insights report.
266
+
267
+ Use this 4-part structure:
268
+
269
+ 1. **What's working** - What is the user's unique style of interacting with PI and what are some impactful things they've done?
270
+
271
+ 2. **What's hindering you** - Split into (a) the agent's fault and (b) user-side friction.
272
+
273
+ 3. **Quick wins to try** - Specific features or workflow techniques.
274
+
275
+ 4. **Ambitious workflows for better models** - As models improve, what workflows should they prepare for?
276
+
277
+ Keep each section to 2-3 sentences. Use a coaching tone.
278
+
279
+ RESPOND WITH ONLY A VALID JSON OBJECT:
280
+ {
281
+ "whatsWorking": "...",
282
+ "whatsHindering": "...",
283
+ "quickWins": "...",
284
+ "ambitiousWorkflows": "..."
285
+ }
286
+
287
+ SESSION DATA:
288
+ ${dataContext}
289
+
290
+ ## Project Areas
291
+ ${projectAreasText}
292
+
293
+ ## Big Wins
294
+ ${bigWinsText}
295
+
296
+ ## Friction Categories
297
+ ${frictionText}
298
+
299
+ ## Features to Try
300
+ ${featuresText}
301
+
302
+ ## On the Horizon
303
+ ${horizonText}`;
304
+
305
+ const resolved = await resolveModel(ctx);
306
+ if (!resolved) return null;
307
+
308
+ const { model, apiKey, headers } = resolved;
309
+
310
+ const response = await withRetry(
311
+ async () => {
312
+ const res = await complete(
313
+ model,
314
+ {
315
+ systemPrompt: "",
316
+ messages: [
317
+ {
318
+ role: "user",
319
+ content: [{ type: "text", text: prompt }],
320
+ timestamp: Date.now(),
321
+ },
322
+ ],
323
+ },
324
+ {
325
+ apiKey,
326
+ headers,
327
+ signal: ctx.signal,
328
+ maxTokens: 4096,
329
+ },
330
+ );
331
+ return res;
332
+ },
333
+ 2,
334
+ 1000,
335
+ );
336
+
337
+ if (!response) return null;
338
+
339
+ const text = response.content
340
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
341
+ .map((c) => c.text)
342
+ .join("");
343
+
344
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
345
+ if (!jsonMatch) return null;
346
+
347
+ try {
348
+ return JSON.parse(jsonMatch[0]);
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+
354
+ function buildDataContext(data: AggregatedData, facets: Map<string, SessionFacets>): string {
355
+ const facetSummaries = Array.from(facets.values())
356
+ .slice(0, 50)
357
+ .map((f) => `- ${f.briefSummary} (${f.outcome}, ${f.claudeHelpfulness})`)
358
+ .join("\n");
359
+
360
+ const frictionDetails = Array.from(facets.values())
361
+ .filter((f) => f.frictionDetail)
362
+ .slice(0, 20)
363
+ .map((f) => `- ${f.frictionDetail}`)
364
+ .join("\n");
365
+
366
+ return (
367
+ JSON.stringify(
368
+ {
369
+ sessions: data.totalSessions,
370
+ analyzed: data.sessionsWithFacets,
371
+ dateRange: data.dateRange,
372
+ messages: data.totalMessages,
373
+ hours: Math.round(data.totalDurationHours),
374
+ commits: data.gitCommits,
375
+ topTools: Object.entries(data.toolCounts)
376
+ .sort((a, b) => b[1] - a[1])
377
+ .slice(0, 8),
378
+ topGoals: Object.entries(data.goalCategories)
379
+ .sort((a, b) => b[1] - a[1])
380
+ .slice(0, 8),
381
+ outcomes: data.outcomes,
382
+ satisfaction: data.satisfaction,
383
+ friction: data.friction,
384
+ success: data.success,
385
+ languages: data.languages,
386
+ },
387
+ null,
388
+ 2,
389
+ ) +
390
+ "\n\nSESSION SUMMARIES:\n" +
391
+ facetSummaries +
392
+ "\n\nFRICTION DETAILS:\n" +
393
+ frictionDetails
394
+ );
395
+ }