codex-session-insights 0.2.0 → 0.2.1

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.
@@ -276,24 +276,27 @@ const SECTION_DEFS = [
276
276
  contextKind: 'project_areas',
277
277
  schemaName: 'codex_project_areas',
278
278
  schema: PROJECT_AREAS_SCHEMA,
279
- prompt: `Analyze this Codex usage data and identify project areas.
279
+ prompt: `Analyze this Codex usage data and identify the user's main workstreams.
280
280
 
281
281
  RESPOND WITH ONLY A VALID JSON OBJECT:
282
282
  {
283
283
  "areas": [
284
- {"name": "Area name", "session_count": 0, "description": "2-3 sentences about what was worked on and how Codex was used."}
284
+ {"name": "Area name", "session_count": 0, "description": "2-3 sentences describing the workstream, its recurring tasks, and why it matters."}
285
285
  ]
286
286
  }
287
287
 
288
- Include 4-5 areas. Skip Codex self-hosting/meta work unless it is a dominant project area.
288
+ Include 3-4 areas. Skip Codex self-hosting/meta work unless it is a dominant project area.
289
289
 
290
290
  Guardrails:
291
291
  - Use concrete project or workstream names, not generic labels like "coding" or "development"
292
292
  - Base areas on repeated evidence across summaries, not one-off threads
293
293
  - Prefer project + task framing over tool-centric framing
294
- - Group related tasks into a coherent workstream instead of listing each task separately
295
- - Each description should sound like a mini report paragraph: what kinds of work clustered together, then how Codex contributed
296
- - Prefer descriptions that mention representative tasks or artifacts instead of vague labels`,
294
+ - Group related tasks into a coherent long-running workstream instead of listing each task separately
295
+ - Prefer fewer, broader areas that still feel accurate over a more complete but fragmented list
296
+ - Do not turn recent sub-tasks, bugfixes, or cleanup passes into separate areas unless they clearly form their own repeated stream
297
+ - Each description should read like a workstream summary, not a changelog
298
+ - Mention representative tasks, artifacts, or decisions so the area feels concrete without enumerating every thread
299
+ - Keep the focus on what the user was trying to accomplish; mention Codex only lightly when it clarifies the shape of the work`,
297
300
  },
298
301
  {
299
302
  name: 'interaction_style',
@@ -311,9 +314,12 @@ RESPOND WITH ONLY A VALID JSON OBJECT:
311
314
 
312
315
  Guardrails:
313
316
  - Focus on stable interaction patterns, not isolated moments
314
- - Talk about how the user scopes work, interrupts, redirects, or trusts execution
317
+ - Talk about how the user scopes work, redirects goals, sets acceptance bars, or trusts execution
318
+ - Prefer evidence from user requests, follow-up corrections, repeated constraints, and outcome patterns over implementation telemetry
315
319
  - Do not infer user preference from Codex's default tool mix or harness behavior; high exec/tool usage can reflect the agent's operating style rather than the user's instructions
316
320
  - Treat shell usage, file reads, and verification commands as weak evidence unless the user explicitly asked for that working style
321
+ - Do not infer style from repository type, documentation volume, or language mix alone
322
+ - Avoid turning a single repo's workflow shape into a personality claim about the user
317
323
  - If evidence is mixed, describe the tension instead of forcing one clean story`,
318
324
  },
319
325
  {
@@ -360,6 +366,7 @@ Include 3 friction categories with 2 examples each.
360
366
  Guardrails:
361
367
  - Separate model-side friction from user/workflow-side friction when useful
362
368
  - Examples must be concrete and tied to the supplied evidence
369
+ - Treat overlap or concurrency metrics as weak supporting evidence unless the summaries or friction details also show real switching pain
363
370
  - Do not invent root causes that are not visible in the data`,
364
371
  },
365
372
  {
@@ -401,6 +408,9 @@ Guardrails:
401
408
  - Suggest only actions that clearly connect to repeated evidence
402
409
  - Avoid generic advice like "give more context" unless it is overwhelmingly justified
403
410
  - Prefer changes with strong leverage: repo memory, repeatable workflows, automation, or parallelism
411
+ - Do not recommend first-time adoption of AGENTS.md, Skills, codex exec, Sub-agents, or MCP Servers when the capability_adoption evidence shows the user already uses them in a moderate or strong way
412
+ - When a capability is already adopted, suggest a deeper refinement or a tighter operating pattern instead of basic adoption
413
+ - Distinguish "you should start using this" from "you should formalize or deepen how you already use this"
404
414
  - Write AGENTS.md additions as directly pasteable instruction lines, not commentary about instructions
405
415
  - Make feature examples immediately usable; avoid placeholders like "insert your repo path here" unless unavoidable
406
416
  - Make usage pattern suggestions sound like concrete next actions the user can try today, not abstract best practices`,
@@ -989,6 +999,8 @@ function buildInsightContext(report, threadSummaries, facets) {
989
999
  ),
990
1000
  ).slice(0, MAX_USER_INSTRUCTIONS)
991
1001
 
1002
+ const capabilityAdoption = summarizeCapabilityAdoption(report, threadSummaries, facets)
1003
+
992
1004
  return {
993
1005
  metadata: {
994
1006
  generated_at: report.metadata.generatedAt,
@@ -1025,6 +1037,7 @@ function buildInsightContext(report, threadSummaries, facets) {
1025
1037
  friction,
1026
1038
  success,
1027
1039
  },
1040
+ capability_adoption: capabilityAdoption,
1028
1041
  session_summaries: sortedFacets.slice(0, MAX_CONTEXT_FACETS).map(facet => ({
1029
1042
  thread_id: facet.threadId,
1030
1043
  title: truncateForContext(facet.title, 80),
@@ -1051,6 +1064,69 @@ function buildInsightContext(report, threadSummaries, facets) {
1051
1064
  }
1052
1065
  }
1053
1066
 
1067
+ function summarizeCapabilityAdoption(report, threadSummaries, facets) {
1068
+ const textByThread = new Map()
1069
+ for (const thread of threadSummaries) {
1070
+ textByThread.set(
1071
+ thread.id,
1072
+ [thread.title, thread.firstUserMessage]
1073
+ .map(value => String(value || ''))
1074
+ .join('\n')
1075
+ .toLowerCase(),
1076
+ )
1077
+ }
1078
+
1079
+ for (const facet of facets) {
1080
+ const existing = textByThread.get(facet.threadId) || ''
1081
+ const facetText = [
1082
+ facet.underlying_goal,
1083
+ facet.brief_summary,
1084
+ ...(facet.user_instructions || []),
1085
+ ]
1086
+ .map(value => String(value || ''))
1087
+ .join('\n')
1088
+ .toLowerCase()
1089
+ textByThread.set(facet.threadId, `${existing}\n${facetText}`.trim())
1090
+ }
1091
+
1092
+ const detectMentionedThreads = regex => {
1093
+ let count = 0
1094
+ for (const text of textByThread.values()) {
1095
+ if (regex.test(text)) count += 1
1096
+ }
1097
+ return count
1098
+ }
1099
+
1100
+ const totalThreads = Math.max(1, Number(report.metadata.threadCount || threadSummaries.length || 0))
1101
+ const signals = {
1102
+ agents_md: detectMentionedThreads(/\bagents\.md\b/i),
1103
+ skills: detectMentionedThreads(/\bskills?\b/i),
1104
+ codex_exec: detectMentionedThreads(/\bcodex exec\b/i),
1105
+ subagents: Number(report.summary.sessionsUsingTaskAgent || 0),
1106
+ mcp_servers: Number(report.summary.sessionsUsingMcp || 0),
1107
+ web_search: Number(report.summary.sessionsUsingWebSearch || 0),
1108
+ web_fetch: Number(report.summary.sessionsUsingWebFetch || 0),
1109
+ }
1110
+
1111
+ return Object.fromEntries(
1112
+ Object.entries(signals).map(([key, count]) => [
1113
+ key,
1114
+ {
1115
+ count,
1116
+ status: classifyCapabilityAdoption(count, totalThreads),
1117
+ },
1118
+ ]),
1119
+ )
1120
+ }
1121
+
1122
+ function classifyCapabilityAdoption(count, totalThreads) {
1123
+ const share = Number(count || 0) / Math.max(1, Number(totalThreads || 0))
1124
+ if (count >= 10 || share >= 0.25) return 'strong'
1125
+ if (count >= 4 || share >= 0.1) return 'moderate'
1126
+ if (count > 0) return 'light'
1127
+ return 'none'
1128
+ }
1129
+
1054
1130
  function buildAtAGlancePrompt(context, insights) {
1055
1131
  return `You are writing an "At a Glance" summary for a Codex usage insights report.
1056
1132
 
package/lib/report.js CHANGED
@@ -1,3 +1,4 @@
1
+ import os from 'node:os'
1
2
  import path from 'node:path'
2
3
  import { promises as fs } from 'node:fs'
3
4
 
@@ -6,6 +7,9 @@ export function buildReport(threadSummaries, options = {}) {
6
7
  const modelCounts = {}
7
8
  const toolCounts = {}
8
9
  const commandKindCounts = {}
10
+ const capabilityCounts = {}
11
+ const outcomeCounts = {}
12
+ const sessionTypeCounts = {}
9
13
  const toolFailureCounts = {}
10
14
  const activeHourCounts = {}
11
15
  const responseTimes = []
@@ -81,8 +85,20 @@ export function buildReport(threadSummaries, options = {}) {
81
85
  if (thread.usesMcp) sessionsUsingMcp += 1
82
86
  if (thread.usesWebSearch) sessionsUsingWebSearch += 1
83
87
  if (thread.usesWebFetch) sessionsUsingWebFetch += 1
88
+ if (thread.filesModified > 0) increment(capabilityCounts, 'Repo edits')
89
+ if (thread.gitCommits > 0 || thread.gitPushes > 0) increment(capabilityCounts, 'Git activity')
84
90
  }
85
91
 
92
+ for (const facet of options.facets || []) {
93
+ if (facet.outcome) increment(outcomeCounts, facet.outcome)
94
+ if (facet.session_type) increment(sessionTypeCounts, facet.session_type)
95
+ }
96
+
97
+ if (sessionsUsingTaskAgent > 0) increment(capabilityCounts, 'Sub-agents', sessionsUsingTaskAgent)
98
+ if (sessionsUsingMcp > 0) increment(capabilityCounts, 'MCP servers', sessionsUsingMcp)
99
+ if (sessionsUsingWebSearch > 0) increment(capabilityCounts, 'Web search', sessionsUsingWebSearch)
100
+ if (sessionsUsingWebFetch > 0) increment(capabilityCounts, 'Web fetch', sessionsUsingWebFetch)
101
+
86
102
  const sortedThreads = [...threadSummaries].sort(
87
103
  (a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt),
88
104
  )
@@ -130,6 +146,9 @@ export function buildReport(threadSummaries, options = {}) {
130
146
  models: topEntries(modelCounts, 10),
131
147
  tools: topEntries(toolCounts, 12),
132
148
  commandKinds: topEntries(commandKindCounts, 12),
149
+ capabilities: topEntries(capabilityCounts, 8),
150
+ outcomes: topEntries(outcomeCounts, 8),
151
+ sessionTypes: topEntries(sessionTypeCounts, 8),
133
152
  toolFailures: topEntries(toolFailureCounts, 8),
134
153
  toolErrorCategories: topEntries(toolErrorCategoryCounts, 8),
135
154
  activeHours: buildHourSeries(activeHourCounts),
@@ -176,13 +195,13 @@ export function renderTerminalSummary(report) {
176
195
  lines.push(`${report.metadata.dateRange.start} -> ${report.metadata.dateRange.end}`)
177
196
  }
178
197
  lines.push('')
179
- lines.push(`${text.topTools}:`)
180
- for (const item of report.charts.tools.slice(0, 5)) {
181
- lines.push(` ${item.label}: ${item.value}`)
182
- }
183
- lines.push('')
184
198
  lines.push(`${text.topProjects}:`)
185
199
  for (const item of report.charts.projects.slice(0, 5)) {
200
+ lines.push(` ${formatProjectLabel(item.label)}: ${item.value}`)
201
+ }
202
+ lines.push('')
203
+ lines.push(`${text.modelMix}:`)
204
+ for (const item of report.charts.models.slice(0, 5)) {
186
205
  lines.push(` ${item.label}: ${item.value}`)
187
206
  }
188
207
  return lines.join('\n')
@@ -193,9 +212,11 @@ function renderHtmlReport(report) {
193
212
  const insights = report.insights
194
213
  if (insights && !insights.__lang) insights.__lang = report.metadata.language
195
214
  const analysisUsage = report.analysisUsage || null
196
- const topTools = renderBarList(report.charts.tools)
197
- const topProjects = renderBarList(report.charts.projects)
198
- const commandKinds = renderBarList(report.charts.commandKinds)
215
+ const topProjects = renderBarList(report.charts.projects, { formatLabel: formatProjectLabel })
216
+ const modelMix = renderBarList(report.charts.models)
217
+ const sessionTypes = renderBarList(report.charts.sessionTypes)
218
+ const outcomes = renderBarList(report.charts.outcomes)
219
+ const capabilitySignals = renderBarList(report.charts.capabilities)
199
220
  const toolFailures = renderBarList(report.charts.toolFailures)
200
221
  const toolErrorCategories = renderBarList(report.charts.toolErrorCategories)
201
222
  const activeHours = renderHourHistogram(report.charts.activeHours)
@@ -639,7 +660,7 @@ function renderHtmlReport(report) {
639
660
  <section class="hero">
640
661
  <span class="eyebrow">${escapeHtml(text.eyebrow)}</span>
641
662
  <h1>${escapeHtml(text.reportTitle)}</h1>
642
- <p class="meta">${escapeHtml(text.generatedLabel)} ${escapeHtml(report.metadata.generatedAt)} ${escapeHtml(text.generatedFrom)} ${report.metadata.threadCount} ${escapeHtml(text.substantiveThreads)} ${escapeHtml(text.inCodexHome)} ${escapeHtml(report.metadata.codexHome)}.</p>
663
+ <p class="meta">${escapeHtml(text.generatedLabel)} ${escapeHtml(report.metadata.generatedAt)} ${escapeHtml(text.generatedFrom)} ${report.metadata.threadCount} ${escapeHtml(text.substantiveThreads)} ${escapeHtml(text.inCodexHome)} ${escapeHtml(formatCodexHome(report.metadata.codexHome))}.</p>
643
664
  <div class="summary-grid">
644
665
  ${renderStat(text.userMessages, formatNumber(report.summary.totalUserMessages))}
645
666
  ${renderStat(text.toolCalls, formatNumber(report.summary.totalToolCalls))}
@@ -672,17 +693,25 @@ function renderHtmlReport(report) {
672
693
  ${renderOnTheHorizon(insights)}
673
694
  </div>
674
695
  <aside class="side-column">
675
- <section class="chart-panel">
676
- <h2>${escapeHtml(text.topTools)}</h2>
677
- ${topTools}
678
- </section>
679
696
  <section class="chart-panel">
680
697
  <h2>${escapeHtml(text.topProjects)}</h2>
681
698
  ${topProjects}
682
699
  </section>
683
700
  <section class="chart-panel">
684
- <h2>${escapeHtml(text.commandKinds)}</h2>
685
- ${commandKinds}
701
+ <h2>${escapeHtml(text.modelMix)}</h2>
702
+ ${modelMix}
703
+ </section>
704
+ <section class="chart-panel">
705
+ <h2>${escapeHtml(text.sessionTypes)}</h2>
706
+ ${sessionTypes}
707
+ </section>
708
+ <section class="chart-panel">
709
+ <h2>${escapeHtml(text.outcomes)}</h2>
710
+ ${outcomes}
711
+ </section>
712
+ <section class="chart-panel">
713
+ <h2>${escapeHtml(text.capabilitySignals)}</h2>
714
+ ${capabilitySignals}
686
715
  </section>
687
716
  <section class="chart-panel">
688
717
  <h2>${escapeHtml(text.failureHotspots)}</h2>
@@ -1054,15 +1083,16 @@ function renderStat(label, value) {
1054
1083
  return `<div class="stat"><div class="value">${escapeHtml(String(value))}</div><div>${escapeHtml(label)}</div></div>`
1055
1084
  }
1056
1085
 
1057
- function renderBarList(items) {
1086
+ function renderBarList(items, options = {}) {
1058
1087
  if (!items.length) return '<p class="meta">No data available.</p>'
1088
+ const formatLabel = options.formatLabel || (value => value)
1059
1089
  const maxValue = Math.max(...items.map(item => item.value), 1)
1060
1090
  return `<div class="bar-list">${items
1061
1091
  .map(
1062
1092
  item => `
1063
1093
  <div class="bar-row">
1064
1094
  <div class="bar-label">
1065
- <span>${escapeHtml(item.label)}</span>
1095
+ <span>${escapeHtml(formatLabel(item.label))}</span>
1066
1096
  <strong>${formatNumber(item.value)}</strong>
1067
1097
  </div>
1068
1098
  <div class="bar-track"><div class="bar-fill" style="width:${Math.max(6, (item.value / maxValue) * 100)}%"></div></div>
@@ -1133,6 +1163,36 @@ function topEntries(map, limit) {
1133
1163
  .map(([label, value]) => ({ label, value }))
1134
1164
  }
1135
1165
 
1166
+ function formatCodexHome(value) {
1167
+ return formatDisplayPath(value, { tailSegments: 2, preferHomeAlias: true, ellipsis: false })
1168
+ }
1169
+
1170
+ function formatProjectLabel(value) {
1171
+ return formatDisplayPath(value, { tailSegments: 2, preferHomeAlias: false, ellipsis: true })
1172
+ }
1173
+
1174
+ function formatDisplayPath(value, options = {}) {
1175
+ const text = String(value || '').trim()
1176
+ if (!text) return '(unknown)'
1177
+
1178
+ const normalized = text.replace(/\\/g, '/')
1179
+ const home = os.homedir().replace(/\\/g, '/')
1180
+ if (options.preferHomeAlias !== false && normalized === home) return '~'
1181
+ if (options.preferHomeAlias !== false && normalized.startsWith(`${home}/`)) {
1182
+ return `~/${normalized.slice(home.length + 1)}`
1183
+ }
1184
+
1185
+ const parts = normalized.split('/').filter(Boolean)
1186
+ const tailSegments = Math.max(1, Number(options.tailSegments || 2))
1187
+ if (parts.length <= tailSegments) {
1188
+ return normalized.startsWith('/') ? `/${parts.join('/')}` : parts.join('/')
1189
+ }
1190
+
1191
+ const tail = parts.slice(-tailSegments).join('/')
1192
+ if (options.ellipsis === false) return tail
1193
+ return `…/${tail}`
1194
+ }
1195
+
1136
1196
  function buildHourSeries(hourMap) {
1137
1197
  return Array.from({ length: 24 }, (_, hour) => ({
1138
1198
  hour,
@@ -1244,9 +1304,11 @@ function getReportText(lang) {
1244
1304
  filesModified: '修改文件',
1245
1305
  toolErrors: '工具错误',
1246
1306
  avgResponse: '平均响应',
1247
- topTools: 'Top Tools',
1248
1307
  topProjects: 'Top Projects',
1249
- commandKinds: '命令类型',
1308
+ modelMix: '模型分布',
1309
+ sessionTypes: '会话类型',
1310
+ outcomes: '结果分布',
1311
+ capabilitySignals: '能力信号',
1250
1312
  failureHotspots: '失败热点',
1251
1313
  errorCategories: '错误分类',
1252
1314
  timeOfDay: '活跃时段',
@@ -1321,9 +1383,11 @@ function getReportText(lang) {
1321
1383
  filesModified: 'Files Modified',
1322
1384
  toolErrors: 'Tool Errors',
1323
1385
  avgResponse: 'Avg Response',
1324
- topTools: 'Top Tools',
1325
1386
  topProjects: 'Top Projects',
1326
- commandKinds: 'Command Kinds',
1387
+ modelMix: 'Model Mix',
1388
+ sessionTypes: 'Session Types',
1389
+ outcomes: 'Outcomes',
1390
+ capabilitySignals: 'Capability Signals',
1327
1391
  failureHotspots: 'Failure Hotspots',
1328
1392
  errorCategories: 'Error Categories',
1329
1393
  timeOfDay: 'Time of Day',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-session-insights",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Generate a report analyzing your Codex sessions.",
5
5
  "type": "module",
6
6
  "bin": {