codex-session-insights 0.2.0 → 0.2.2
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 +9 -0
- package/lib/cli.js +44 -1
- package/lib/codex-data.js +56 -2
- package/lib/llm-insights.js +305 -125
- package/lib/report.js +252 -26
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -60,6 +60,12 @@ Default run:
|
|
|
60
60
|
npx codex-session-insights
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
Lite local run for prompt and layout testing:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx codex-session-insights --preset lite
|
|
67
|
+
```
|
|
68
|
+
|
|
63
69
|
Estimate first, then decide:
|
|
64
70
|
|
|
65
71
|
```bash
|
|
@@ -120,6 +126,7 @@ Current default analysis plan:
|
|
|
120
126
|
|
|
121
127
|
Important behavior defaults:
|
|
122
128
|
|
|
129
|
+
- `--preset lite` maps to `days=7`, `limit=20`, `facet-limit=8`, `preview=10`
|
|
123
130
|
- `limit` means the target number of substantive threads to include in the report, not just the first 50 indexed threads
|
|
124
131
|
- `facet-limit` means the max number of uncached per-thread facet analyses to run in a single report
|
|
125
132
|
- Report language follows a best-effort system locale check
|
|
@@ -195,7 +202,9 @@ Useful local commands:
|
|
|
195
202
|
npm install
|
|
196
203
|
npm test
|
|
197
204
|
npm run check
|
|
205
|
+
npm run report:lite
|
|
198
206
|
npm run generate:test-report
|
|
199
207
|
```
|
|
200
208
|
|
|
209
|
+
`npm run report:lite` runs a smaller local analysis preset for testing prompt and layout changes without paying the full 200/50 default cost.
|
|
201
210
|
`npm run generate:test-report` writes a deterministic sample report page to `test-artifacts/sample-report/`.
|
package/lib/cli.js
CHANGED
|
@@ -99,6 +99,7 @@ export async function runCli(argv) {
|
|
|
99
99
|
})
|
|
100
100
|
report.analysisMode = 'llm'
|
|
101
101
|
report.provider = parsed.options.provider
|
|
102
|
+
report.analysisEstimate = estimate
|
|
102
103
|
report.analysisUsage = llmResult.analysisUsage
|
|
103
104
|
|
|
104
105
|
progress.startStage(parsed.options, getUiText(parsed.options.lang).writingFiles)
|
|
@@ -135,11 +136,18 @@ export async function runCli(argv) {
|
|
|
135
136
|
|
|
136
137
|
function parseArgs(argv) {
|
|
137
138
|
let command = null
|
|
139
|
+
const explicit = {
|
|
140
|
+
days: false,
|
|
141
|
+
limit: false,
|
|
142
|
+
preview: false,
|
|
143
|
+
facetLimit: false,
|
|
144
|
+
}
|
|
138
145
|
const options = {
|
|
139
146
|
codexHome: null,
|
|
140
147
|
outDir: null,
|
|
141
148
|
jsonPath: null,
|
|
142
149
|
htmlPath: null,
|
|
150
|
+
preset: DEFAULT_SCOPE_PRESET,
|
|
143
151
|
days: 30,
|
|
144
152
|
limit: 200,
|
|
145
153
|
preview: 50,
|
|
@@ -195,14 +203,21 @@ function parseArgs(argv) {
|
|
|
195
203
|
continue
|
|
196
204
|
}
|
|
197
205
|
if (arg === '--days') {
|
|
206
|
+
explicit.days = true
|
|
198
207
|
options.days = toPositiveInt(requireValue(argv, ++i, '--days'), '--days')
|
|
199
208
|
continue
|
|
200
209
|
}
|
|
210
|
+
if (arg === '--preset') {
|
|
211
|
+
options.preset = normalizeScopePreset(requireValue(argv, ++i, '--preset'))
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
201
214
|
if (arg === '--limit') {
|
|
215
|
+
explicit.limit = true
|
|
202
216
|
options.limit = toPositiveInt(requireValue(argv, ++i, '--limit'), '--limit')
|
|
203
217
|
continue
|
|
204
218
|
}
|
|
205
219
|
if (arg === '--preview') {
|
|
220
|
+
explicit.preview = true
|
|
206
221
|
options.preview = toPositiveInt(requireValue(argv, ++i, '--preview'), '--preview')
|
|
207
222
|
continue
|
|
208
223
|
}
|
|
@@ -255,6 +270,7 @@ function parseArgs(argv) {
|
|
|
255
270
|
continue
|
|
256
271
|
}
|
|
257
272
|
if (arg === '--facet-limit') {
|
|
273
|
+
explicit.facetLimit = true
|
|
258
274
|
options.facetLimit = toPositiveInt(requireValue(argv, ++i, '--facet-limit'), '--facet-limit')
|
|
259
275
|
continue
|
|
260
276
|
}
|
|
@@ -298,6 +314,19 @@ function parseArgs(argv) {
|
|
|
298
314
|
throw new Error(`Invalid provider "${options.provider}". Expected codex-cli or openai.`)
|
|
299
315
|
}
|
|
300
316
|
|
|
317
|
+
options.preset = normalizeScopePreset(options.preset)
|
|
318
|
+
const explicitValues = {
|
|
319
|
+
days: options.days,
|
|
320
|
+
limit: options.limit,
|
|
321
|
+
preview: options.preview,
|
|
322
|
+
facetLimit: options.facetLimit,
|
|
323
|
+
}
|
|
324
|
+
Object.assign(options, applyScopePreset(options, options.preset))
|
|
325
|
+
if (explicit.days) options.days = explicitValues.days
|
|
326
|
+
if (explicit.limit) options.limit = explicitValues.limit
|
|
327
|
+
if (explicit.preview) options.preview = explicitValues.preview
|
|
328
|
+
if (explicit.facetLimit) options.facetLimit = explicitValues.facetLimit
|
|
329
|
+
|
|
301
330
|
return { command, options, help }
|
|
302
331
|
}
|
|
303
332
|
|
|
@@ -330,6 +359,7 @@ Options:
|
|
|
330
359
|
--json-path <path> Exact path for report.json
|
|
331
360
|
--html-path <path> Exact path for report.html
|
|
332
361
|
--days <n> Only include threads updated in the last N days (default: 30)
|
|
362
|
+
--preset <name> Scope preset: lite, standard, or deep (default: standard)
|
|
333
363
|
--limit <n> Target number of substantive threads to include (default: 200)
|
|
334
364
|
--preview <n> Number of threads to embed in the HTML report (default: 50)
|
|
335
365
|
--provider <name> Model provider: codex-cli or openai (default: codex-cli)
|
|
@@ -681,11 +711,22 @@ function inferQualityPreset(options) {
|
|
|
681
711
|
}
|
|
682
712
|
|
|
683
713
|
function applyScopePreset(options, preset) {
|
|
684
|
-
if (preset === '
|
|
714
|
+
if (preset === 'lite' || preset === 'conservative') {
|
|
715
|
+
return { ...options, days: 7, limit: 20, facetLimit: 8, preview: 10 }
|
|
716
|
+
}
|
|
685
717
|
if (preset === 'deep') return { ...options, limit: 400, facetLimit: 50 }
|
|
686
718
|
return { ...options, limit: 200, facetLimit: 50 }
|
|
687
719
|
}
|
|
688
720
|
|
|
721
|
+
function normalizeScopePreset(value) {
|
|
722
|
+
const preset = String(value || '').trim().toLowerCase()
|
|
723
|
+
if (preset === 'lite') return 'lite'
|
|
724
|
+
if (preset === 'conservative') return 'conservative'
|
|
725
|
+
if (preset === 'deep') return 'deep'
|
|
726
|
+
if (preset === 'standard' || !preset) return 'standard'
|
|
727
|
+
throw new Error(`Invalid preset "${value}". Expected lite, standard, or deep.`)
|
|
728
|
+
}
|
|
729
|
+
|
|
689
730
|
function applyQualityPreset(options, preset) {
|
|
690
731
|
if (preset === 'cheaper') {
|
|
691
732
|
return {
|
|
@@ -997,6 +1038,8 @@ export const __test = {
|
|
|
997
1038
|
applyScopePreset,
|
|
998
1039
|
applyQualityPreset,
|
|
999
1040
|
buildEquivalentCommand,
|
|
1041
|
+
parseArgs,
|
|
1042
|
+
normalizeScopePreset,
|
|
1000
1043
|
normalizeLang,
|
|
1001
1044
|
detectSystemLanguage,
|
|
1002
1045
|
}
|
package/lib/codex-data.js
CHANGED
|
@@ -8,8 +8,16 @@ import { promisify } from 'node:util'
|
|
|
8
8
|
const execFileAsync = promisify(execFile)
|
|
9
9
|
const MAX_TRANSCRIPT_CHARS = 30000
|
|
10
10
|
const USER_TRANSCRIPT_LIMIT = 500
|
|
11
|
-
const ASSISTANT_TRANSCRIPT_LIMIT =
|
|
11
|
+
const ASSISTANT_TRANSCRIPT_LIMIT = 300
|
|
12
12
|
const FAILURE_SUMMARY_LIMIT = 120
|
|
13
|
+
const MAX_TOOL_LINES_PER_BURST = 2
|
|
14
|
+
const LOW_SIGNAL_ASSISTANT_PATTERNS = [
|
|
15
|
+
/^i('| a)?m\s+(checking|looking|reviewing|reading|investigating)\b/i,
|
|
16
|
+
/^i('| wi)?ll\s+(check|look|review|inspect|read|investigate|start by)\b/i,
|
|
17
|
+
/^let me\b/i,
|
|
18
|
+
/^(checking|reviewing|reading|looking at|investigating)\b/i,
|
|
19
|
+
/^(next|now)\b/i,
|
|
20
|
+
]
|
|
13
21
|
const TASK_AGENT_TOOLS = new Set([
|
|
14
22
|
'spawn_agent',
|
|
15
23
|
'send_input',
|
|
@@ -422,7 +430,7 @@ export function summarizeThread(thread, events) {
|
|
|
422
430
|
averageResponseTimeSeconds: average(responseTimesSeconds),
|
|
423
431
|
activeHours,
|
|
424
432
|
userMessageTimestamps: sortedUserTs.map(tsMs => new Date(tsMs).toISOString()),
|
|
425
|
-
transcriptForAnalysis: clampTranscript(transcriptLines.join('\n')),
|
|
433
|
+
transcriptForAnalysis: clampTranscript(compactTranscriptLines(transcriptLines).join('\n')),
|
|
426
434
|
gitCommits,
|
|
427
435
|
gitPushes,
|
|
428
436
|
userInterruptions,
|
|
@@ -627,6 +635,38 @@ function clampTranscript(text) {
|
|
|
627
635
|
return `${clean.slice(0, MAX_TRANSCRIPT_CHARS)}\n[Transcript truncated]`
|
|
628
636
|
}
|
|
629
637
|
|
|
638
|
+
function compactTranscriptLines(lines) {
|
|
639
|
+
const compacted = []
|
|
640
|
+
let pendingToolBurst = []
|
|
641
|
+
|
|
642
|
+
const flushToolBurst = () => {
|
|
643
|
+
if (!pendingToolBurst.length) return
|
|
644
|
+
compacted.push(...pendingToolBurst.slice(0, MAX_TOOL_LINES_PER_BURST))
|
|
645
|
+
const remaining = pendingToolBurst.length - MAX_TOOL_LINES_PER_BURST
|
|
646
|
+
if (remaining > 0) {
|
|
647
|
+
compacted.push(`[Tool activity truncated: ${remaining} more tool calls]`)
|
|
648
|
+
}
|
|
649
|
+
pendingToolBurst = []
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
for (const line of lines) {
|
|
653
|
+
if (isLowSignalAssistantLine(line)) continue
|
|
654
|
+
|
|
655
|
+
if (isToolLine(line)) {
|
|
656
|
+
pendingToolBurst.push(line)
|
|
657
|
+
continue
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
flushToolBurst()
|
|
661
|
+
if (!compacted.length || compacted[compacted.length - 1] !== line) {
|
|
662
|
+
compacted.push(line)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
flushToolBurst()
|
|
667
|
+
return compacted
|
|
668
|
+
}
|
|
669
|
+
|
|
630
670
|
function appendTranscriptLine(lines, line, setLastLine, lastLine) {
|
|
631
671
|
const clean = String(line ?? '').trim()
|
|
632
672
|
if (!clean || clean === lastLine) return
|
|
@@ -634,6 +674,20 @@ function appendTranscriptLine(lines, line, setLastLine, lastLine) {
|
|
|
634
674
|
setLastLine(clean)
|
|
635
675
|
}
|
|
636
676
|
|
|
677
|
+
function isToolLine(line) {
|
|
678
|
+
const text = String(line || '')
|
|
679
|
+
return text.startsWith('[Tool: ')
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function isLowSignalAssistantLine(line) {
|
|
683
|
+
const text = String(line || '')
|
|
684
|
+
if (!text.startsWith('[Assistant] ')) return false
|
|
685
|
+
const body = text.slice('[Assistant] '.length).trim()
|
|
686
|
+
if (!body) return true
|
|
687
|
+
if (body.length > 60) return false
|
|
688
|
+
return LOW_SIGNAL_ASSISTANT_PATTERNS.some(pattern => pattern.test(body))
|
|
689
|
+
}
|
|
690
|
+
|
|
637
691
|
function sanitizeTranscriptText(value, limit) {
|
|
638
692
|
const text = String(value ?? '')
|
|
639
693
|
.replace(/<system_instruction>[\s\S]*?<\/system_instruction>/g, ' ')
|
package/lib/llm-insights.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
296
|
-
-
|
|
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,
|
|
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,10 @@ 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"
|
|
414
|
+
- Use AGENTS.md as the canonical repo instruction filename in examples; do not mention CLAUDE.md
|
|
404
415
|
- Write AGENTS.md additions as directly pasteable instruction lines, not commentary about instructions
|
|
405
416
|
- Make feature examples immediately usable; avoid placeholders like "insert your repo path here" unless unavoidable
|
|
406
417
|
- Make usage pattern suggestions sound like concrete next actions the user can try today, not abstract best practices`,
|
|
@@ -610,18 +621,31 @@ export async function estimateLlmAnalysisCost({ threadSummaries, options = {} })
|
|
|
610
621
|
let estimatedFacetOutputTokens = 0
|
|
611
622
|
let estimatedSummaryInputTokens = 0
|
|
612
623
|
let estimatedSummaryOutputTokens = 0
|
|
624
|
+
const facetSystemPrompt = buildFacetSystemPrompt(options.lang)
|
|
613
625
|
|
|
614
626
|
for (const job of uncachedFacetJobs) {
|
|
615
627
|
const transcript = String(job.thread.transcriptForAnalysis || '').trim()
|
|
616
628
|
const transcriptChars = transcript.length
|
|
617
629
|
if (!transcriptChars) {
|
|
618
|
-
estimatedFacetInputTokens +=
|
|
630
|
+
estimatedFacetInputTokens += estimateModelInputTokens({
|
|
631
|
+
provider: options.provider || DEFAULT_PROVIDER,
|
|
632
|
+
systemPrompt: facetSystemPrompt,
|
|
633
|
+
userPrompt: buildFacetExtractionPrompt(job.thread, `${job.thread.title || '(untitled)'}\n${job.thread.firstUserMessage || ''}`.trim(), options.lang),
|
|
634
|
+
schema: FACET_SCHEMA,
|
|
635
|
+
structured: true,
|
|
636
|
+
})
|
|
619
637
|
estimatedFacetOutputTokens += 350
|
|
620
638
|
continue
|
|
621
639
|
}
|
|
622
640
|
|
|
623
641
|
if (transcriptChars <= LONG_TRANSCRIPT_THRESHOLD) {
|
|
624
|
-
estimatedFacetInputTokens +=
|
|
642
|
+
estimatedFacetInputTokens += estimateModelInputTokens({
|
|
643
|
+
provider: options.provider || DEFAULT_PROVIDER,
|
|
644
|
+
systemPrompt: facetSystemPrompt,
|
|
645
|
+
userPrompt: buildFacetExtractionPrompt(job.thread, transcript, options.lang),
|
|
646
|
+
schema: FACET_SCHEMA,
|
|
647
|
+
structured: true,
|
|
648
|
+
})
|
|
625
649
|
estimatedFacetOutputTokens += 350
|
|
626
650
|
continue
|
|
627
651
|
}
|
|
@@ -629,20 +653,44 @@ export async function estimateLlmAnalysisCost({ threadSummaries, options = {} })
|
|
|
629
653
|
const chunks = chunkText(transcript, TRANSCRIPT_CHUNK_SIZE)
|
|
630
654
|
chunkSummaryCalls += chunks.length
|
|
631
655
|
for (const chunk of chunks) {
|
|
632
|
-
estimatedSummaryInputTokens +=
|
|
656
|
+
estimatedSummaryInputTokens += estimateModelInputTokens({
|
|
657
|
+
provider: options.provider || DEFAULT_PROVIDER,
|
|
658
|
+
systemPrompt: `${FACET_TRANSCRIPT_SUMMARY_DIRECTIVE}\n\nPreserve user goal, outcome, friction, command/tool issues, and what the assistant actually achieved.`,
|
|
659
|
+
userPrompt: `Chunk 1 of ${chunks.length}\n\n${chunk}`,
|
|
660
|
+
structured: false,
|
|
661
|
+
})
|
|
633
662
|
estimatedSummaryOutputTokens += 260
|
|
634
663
|
}
|
|
635
664
|
const combinedSummaryChars = chunks.length * 1100
|
|
636
665
|
if (combinedSummaryChars > LONG_TRANSCRIPT_THRESHOLD) {
|
|
637
666
|
combineSummaryCalls += 1
|
|
638
|
-
estimatedSummaryInputTokens +=
|
|
667
|
+
estimatedSummaryInputTokens += estimateModelInputTokens({
|
|
668
|
+
provider: options.provider || DEFAULT_PROVIDER,
|
|
669
|
+
systemPrompt:
|
|
670
|
+
'Combine these coding-session chunk summaries into one compact transcript summary. Keep only material signal for later facet extraction. Do not carry boilerplate, stack traces, or command details.',
|
|
671
|
+
userPrompt: makePlaceholderText(combinedSummaryChars, 'Chunk summaries'),
|
|
672
|
+
structured: false,
|
|
673
|
+
})
|
|
639
674
|
estimatedSummaryOutputTokens += 320
|
|
640
675
|
}
|
|
641
|
-
estimatedFacetInputTokens +=
|
|
676
|
+
estimatedFacetInputTokens += estimateModelInputTokens({
|
|
677
|
+
provider: options.provider || DEFAULT_PROVIDER,
|
|
678
|
+
systemPrompt: facetSystemPrompt,
|
|
679
|
+
userPrompt: buildFacetExtractionPrompt(
|
|
680
|
+
job.thread,
|
|
681
|
+
makePlaceholderText(
|
|
682
|
+
combinedSummaryChars > LONG_TRANSCRIPT_THRESHOLD ? 1200 : combinedSummaryChars,
|
|
683
|
+
'[Long transcript summarized before facet extraction]',
|
|
684
|
+
),
|
|
685
|
+
options.lang,
|
|
686
|
+
),
|
|
687
|
+
schema: FACET_SCHEMA,
|
|
688
|
+
structured: true,
|
|
689
|
+
})
|
|
642
690
|
estimatedFacetOutputTokens += 350
|
|
643
691
|
}
|
|
644
692
|
|
|
645
|
-
const estimatedSectionInputs = estimateSectionInputs(candidateThreads, facetJobs)
|
|
693
|
+
const estimatedSectionInputs = estimateSectionInputs(candidateThreads, facetJobs, options)
|
|
646
694
|
const fastSectionCalls = SECTION_DEFS.filter(section => section.modelTier === 'fast').length
|
|
647
695
|
const fullSectionCalls = SECTION_DEFS.filter(section => section.modelTier !== 'fast').length
|
|
648
696
|
const estimatedFastSectionInputTokens = SECTION_DEFS.filter(section => section.modelTier === 'fast')
|
|
@@ -814,61 +862,14 @@ async function getFacetForThread(thread, { cacheDir, model, provider, providerOp
|
|
|
814
862
|
provider,
|
|
815
863
|
providerOptions,
|
|
816
864
|
})
|
|
817
|
-
const prompt =
|
|
818
|
-
|
|
819
|
-
CRITICAL GUIDELINES:
|
|
820
|
-
1. goal_categories should count only what the user explicitly asked for.
|
|
821
|
-
2. user_satisfaction_counts should rely on explicit user signals or strong transcript evidence.
|
|
822
|
-
3. friction_counts should be specific: misunderstood_request, wrong_approach, buggy_code, user_rejected_action, excessive_changes, tool_failed, slow_or_verbose, user_unclear, external_issue.
|
|
823
|
-
4. If the session is mostly warmup, rehearsal, or cache-filling, use warmup_minimal as the only goal category.
|
|
824
|
-
5. If evidence is insufficient after transcript compression, use conservative values such as unclear_from_transcript rather than guessing.
|
|
825
|
-
6. Do not infer the user's goal from assistant or tool activity alone.
|
|
826
|
-
7. Do not count assistant-led exploration or extra implementation work unless the user clearly asked for it.
|
|
827
|
-
|
|
828
|
-
Allowed values:
|
|
829
|
-
- outcome: fully_achieved | mostly_achieved | partially_achieved | not_achieved | unclear_from_transcript
|
|
830
|
-
- assistant_helpfulness: unhelpful | slightly_helpful | moderately_helpful | very_helpful | essential
|
|
831
|
-
- session_type: single_task | multi_task | iterative_refinement | exploration | quick_question
|
|
832
|
-
- primary_success: none | fast_accurate_search | correct_code_edits | good_explanations | proactive_help | multi_file_changes | good_debugging
|
|
833
|
-
|
|
834
|
-
Language:
|
|
835
|
-
- Keep enum values and keys exactly as requested.
|
|
836
|
-
- Write free-text fields in ${describeLanguage(langFromProviderOptions(providerOptions))}.
|
|
837
|
-
|
|
838
|
-
Transcript:
|
|
839
|
-
${transcript}
|
|
840
|
-
|
|
841
|
-
Summary stats:
|
|
842
|
-
${JSON.stringify(
|
|
843
|
-
{
|
|
844
|
-
title: thread.title,
|
|
845
|
-
cwd: thread.cwd,
|
|
846
|
-
durationMinutes: thread.durationMinutes,
|
|
847
|
-
userMessages: thread.userMessages,
|
|
848
|
-
assistantMessages: thread.assistantMessages,
|
|
849
|
-
totalToolCalls: thread.totalToolCalls,
|
|
850
|
-
totalCommandFailures: thread.totalCommandFailures,
|
|
851
|
-
toolCounts: thread.toolCounts,
|
|
852
|
-
toolFailures: thread.toolFailures,
|
|
853
|
-
userInterruptions: thread.userInterruptions,
|
|
854
|
-
usesTaskAgent: thread.usesTaskAgent,
|
|
855
|
-
usesMcp: thread.usesMcp,
|
|
856
|
-
usesWebSearch: thread.usesWebSearch,
|
|
857
|
-
usesWebFetch: thread.usesWebFetch,
|
|
858
|
-
},
|
|
859
|
-
null,
|
|
860
|
-
2,
|
|
861
|
-
)}
|
|
862
|
-
|
|
863
|
-
RESPOND WITH ONLY A VALID JSON OBJECT matching the requested schema.`
|
|
865
|
+
const prompt = buildFacetExtractionPrompt(thread, transcript, langFromProviderOptions(providerOptions))
|
|
864
866
|
|
|
865
867
|
const rawFacet = await callStructuredModel({
|
|
866
868
|
provider,
|
|
867
869
|
model,
|
|
868
870
|
schemaName: 'codex_session_facet',
|
|
869
871
|
schema: FACET_SCHEMA,
|
|
870
|
-
systemPrompt:
|
|
871
|
-
`You extract structured coding-session facets from compressed transcripts. Use only transcript evidence. Be conservative when evidence is weak. Do not infer intent from tool activity alone. ${getStructuredLanguageInstruction(langFromProviderOptions(providerOptions))}`.trim(),
|
|
872
|
+
systemPrompt: buildFacetSystemPrompt(langFromProviderOptions(providerOptions)),
|
|
872
873
|
userPrompt: prompt,
|
|
873
874
|
options: {
|
|
874
875
|
...providerOptions,
|
|
@@ -989,6 +990,8 @@ function buildInsightContext(report, threadSummaries, facets) {
|
|
|
989
990
|
),
|
|
990
991
|
).slice(0, MAX_USER_INSTRUCTIONS)
|
|
991
992
|
|
|
993
|
+
const capabilityAdoption = summarizeCapabilityAdoption(report, threadSummaries, facets)
|
|
994
|
+
|
|
992
995
|
return {
|
|
993
996
|
metadata: {
|
|
994
997
|
generated_at: report.metadata.generatedAt,
|
|
@@ -1025,6 +1028,7 @@ function buildInsightContext(report, threadSummaries, facets) {
|
|
|
1025
1028
|
friction,
|
|
1026
1029
|
success,
|
|
1027
1030
|
},
|
|
1031
|
+
capability_adoption: capabilityAdoption,
|
|
1028
1032
|
session_summaries: sortedFacets.slice(0, MAX_CONTEXT_FACETS).map(facet => ({
|
|
1029
1033
|
thread_id: facet.threadId,
|
|
1030
1034
|
title: truncateForContext(facet.title, 80),
|
|
@@ -1051,6 +1055,69 @@ function buildInsightContext(report, threadSummaries, facets) {
|
|
|
1051
1055
|
}
|
|
1052
1056
|
}
|
|
1053
1057
|
|
|
1058
|
+
function summarizeCapabilityAdoption(report, threadSummaries, facets) {
|
|
1059
|
+
const textByThread = new Map()
|
|
1060
|
+
for (const thread of threadSummaries) {
|
|
1061
|
+
textByThread.set(
|
|
1062
|
+
thread.id,
|
|
1063
|
+
[thread.title, thread.firstUserMessage]
|
|
1064
|
+
.map(value => String(value || ''))
|
|
1065
|
+
.join('\n')
|
|
1066
|
+
.toLowerCase(),
|
|
1067
|
+
)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
for (const facet of facets) {
|
|
1071
|
+
const existing = textByThread.get(facet.threadId) || ''
|
|
1072
|
+
const facetText = [
|
|
1073
|
+
facet.underlying_goal,
|
|
1074
|
+
facet.brief_summary,
|
|
1075
|
+
...(facet.user_instructions || []),
|
|
1076
|
+
]
|
|
1077
|
+
.map(value => String(value || ''))
|
|
1078
|
+
.join('\n')
|
|
1079
|
+
.toLowerCase()
|
|
1080
|
+
textByThread.set(facet.threadId, `${existing}\n${facetText}`.trim())
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const detectMentionedThreads = regex => {
|
|
1084
|
+
let count = 0
|
|
1085
|
+
for (const text of textByThread.values()) {
|
|
1086
|
+
if (regex.test(text)) count += 1
|
|
1087
|
+
}
|
|
1088
|
+
return count
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const totalThreads = Math.max(1, Number(report.metadata.threadCount || threadSummaries.length || 0))
|
|
1092
|
+
const signals = {
|
|
1093
|
+
agents_md: detectMentionedThreads(/\bagents\.md\b/i),
|
|
1094
|
+
skills: detectMentionedThreads(/\bskills?\b/i),
|
|
1095
|
+
codex_exec: detectMentionedThreads(/\bcodex exec\b/i),
|
|
1096
|
+
subagents: Number(report.summary.sessionsUsingTaskAgent || 0),
|
|
1097
|
+
mcp_servers: Number(report.summary.sessionsUsingMcp || 0),
|
|
1098
|
+
web_search: Number(report.summary.sessionsUsingWebSearch || 0),
|
|
1099
|
+
web_fetch: Number(report.summary.sessionsUsingWebFetch || 0),
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return Object.fromEntries(
|
|
1103
|
+
Object.entries(signals).map(([key, count]) => [
|
|
1104
|
+
key,
|
|
1105
|
+
{
|
|
1106
|
+
count,
|
|
1107
|
+
status: classifyCapabilityAdoption(count, totalThreads),
|
|
1108
|
+
},
|
|
1109
|
+
]),
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function classifyCapabilityAdoption(count, totalThreads) {
|
|
1114
|
+
const share = Number(count || 0) / Math.max(1, Number(totalThreads || 0))
|
|
1115
|
+
if (count >= 10 || share >= 0.25) return 'strong'
|
|
1116
|
+
if (count >= 4 || share >= 0.1) return 'moderate'
|
|
1117
|
+
if (count > 0) return 'light'
|
|
1118
|
+
return 'none'
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1054
1121
|
function buildAtAGlancePrompt(context, insights) {
|
|
1055
1122
|
return `You are writing an "At a Glance" summary for a Codex usage insights report.
|
|
1056
1123
|
|
|
@@ -1078,6 +1145,124 @@ ${compactJson(compactInsightDigest(insights))}
|
|
|
1078
1145
|
RESPOND WITH ONLY A VALID JSON OBJECT matching the schema.`
|
|
1079
1146
|
}
|
|
1080
1147
|
|
|
1148
|
+
function buildFacetSystemPrompt(lang) {
|
|
1149
|
+
return `You extract structured coding-session facets from compressed transcripts. Use only transcript evidence. Be conservative when evidence is weak. Do not infer intent from tool activity alone. ${getStructuredLanguageInstruction(lang)}`.trim()
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function buildFacetExtractionPrompt(thread, transcript, lang) {
|
|
1153
|
+
return `Analyze this Codex coding session and extract structured facets.
|
|
1154
|
+
|
|
1155
|
+
CRITICAL GUIDELINES:
|
|
1156
|
+
1. goal_categories should count only what the user explicitly asked for.
|
|
1157
|
+
2. user_satisfaction_counts should rely on explicit user signals or strong transcript evidence.
|
|
1158
|
+
3. friction_counts should be specific: misunderstood_request, wrong_approach, buggy_code, user_rejected_action, excessive_changes, tool_failed, slow_or_verbose, user_unclear, external_issue.
|
|
1159
|
+
4. If the session is mostly warmup, rehearsal, or cache-filling, use warmup_minimal as the only goal category.
|
|
1160
|
+
5. If evidence is insufficient after transcript compression, use conservative values such as unclear_from_transcript rather than guessing.
|
|
1161
|
+
6. Do not infer the user's goal from assistant or tool activity alone.
|
|
1162
|
+
7. Do not count assistant-led exploration or extra implementation work unless the user clearly asked for it.
|
|
1163
|
+
|
|
1164
|
+
Allowed values:
|
|
1165
|
+
- outcome: fully_achieved | mostly_achieved | partially_achieved | not_achieved | unclear_from_transcript
|
|
1166
|
+
- assistant_helpfulness: unhelpful | slightly_helpful | moderately_helpful | very_helpful | essential
|
|
1167
|
+
- session_type: single_task | multi_task | iterative_refinement | exploration | quick_question
|
|
1168
|
+
- primary_success: none | fast_accurate_search | correct_code_edits | good_explanations | proactive_help | multi_file_changes | good_debugging
|
|
1169
|
+
|
|
1170
|
+
Language:
|
|
1171
|
+
- Keep enum values and keys exactly as requested.
|
|
1172
|
+
- Write free-text fields in ${describeLanguage(lang)}.
|
|
1173
|
+
|
|
1174
|
+
Transcript:
|
|
1175
|
+
${transcript}
|
|
1176
|
+
|
|
1177
|
+
Summary stats:
|
|
1178
|
+
${JSON.stringify(
|
|
1179
|
+
{
|
|
1180
|
+
title: thread.title,
|
|
1181
|
+
cwd: thread.cwd,
|
|
1182
|
+
durationMinutes: thread.durationMinutes,
|
|
1183
|
+
userMessages: thread.userMessages,
|
|
1184
|
+
assistantMessages: thread.assistantMessages,
|
|
1185
|
+
totalToolCalls: thread.totalToolCalls,
|
|
1186
|
+
totalCommandFailures: thread.totalCommandFailures,
|
|
1187
|
+
toolCounts: thread.toolCounts,
|
|
1188
|
+
toolFailures: thread.toolFailures,
|
|
1189
|
+
userInterruptions: thread.userInterruptions,
|
|
1190
|
+
usesTaskAgent: thread.usesTaskAgent,
|
|
1191
|
+
usesMcp: thread.usesMcp,
|
|
1192
|
+
usesWebSearch: thread.usesWebSearch,
|
|
1193
|
+
usesWebFetch: thread.usesWebFetch,
|
|
1194
|
+
},
|
|
1195
|
+
null,
|
|
1196
|
+
2,
|
|
1197
|
+
)}
|
|
1198
|
+
|
|
1199
|
+
RESPOND WITH ONLY A VALID JSON OBJECT matching the requested schema.`
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function buildEstimatedFacet(thread) {
|
|
1203
|
+
return {
|
|
1204
|
+
threadId: thread.id,
|
|
1205
|
+
title: thread.title,
|
|
1206
|
+
cwd: thread.cwd,
|
|
1207
|
+
updatedAt: thread.updatedAt,
|
|
1208
|
+
durationMinutes: thread.durationMinutes,
|
|
1209
|
+
userMessages: thread.userMessages,
|
|
1210
|
+
assistantMessages: thread.assistantMessages,
|
|
1211
|
+
totalToolCalls: thread.totalToolCalls,
|
|
1212
|
+
totalCommandFailures: thread.totalCommandFailures,
|
|
1213
|
+
underlying_goal: truncateForContext(thread.firstUserMessage || thread.title, 160),
|
|
1214
|
+
goal_categories: {},
|
|
1215
|
+
outcome: thread.totalCommandFailures > 0 ? 'partially_achieved' : 'unclear_from_transcript',
|
|
1216
|
+
user_satisfaction_counts: {},
|
|
1217
|
+
assistant_helpfulness: 'moderately_helpful',
|
|
1218
|
+
session_type: thread.userMessages > 2 ? 'iterative_refinement' : 'single_task',
|
|
1219
|
+
friction_counts: thread.totalCommandFailures > 0 ? { tool_failed: 1 } : {},
|
|
1220
|
+
friction_detail: 'none',
|
|
1221
|
+
primary_success: 'none',
|
|
1222
|
+
brief_summary: truncateForContext(thread.firstUserMessage || thread.title, 180),
|
|
1223
|
+
user_instructions: [],
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function buildEstimatedInsightsPlaceholder(context) {
|
|
1228
|
+
return {
|
|
1229
|
+
project_areas: {
|
|
1230
|
+
areas: (context.session_summaries || []).slice(0, 3).map((item, index) => ({
|
|
1231
|
+
name: item.project || item.title || `Workstream ${index + 1}`,
|
|
1232
|
+
session_count: 1,
|
|
1233
|
+
description: truncateForContext(item.summary || item.goal || '', 120),
|
|
1234
|
+
})),
|
|
1235
|
+
},
|
|
1236
|
+
interaction_style: {
|
|
1237
|
+
key_pattern: 'Tight scope before execution',
|
|
1238
|
+
narrative: truncateForContext('You first align scope and constraints, then execute and verify against explicit acceptance bars.', 180),
|
|
1239
|
+
},
|
|
1240
|
+
what_works: {
|
|
1241
|
+
impressive_workflows: [
|
|
1242
|
+
{ title: 'Scope first', description: 'You repeatedly tighten scope before execution.' },
|
|
1243
|
+
{ title: 'Verification loop', description: 'You use evidence to confirm changes before closing.' },
|
|
1244
|
+
],
|
|
1245
|
+
},
|
|
1246
|
+
friction_analysis: {
|
|
1247
|
+
categories: [
|
|
1248
|
+
{ category: 'Direction drift', description: 'Some sessions need scope tightening.', examples: ['Scope had to be pulled back.'] },
|
|
1249
|
+
],
|
|
1250
|
+
},
|
|
1251
|
+
suggestions: {
|
|
1252
|
+
features_to_try: [{ feature: 'AGENTS.md' }],
|
|
1253
|
+
usage_patterns: [{ title: 'Split review and execution' }],
|
|
1254
|
+
agents_md_additions: [{ addition: 'Read existing docs before editing.' }],
|
|
1255
|
+
},
|
|
1256
|
+
on_the_horizon: {
|
|
1257
|
+
opportunities: [{ title: 'Longer workflows', how_to_try: 'Add staged execution.' }],
|
|
1258
|
+
},
|
|
1259
|
+
fun_ending: {
|
|
1260
|
+
headline: 'Memorable moment',
|
|
1261
|
+
detail: 'A compact placeholder for estimate sizing.',
|
|
1262
|
+
},
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1081
1266
|
/**
|
|
1082
1267
|
* @param {InsightRunOptions} options
|
|
1083
1268
|
* @param {string} provider
|
|
@@ -1096,71 +1281,36 @@ function buildProviderOptions(options, provider, onUsage) {
|
|
|
1096
1281
|
}
|
|
1097
1282
|
}
|
|
1098
1283
|
|
|
1099
|
-
function estimateSectionInputs(candidateThreads, facetJobs) {
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
total_user_messages: candidateThreads.reduce((sum, thread) => sum + Number(thread.userMessages || 0), 0),
|
|
1106
|
-
total_tool_calls: candidateThreads.reduce((sum, thread) => sum + Number(thread.totalToolCalls || 0), 0),
|
|
1107
|
-
total_failures: candidateThreads.reduce((sum, thread) => sum + Number(thread.totalCommandFailures || 0), 0),
|
|
1108
|
-
},
|
|
1109
|
-
charts: {
|
|
1110
|
-
projects: candidateThreads.slice(0, 6).map(thread => compactProjectPath(thread.cwd)),
|
|
1111
|
-
models: Array.from(new Set(candidateThreads.map(thread => thread.model).filter(Boolean))).slice(0, 4),
|
|
1112
|
-
tools: [],
|
|
1113
|
-
},
|
|
1114
|
-
aggregate_facets: {
|
|
1115
|
-
sessions_with_facets: facetJobs.length,
|
|
1116
|
-
},
|
|
1117
|
-
session_summaries: facetJobs.slice(0, MAX_CONTEXT_FACETS).map(job => {
|
|
1118
|
-
if (job.cachedFacet) {
|
|
1119
|
-
return {
|
|
1120
|
-
title: truncateForContext(job.cachedFacet.title, 80),
|
|
1121
|
-
project: compactProjectPath(job.cachedFacet.cwd),
|
|
1122
|
-
goal: truncateForContext(job.cachedFacet.underlying_goal, 120),
|
|
1123
|
-
outcome: job.cachedFacet.outcome,
|
|
1124
|
-
primary_success: job.cachedFacet.primary_success,
|
|
1125
|
-
summary: truncateForContext(job.cachedFacet.brief_summary, 160),
|
|
1126
|
-
friction: compactCountObject(job.cachedFacet.friction_counts, 2),
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
return {
|
|
1130
|
-
title: truncateForContext(job.thread.title, 80),
|
|
1131
|
-
project: compactProjectPath(job.thread.cwd),
|
|
1132
|
-
goal: truncateForContext(job.thread.firstUserMessage, 120),
|
|
1133
|
-
outcome: 'unknown',
|
|
1134
|
-
primary_success: 'unknown',
|
|
1135
|
-
summary: truncateForContext(job.thread.firstUserMessage, 160),
|
|
1136
|
-
friction: {},
|
|
1137
|
-
}
|
|
1138
|
-
}),
|
|
1139
|
-
recent_threads: candidateThreads.slice(0, MAX_RECENT_THREADS).map(thread => ({
|
|
1140
|
-
title: truncateForContext(thread.title, 80),
|
|
1141
|
-
project: compactProjectPath(thread.cwd),
|
|
1142
|
-
duration_minutes: thread.durationMinutes,
|
|
1143
|
-
user_messages: thread.userMessages,
|
|
1144
|
-
tool_calls: thread.totalToolCalls,
|
|
1145
|
-
files_modified: thread.filesModified,
|
|
1146
|
-
})),
|
|
1147
|
-
friction_details: [],
|
|
1148
|
-
user_instructions: [],
|
|
1149
|
-
}
|
|
1150
|
-
const sectionKinds = [
|
|
1151
|
-
'project_areas',
|
|
1152
|
-
'interaction_style',
|
|
1153
|
-
'what_works',
|
|
1154
|
-
'friction_analysis',
|
|
1155
|
-
'suggestions',
|
|
1156
|
-
'on_the_horizon',
|
|
1157
|
-
'fun_ending',
|
|
1158
|
-
'at_a_glance',
|
|
1159
|
-
]
|
|
1284
|
+
function estimateSectionInputs(candidateThreads, facetJobs, options = {}) {
|
|
1285
|
+
const provider = options.provider || DEFAULT_PROVIDER
|
|
1286
|
+
const lang = options.lang || 'en'
|
|
1287
|
+
const estimatedFacets = facetJobs.map(job => job.cachedFacet || buildEstimatedFacet(job.thread))
|
|
1288
|
+
const report = buildReport(candidateThreads, { facets: estimatedFacets })
|
|
1289
|
+
const context = buildInsightContext(report, candidateThreads, estimatedFacets)
|
|
1160
1290
|
const estimated = {}
|
|
1161
|
-
|
|
1162
|
-
|
|
1291
|
+
|
|
1292
|
+
for (const section of SECTION_DEFS) {
|
|
1293
|
+
const systemPrompt = `${SECTION_SYSTEM_PROMPT} ${getNarrativeLanguageInstruction(lang)}`.trim()
|
|
1294
|
+
const sectionContext = buildSectionContext(context, section.contextKind)
|
|
1295
|
+
const userPrompt = `${section.prompt}\n\n${getNarrativeLanguageInstruction(lang)}\n\nDATA:\n${compactJson(sectionContext)}`
|
|
1296
|
+
estimated[section.contextKind] = estimateModelInputTokens({
|
|
1297
|
+
provider,
|
|
1298
|
+
systemPrompt,
|
|
1299
|
+
userPrompt,
|
|
1300
|
+
schema: section.schema,
|
|
1301
|
+
structured: true,
|
|
1302
|
+
})
|
|
1163
1303
|
}
|
|
1304
|
+
|
|
1305
|
+
const placeholderInsights = buildEstimatedInsightsPlaceholder(context)
|
|
1306
|
+
estimated.at_a_glance = estimateModelInputTokens({
|
|
1307
|
+
provider,
|
|
1308
|
+
systemPrompt: `${AT_A_GLANCE_SYSTEM_PROMPT} ${getNarrativeLanguageInstruction(lang)}`.trim(),
|
|
1309
|
+
userPrompt: buildAtAGlancePrompt(buildSectionContext(context, 'at_a_glance'), placeholderInsights),
|
|
1310
|
+
schema: AT_A_GLANCE_SCHEMA,
|
|
1311
|
+
structured: true,
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1164
1314
|
return estimated
|
|
1165
1315
|
}
|
|
1166
1316
|
|
|
@@ -1321,6 +1471,36 @@ function estimateTokensFromChars(chars) {
|
|
|
1321
1471
|
return Math.ceil(Number(chars || 0) / 4)
|
|
1322
1472
|
}
|
|
1323
1473
|
|
|
1474
|
+
function estimateModelInputTokens({ provider, systemPrompt, userPrompt, schema = null, structured }) {
|
|
1475
|
+
const promptText =
|
|
1476
|
+
provider === 'codex-cli'
|
|
1477
|
+
? structured
|
|
1478
|
+
? buildStructuredEstimatePrompt(systemPrompt, userPrompt, schema)
|
|
1479
|
+
: buildPlainEstimatePrompt(systemPrompt, userPrompt)
|
|
1480
|
+
: buildApiEstimatePrompt(systemPrompt, userPrompt, schema, structured)
|
|
1481
|
+
return estimateTokensFromChars(promptText.length)
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function buildPlainEstimatePrompt(systemPrompt, userPrompt) {
|
|
1485
|
+
return `${String(systemPrompt || '').trim()}\n\n${String(userPrompt || '').trim()}`
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function buildStructuredEstimatePrompt(systemPrompt, userPrompt, schema) {
|
|
1489
|
+
return `${buildPlainEstimatePrompt(systemPrompt, userPrompt)}\n\nRESPOND WITH ONLY A VALID JSON OBJECT matching this schema:\n${JSON.stringify(schema, null, 2)}`
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function buildApiEstimatePrompt(systemPrompt, userPrompt, schema, structured) {
|
|
1493
|
+
if (!structured) return buildPlainEstimatePrompt(systemPrompt, userPrompt)
|
|
1494
|
+
return `${buildPlainEstimatePrompt(systemPrompt, userPrompt)}\n\nJSON schema:\n${JSON.stringify(schema, null, 2)}`
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function makePlaceholderText(length, prefix = '') {
|
|
1498
|
+
const target = Math.max(0, Number(length || 0))
|
|
1499
|
+
const seed = prefix ? `${prefix}\n` : ''
|
|
1500
|
+
if (seed.length >= target) return seed.slice(0, target)
|
|
1501
|
+
return `${seed}${'x'.repeat(Math.max(0, target - seed.length))}`
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1324
1504
|
function getNarrativeLanguageInstruction(lang) {
|
|
1325
1505
|
if (lang === 'zh-CN') {
|
|
1326
1506
|
return 'Write all free-text narrative fields in Simplified Chinese.'
|
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),
|
|
@@ -159,6 +178,7 @@ export async function writeReportFiles(report, options) {
|
|
|
159
178
|
|
|
160
179
|
export function renderTerminalSummary(report) {
|
|
161
180
|
const text = getReportText(report.metadata.language)
|
|
181
|
+
const estimateComparison = buildEstimateComparison(report)
|
|
162
182
|
const lines = []
|
|
163
183
|
lines.push(text.reportTitle)
|
|
164
184
|
lines.push(
|
|
@@ -171,18 +191,23 @@ export function renderTerminalSummary(report) {
|
|
|
171
191
|
lines.push(
|
|
172
192
|
` ${text.inputLabel}=${formatMillionTokens(report.analysisUsage.inputTokens)} | ${text.cachedLabel}=${formatMillionTokens(report.analysisUsage.cachedInputTokens)} | ${text.outputLabel}=${formatMillionTokens(report.analysisUsage.outputTokens)}`,
|
|
173
193
|
)
|
|
194
|
+
if (estimateComparison) {
|
|
195
|
+
lines.push(
|
|
196
|
+
` ${text.estimateVsActualLabel}: ${formatMillionTokens(estimateComparison.estimatedTotalTokens)} -> ${formatMillionTokens(estimateComparison.actualTotalTokens)} ${text.actualFreshSuffix} (${formatSignedMillionTokens(estimateComparison.deltaTokens)}, ${formatSignedPercent(estimateComparison.deltaPercent)})`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
174
199
|
}
|
|
175
200
|
if (report.metadata.dateRange.start && report.metadata.dateRange.end) {
|
|
176
201
|
lines.push(`${report.metadata.dateRange.start} -> ${report.metadata.dateRange.end}`)
|
|
177
202
|
}
|
|
178
203
|
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
204
|
lines.push(`${text.topProjects}:`)
|
|
185
205
|
for (const item of report.charts.projects.slice(0, 5)) {
|
|
206
|
+
lines.push(` ${formatProjectLabel(item.label)}: ${item.value}`)
|
|
207
|
+
}
|
|
208
|
+
lines.push('')
|
|
209
|
+
lines.push(`${text.modelMix}:`)
|
|
210
|
+
for (const item of report.charts.models.slice(0, 5)) {
|
|
186
211
|
lines.push(` ${item.label}: ${item.value}`)
|
|
187
212
|
}
|
|
188
213
|
return lines.join('\n')
|
|
@@ -193,9 +218,15 @@ function renderHtmlReport(report) {
|
|
|
193
218
|
const insights = report.insights
|
|
194
219
|
if (insights && !insights.__lang) insights.__lang = report.metadata.language
|
|
195
220
|
const analysisUsage = report.analysisUsage || null
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
221
|
+
const topProjects = renderBarList(report.charts.projects, { formatLabel: formatProjectLabel })
|
|
222
|
+
const modelMix = renderBarList(report.charts.models)
|
|
223
|
+
const sessionTypes = renderBarList(report.charts.sessionTypes, {
|
|
224
|
+
formatLabel: value => formatSessionTypeLabel(value, report.metadata.language),
|
|
225
|
+
})
|
|
226
|
+
const outcomes = renderBarList(report.charts.outcomes, {
|
|
227
|
+
formatLabel: value => formatOutcomeLabel(value, report.metadata.language),
|
|
228
|
+
})
|
|
229
|
+
const capabilitySignals = renderBarList(report.charts.capabilities)
|
|
199
230
|
const toolFailures = renderBarList(report.charts.toolFailures)
|
|
200
231
|
const toolErrorCategories = renderBarList(report.charts.toolErrorCategories)
|
|
201
232
|
const activeHours = renderHourHistogram(report.charts.activeHours)
|
|
@@ -639,7 +670,7 @@ function renderHtmlReport(report) {
|
|
|
639
670
|
<section class="hero">
|
|
640
671
|
<span class="eyebrow">${escapeHtml(text.eyebrow)}</span>
|
|
641
672
|
<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>
|
|
673
|
+
<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
674
|
<div class="summary-grid">
|
|
644
675
|
${renderStat(text.userMessages, formatNumber(report.summary.totalUserMessages))}
|
|
645
676
|
${renderStat(text.toolCalls, formatNumber(report.summary.totalToolCalls))}
|
|
@@ -672,17 +703,25 @@ function renderHtmlReport(report) {
|
|
|
672
703
|
${renderOnTheHorizon(insights)}
|
|
673
704
|
</div>
|
|
674
705
|
<aside class="side-column">
|
|
675
|
-
<section class="chart-panel">
|
|
676
|
-
<h2>${escapeHtml(text.topTools)}</h2>
|
|
677
|
-
${topTools}
|
|
678
|
-
</section>
|
|
679
706
|
<section class="chart-panel">
|
|
680
707
|
<h2>${escapeHtml(text.topProjects)}</h2>
|
|
681
708
|
${topProjects}
|
|
682
709
|
</section>
|
|
683
710
|
<section class="chart-panel">
|
|
684
|
-
<h2>${escapeHtml(text.
|
|
685
|
-
${
|
|
711
|
+
<h2>${escapeHtml(text.modelMix)}</h2>
|
|
712
|
+
${modelMix}
|
|
713
|
+
</section>
|
|
714
|
+
<section class="chart-panel">
|
|
715
|
+
<h2>${escapeHtml(text.sessionTypes)}</h2>
|
|
716
|
+
${sessionTypes}
|
|
717
|
+
</section>
|
|
718
|
+
<section class="chart-panel">
|
|
719
|
+
<h2>${escapeHtml(text.outcomes)}</h2>
|
|
720
|
+
${outcomes}
|
|
721
|
+
</section>
|
|
722
|
+
<section class="chart-panel">
|
|
723
|
+
<h2>${escapeHtml(text.capabilitySignals)}</h2>
|
|
724
|
+
${capabilitySignals}
|
|
686
725
|
</section>
|
|
687
726
|
<section class="chart-panel">
|
|
688
727
|
<h2>${escapeHtml(text.failureHotspots)}</h2>
|
|
@@ -764,7 +803,7 @@ function renderProjectAreas(insights) {
|
|
|
764
803
|
<div class="project-area">
|
|
765
804
|
<div class="area-header">
|
|
766
805
|
<span class="area-name">${escapeHtml(area.name)}</span>
|
|
767
|
-
<span class="area-count"
|
|
806
|
+
<span class="area-count">${escapeHtml(text.workstreamBadge)}</span>
|
|
768
807
|
</div>
|
|
769
808
|
<div class="area-desc">${escapeHtml(area.description)}</div>
|
|
770
809
|
</div>
|
|
@@ -846,9 +885,9 @@ function renderSuggestions(insights) {
|
|
|
846
885
|
const suggestions = insights.suggestions
|
|
847
886
|
if (!suggestions) return ''
|
|
848
887
|
const text = getReportText(insights.__lang)
|
|
849
|
-
const agentItems = suggestions.agents_md_additions || []
|
|
850
|
-
const featureItems = suggestions.features_to_try || []
|
|
851
|
-
const patternItems = suggestions.usage_patterns || []
|
|
888
|
+
const agentItems = (suggestions.agents_md_additions || []).map(normalizeSuggestionTextItem)
|
|
889
|
+
const featureItems = (suggestions.features_to_try || []).map(normalizeSuggestionTextItem)
|
|
890
|
+
const patternItems = (suggestions.usage_patterns || []).map(normalizeSuggestionTextItem)
|
|
852
891
|
|
|
853
892
|
return `
|
|
854
893
|
<section class="panel">
|
|
@@ -966,6 +1005,7 @@ function renderReportMeta(report, context = {}) {
|
|
|
966
1005
|
const text = getReportText(report.metadata.language)
|
|
967
1006
|
const usage = report.analysisUsage
|
|
968
1007
|
const hasUsage = Boolean(usage?.totalTokens)
|
|
1008
|
+
const estimateComparison = buildEstimateComparison(report)
|
|
969
1009
|
const analysisByStage = context.analysisByStage || ''
|
|
970
1010
|
const analysisByModel = context.analysisByModel || ''
|
|
971
1011
|
|
|
@@ -981,6 +1021,7 @@ function renderReportMeta(report, context = {}) {
|
|
|
981
1021
|
${hasUsage ? renderStat(text.analysisTokens, formatMillionTokens(usage.totalTokens)) : ''}
|
|
982
1022
|
${hasUsage ? renderStat(text.modelCalls, formatNumber(usage.calls)) : ''}
|
|
983
1023
|
${hasUsage ? renderStat(text.cachedInput, formatMillionTokens(usage.cachedInputTokens)) : ''}
|
|
1024
|
+
${estimateComparison ? renderStat(text.estimateDelta, `${formatSignedMillionTokens(estimateComparison.deltaTokens)} (${formatSignedPercent(estimateComparison.deltaPercent)})`) : ''}
|
|
984
1025
|
${renderStat(text.historicalSessionTokens, formatNumber(report.summary.totalTokens))}
|
|
985
1026
|
</div>
|
|
986
1027
|
${
|
|
@@ -1006,6 +1047,14 @@ function renderReportMeta(report, context = {}) {
|
|
|
1006
1047
|
})}
|
|
1007
1048
|
<p class="meta">${escapeHtml(text.freshInput)}: ${formatMillionTokens(freshInputTokens)}. ${escapeHtml(text.analysisCostFootnote)}</p>
|
|
1008
1049
|
</div>
|
|
1050
|
+
${
|
|
1051
|
+
estimateComparison
|
|
1052
|
+
? `<div class="usage-breakdown">
|
|
1053
|
+
<h3>${escapeHtml(text.estimateVsActualHeading)}</h3>
|
|
1054
|
+
${renderEstimateComparisonCard(estimateComparison, text)}
|
|
1055
|
+
</div>`
|
|
1056
|
+
: ''
|
|
1057
|
+
}
|
|
1009
1058
|
</div>`
|
|
1010
1059
|
: ''
|
|
1011
1060
|
}
|
|
@@ -1050,19 +1099,43 @@ function renderUsageCard(item) {
|
|
|
1050
1099
|
`
|
|
1051
1100
|
}
|
|
1052
1101
|
|
|
1102
|
+
function renderEstimateComparisonCard(comparison, text) {
|
|
1103
|
+
return `
|
|
1104
|
+
<div class="usage-card">
|
|
1105
|
+
<div class="topline">
|
|
1106
|
+
<h3>${escapeHtml(text.estimateVsActualLabel)}</h3>
|
|
1107
|
+
<span class="pill">${escapeHtml(comparison.verdictLabel)}</span>
|
|
1108
|
+
</div>
|
|
1109
|
+
<div class="meta-row">
|
|
1110
|
+
<span>${escapeHtml(text.estimatedLabel)}: ${formatMillionTokens(comparison.estimatedTotalTokens)}</span>
|
|
1111
|
+
<span>${escapeHtml(text.actualFreshLabel)}: ${formatMillionTokens(comparison.actualTotalTokens)}</span>
|
|
1112
|
+
<span>${escapeHtml(text.estimateDelta)}: ${formatSignedMillionTokens(comparison.deltaTokens)} (${formatSignedPercent(comparison.deltaPercent)})</span>
|
|
1113
|
+
</div>
|
|
1114
|
+
<div class="token-row">
|
|
1115
|
+
<div class="token-box"><strong>${escapeHtml(text.estimatedLabel)}</strong><br>${formatMillionTokens(comparison.estimatedTotalTokens)}</div>
|
|
1116
|
+
<div class="token-box"><strong>${escapeHtml(text.actualFreshLabel)}</strong><br>${formatMillionTokens(comparison.actualTotalTokens)}</div>
|
|
1117
|
+
<div class="token-box"><strong>${escapeHtml(text.estimateRangeLabel)}</strong><br>${formatMillionTokens(comparison.lowEstimate)} -> ${formatMillionTokens(comparison.highEstimate)}</div>
|
|
1118
|
+
<div class="token-box"><strong>${escapeHtml(text.estimateDelta)}</strong><br>${formatSignedMillionTokens(comparison.deltaTokens)}</div>
|
|
1119
|
+
<div class="token-box"><strong>${escapeHtml(text.estimateError)}</strong><br>${formatSignedPercent(comparison.deltaPercent)}</div>
|
|
1120
|
+
</div>
|
|
1121
|
+
</div>
|
|
1122
|
+
`
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1053
1125
|
function renderStat(label, value) {
|
|
1054
1126
|
return `<div class="stat"><div class="value">${escapeHtml(String(value))}</div><div>${escapeHtml(label)}</div></div>`
|
|
1055
1127
|
}
|
|
1056
1128
|
|
|
1057
|
-
function renderBarList(items) {
|
|
1129
|
+
function renderBarList(items, options = {}) {
|
|
1058
1130
|
if (!items.length) return '<p class="meta">No data available.</p>'
|
|
1131
|
+
const formatLabel = options.formatLabel || (value => value)
|
|
1059
1132
|
const maxValue = Math.max(...items.map(item => item.value), 1)
|
|
1060
1133
|
return `<div class="bar-list">${items
|
|
1061
1134
|
.map(
|
|
1062
1135
|
item => `
|
|
1063
1136
|
<div class="bar-row">
|
|
1064
1137
|
<div class="bar-label">
|
|
1065
|
-
<span>${escapeHtml(item.label)}</span>
|
|
1138
|
+
<span>${escapeHtml(formatLabel(item.label))}</span>
|
|
1066
1139
|
<strong>${formatNumber(item.value)}</strong>
|
|
1067
1140
|
</div>
|
|
1068
1141
|
<div class="bar-track"><div class="bar-fill" style="width:${Math.max(6, (item.value / maxValue) * 100)}%"></div></div>
|
|
@@ -1117,6 +1190,20 @@ function renderCopyRow(value, text) {
|
|
|
1117
1190
|
`
|
|
1118
1191
|
}
|
|
1119
1192
|
|
|
1193
|
+
function normalizeSuggestionTextItem(item) {
|
|
1194
|
+
if (!item || typeof item !== 'object') return item
|
|
1195
|
+
return Object.fromEntries(
|
|
1196
|
+
Object.entries(item).map(([key, value]) => [key, normalizeSuggestionText(value)]),
|
|
1197
|
+
)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function normalizeSuggestionText(value) {
|
|
1201
|
+
if (typeof value !== 'string') return value
|
|
1202
|
+
return value
|
|
1203
|
+
.replaceAll('CLAUDE.md / AGENTS.md', 'AGENTS.md')
|
|
1204
|
+
.replaceAll('CLAUDE.md', 'AGENTS.md')
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1120
1207
|
function formatAgentInstruction(item) {
|
|
1121
1208
|
const placement = String(item?.prompt_scaffold || '').trim()
|
|
1122
1209
|
const addition = String(item?.addition || '').trim()
|
|
@@ -1133,6 +1220,74 @@ function topEntries(map, limit) {
|
|
|
1133
1220
|
.map(([label, value]) => ({ label, value }))
|
|
1134
1221
|
}
|
|
1135
1222
|
|
|
1223
|
+
function formatCodexHome(value) {
|
|
1224
|
+
return formatDisplayPath(value, { tailSegments: 2, preferHomeAlias: true, ellipsis: false })
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function formatProjectLabel(value) {
|
|
1228
|
+
return formatDisplayPath(value, { tailSegments: 2, preferHomeAlias: false, ellipsis: true })
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function formatSessionTypeLabel(value, lang) {
|
|
1232
|
+
const key = String(value || '')
|
|
1233
|
+
const zh = {
|
|
1234
|
+
iterative_refinement: '反复收敛',
|
|
1235
|
+
exploration: '探索调研',
|
|
1236
|
+
single_task: '单任务推进',
|
|
1237
|
+
multi_task: '多任务并行',
|
|
1238
|
+
quick_question: '快速提问',
|
|
1239
|
+
}
|
|
1240
|
+
const en = {
|
|
1241
|
+
iterative_refinement: 'Iterative refinement',
|
|
1242
|
+
exploration: 'Exploration',
|
|
1243
|
+
single_task: 'Single-task execution',
|
|
1244
|
+
multi_task: 'Multi-task coordination',
|
|
1245
|
+
quick_question: 'Quick question',
|
|
1246
|
+
}
|
|
1247
|
+
return lang === 'zh-CN' ? zh[key] || key : en[key] || key
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function formatOutcomeLabel(value, lang) {
|
|
1251
|
+
const key = String(value || '')
|
|
1252
|
+
const zh = {
|
|
1253
|
+
fully_achieved: '完全达成',
|
|
1254
|
+
mostly_achieved: '基本达成',
|
|
1255
|
+
partially_achieved: '部分达成',
|
|
1256
|
+
not_achieved: '未达成',
|
|
1257
|
+
unclear_from_transcript: '从记录中无法判断',
|
|
1258
|
+
}
|
|
1259
|
+
const en = {
|
|
1260
|
+
fully_achieved: 'Fully achieved',
|
|
1261
|
+
mostly_achieved: 'Mostly achieved',
|
|
1262
|
+
partially_achieved: 'Partially achieved',
|
|
1263
|
+
not_achieved: 'Not achieved',
|
|
1264
|
+
unclear_from_transcript: 'Unclear from transcript',
|
|
1265
|
+
}
|
|
1266
|
+
return lang === 'zh-CN' ? zh[key] || key : en[key] || key
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function formatDisplayPath(value, options = {}) {
|
|
1270
|
+
const text = String(value || '').trim()
|
|
1271
|
+
if (!text) return '(unknown)'
|
|
1272
|
+
|
|
1273
|
+
const normalized = text.replace(/\\/g, '/')
|
|
1274
|
+
const home = os.homedir().replace(/\\/g, '/')
|
|
1275
|
+
if (options.preferHomeAlias !== false && normalized === home) return '~'
|
|
1276
|
+
if (options.preferHomeAlias !== false && normalized.startsWith(`${home}/`)) {
|
|
1277
|
+
return `~/${normalized.slice(home.length + 1)}`
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const parts = normalized.split('/').filter(Boolean)
|
|
1281
|
+
const tailSegments = Math.max(1, Number(options.tailSegments || 2))
|
|
1282
|
+
if (parts.length <= tailSegments) {
|
|
1283
|
+
return normalized.startsWith('/') ? `/${parts.join('/')}` : parts.join('/')
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const tail = parts.slice(-tailSegments).join('/')
|
|
1287
|
+
if (options.ellipsis === false) return tail
|
|
1288
|
+
return `…/${tail}`
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1136
1291
|
function buildHourSeries(hourMap) {
|
|
1137
1292
|
return Array.from({ length: 24 }, (_, hour) => ({
|
|
1138
1293
|
hour,
|
|
@@ -1165,6 +1320,12 @@ function formatMillionTokens(value) {
|
|
|
1165
1320
|
return `${round(number / 1_000)}K tokens`
|
|
1166
1321
|
}
|
|
1167
1322
|
|
|
1323
|
+
function formatSignedMillionTokens(value) {
|
|
1324
|
+
const number = Number(value || 0)
|
|
1325
|
+
const prefix = number > 0 ? '+' : number < 0 ? '-' : ''
|
|
1326
|
+
return `${prefix}${formatMillionTokens(Math.abs(number))}`
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1168
1329
|
function formatPercent(value, total) {
|
|
1169
1330
|
const numerator = Number(value || 0)
|
|
1170
1331
|
const denominator = Number(total || 0)
|
|
@@ -1172,6 +1333,12 @@ function formatPercent(value, total) {
|
|
|
1172
1333
|
return `${round((numerator / denominator) * 100)}%`
|
|
1173
1334
|
}
|
|
1174
1335
|
|
|
1336
|
+
function formatSignedPercent(value) {
|
|
1337
|
+
const number = Number(value || 0)
|
|
1338
|
+
const prefix = number > 0 ? '+' : number < 0 ? '-' : ''
|
|
1339
|
+
return `${prefix}${round(Math.abs(number))}%`
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1175
1342
|
function average(values) {
|
|
1176
1343
|
if (!values.length) return 0
|
|
1177
1344
|
return round(values.reduce((sum, value) => sum + value, 0) / values.length)
|
|
@@ -1226,6 +1393,41 @@ function escapeAttribute(value) {
|
|
|
1226
1393
|
return escapeHtml(String(value)).replaceAll("'", ''').replaceAll('\n', ' ')
|
|
1227
1394
|
}
|
|
1228
1395
|
|
|
1396
|
+
function buildEstimateComparison(report) {
|
|
1397
|
+
const estimate = report.analysisEstimate
|
|
1398
|
+
const usage = report.analysisUsage
|
|
1399
|
+
if (!estimate?.estimatedTotalTokens || !usage?.totalTokens) return null
|
|
1400
|
+
|
|
1401
|
+
const estimated = Number(estimate.estimatedTotalTokens || 0)
|
|
1402
|
+
const actualFresh =
|
|
1403
|
+
Math.max(0, Number(usage.inputTokens || 0) - Number(usage.cachedInputTokens || 0)) +
|
|
1404
|
+
Number(usage.outputTokens || 0)
|
|
1405
|
+
const delta = actualFresh - estimated
|
|
1406
|
+
const deltaPercent = estimated > 0 ? (delta / estimated) * 100 : 0
|
|
1407
|
+
|
|
1408
|
+
return {
|
|
1409
|
+
estimatedTotalTokens: estimated,
|
|
1410
|
+
actualTotalTokens: actualFresh,
|
|
1411
|
+
lowEstimate: Number(estimate.estimatedRange?.low || estimated),
|
|
1412
|
+
highEstimate: Number(estimate.estimatedRange?.high || estimated),
|
|
1413
|
+
deltaTokens: delta,
|
|
1414
|
+
deltaPercent,
|
|
1415
|
+
verdictLabel: classifyEstimateDelta(deltaPercent, report.metadata.language),
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function classifyEstimateDelta(deltaPercent, lang) {
|
|
1420
|
+
const abs = Math.abs(Number(deltaPercent || 0))
|
|
1421
|
+
if (lang === 'zh-CN') {
|
|
1422
|
+
if (abs <= 10) return '估算接近'
|
|
1423
|
+
if (deltaPercent > 0) return '实际偏高'
|
|
1424
|
+
return '实际偏低'
|
|
1425
|
+
}
|
|
1426
|
+
if (abs <= 10) return 'Close estimate'
|
|
1427
|
+
if (deltaPercent > 0) return 'Actual higher'
|
|
1428
|
+
return 'Actual lower'
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1229
1431
|
function getReportText(lang) {
|
|
1230
1432
|
if (lang === 'zh-CN') {
|
|
1231
1433
|
return {
|
|
@@ -1244,9 +1446,11 @@ function getReportText(lang) {
|
|
|
1244
1446
|
filesModified: '修改文件',
|
|
1245
1447
|
toolErrors: '工具错误',
|
|
1246
1448
|
avgResponse: '平均响应',
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1449
|
+
topProjects: '项目分布',
|
|
1450
|
+
modelMix: '模型分布',
|
|
1451
|
+
sessionTypes: '会话类型',
|
|
1452
|
+
outcomes: '结果分布',
|
|
1453
|
+
capabilitySignals: '能力信号',
|
|
1250
1454
|
failureHotspots: '失败热点',
|
|
1251
1455
|
errorCategories: '错误分类',
|
|
1252
1456
|
timeOfDay: '活跃时段',
|
|
@@ -1256,6 +1460,7 @@ function getReportText(lang) {
|
|
|
1256
1460
|
quickWins: '可以立刻尝试的优化',
|
|
1257
1461
|
ambitiousWorkflows: '值得尝试的更强工作流',
|
|
1258
1462
|
sessionsLabel: '次会话',
|
|
1463
|
+
workstreamBadge: '代表性工作流',
|
|
1259
1464
|
impressiveThingsLink: '做得好的地方',
|
|
1260
1465
|
whereThingsGoWrongLink: '容易出问题的地方',
|
|
1261
1466
|
whatYouWorkOn: '你主要在做什么',
|
|
@@ -1286,6 +1491,15 @@ function getReportText(lang) {
|
|
|
1286
1491
|
dateRange: '时间范围',
|
|
1287
1492
|
modelCalls: '模型调用',
|
|
1288
1493
|
cachedInput: '缓存输入',
|
|
1494
|
+
estimatedLabel: '预估',
|
|
1495
|
+
actualLabel: '实际',
|
|
1496
|
+
actualFreshLabel: '实际(不含缓存)',
|
|
1497
|
+
actualFreshSuffix: '(不含缓存)',
|
|
1498
|
+
estimateRangeLabel: '预估区间',
|
|
1499
|
+
estimateVsActualHeading: '预估与实际',
|
|
1500
|
+
estimateVsActualLabel: '预估 vs 实际',
|
|
1501
|
+
estimateDelta: '偏差',
|
|
1502
|
+
estimateError: '偏差比例',
|
|
1289
1503
|
historicalSessionTokens: '历史会话 Tokens',
|
|
1290
1504
|
analysisCostByStage: '按阶段拆分的分析成本',
|
|
1291
1505
|
analysisCostByModel: '按模型拆分的分析成本',
|
|
@@ -1321,9 +1535,11 @@ function getReportText(lang) {
|
|
|
1321
1535
|
filesModified: 'Files Modified',
|
|
1322
1536
|
toolErrors: 'Tool Errors',
|
|
1323
1537
|
avgResponse: 'Avg Response',
|
|
1324
|
-
topTools: 'Top Tools',
|
|
1325
1538
|
topProjects: 'Top Projects',
|
|
1326
|
-
|
|
1539
|
+
modelMix: 'Model Mix',
|
|
1540
|
+
sessionTypes: 'Session Types',
|
|
1541
|
+
outcomes: 'Outcomes',
|
|
1542
|
+
capabilitySignals: 'Capability Signals',
|
|
1327
1543
|
failureHotspots: 'Failure Hotspots',
|
|
1328
1544
|
errorCategories: 'Error Categories',
|
|
1329
1545
|
timeOfDay: 'Time of Day',
|
|
@@ -1333,6 +1549,7 @@ function getReportText(lang) {
|
|
|
1333
1549
|
quickWins: 'Quick wins to try',
|
|
1334
1550
|
ambitiousWorkflows: 'Ambitious workflows',
|
|
1335
1551
|
sessionsLabel: 'sessions',
|
|
1552
|
+
workstreamBadge: 'Representative workstream',
|
|
1336
1553
|
impressiveThingsLink: 'Impressive Things You Did',
|
|
1337
1554
|
whereThingsGoWrongLink: 'Where Things Go Wrong',
|
|
1338
1555
|
whatYouWorkOn: 'What You Work On',
|
|
@@ -1363,6 +1580,15 @@ function getReportText(lang) {
|
|
|
1363
1580
|
dateRange: 'Date Range',
|
|
1364
1581
|
modelCalls: 'Model Calls',
|
|
1365
1582
|
cachedInput: 'Cached Input',
|
|
1583
|
+
estimatedLabel: 'Estimated',
|
|
1584
|
+
actualLabel: 'Actual',
|
|
1585
|
+
actualFreshLabel: 'Actual (fresh)',
|
|
1586
|
+
actualFreshSuffix: '(fresh)',
|
|
1587
|
+
estimateRangeLabel: 'Estimate range',
|
|
1588
|
+
estimateVsActualHeading: 'Estimate vs Actual',
|
|
1589
|
+
estimateVsActualLabel: 'Estimate vs Actual',
|
|
1590
|
+
estimateDelta: 'Delta',
|
|
1591
|
+
estimateError: 'Error',
|
|
1366
1592
|
historicalSessionTokens: 'Historical Session Tokens',
|
|
1367
1593
|
analysisCostByStage: 'Analysis Cost by Stage',
|
|
1368
1594
|
analysisCostByModel: 'Analysis Cost by Model',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-session-insights",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Generate a report analyzing your Codex sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"report": "node ./bin/codex-insights.js report",
|
|
28
|
+
"report:lite": "node ./bin/codex-insights.js --preset lite --yes",
|
|
28
29
|
"generate:test-report": "node ./scripts/generate-test-report.mjs",
|
|
29
30
|
"test": "node --test",
|
|
30
31
|
"check": "node --check ./bin/codex-insights.js && node --check ./lib/cli.js && node --check ./lib/codex-data.js && node --check ./lib/report.js && node --check ./lib/llm-insights.js && node --check ./lib/model-provider.js && npm run typecheck",
|