codex-session-insights 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/bin/codex-insights.js +9 -0
- package/lib/cli.js +1002 -0
- package/lib/codex-data.js +640 -0
- package/lib/llm-insights.js +1486 -0
- package/lib/model-provider.js +589 -0
- package/lib/report.js +1383 -0
- package/lib/types.d.ts +87 -0
- package/package.json +47 -0
package/lib/cli.js
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { confirm, input, select } from '@inquirer/prompts'
|
|
4
|
+
import ora from 'ora'
|
|
5
|
+
import { collectThreadSummaries, resolveCodexHome } from './codex-data.js'
|
|
6
|
+
import { buildReport, renderTerminalSummary, writeReportFiles } from './report.js'
|
|
7
|
+
import { estimateLlmAnalysisCost, generateLlmInsights } from './llm-insights.js'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SCOPE_PRESET = 'standard'
|
|
10
|
+
const DEFAULT_QUALITY_PRESET = 'balanced'
|
|
11
|
+
|
|
12
|
+
export async function runCli(argv) {
|
|
13
|
+
const parsed = parseArgs(argv)
|
|
14
|
+
const progress = createProgressUi(parsed.options)
|
|
15
|
+
|
|
16
|
+
if (parsed.help) {
|
|
17
|
+
printHelp()
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const command = parsed.command ?? 'report'
|
|
22
|
+
if (command !== 'report') {
|
|
23
|
+
throw new Error(`Unsupported command "${command}". Only "report" is available in this build.`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const codexHome = resolveCodexHome(parsed.options.codexHome)
|
|
27
|
+
const outDir = path.resolve(parsed.options.outDir ?? path.join(codexHome, 'usage-data'))
|
|
28
|
+
let threadSummaries
|
|
29
|
+
let estimate
|
|
30
|
+
|
|
31
|
+
if (shouldUseInteractiveMode(parsed.options)) {
|
|
32
|
+
const wizardResult = await runInteractiveWizard({
|
|
33
|
+
options: parsed.options,
|
|
34
|
+
codexHome,
|
|
35
|
+
defaultOutDir: outDir,
|
|
36
|
+
progress,
|
|
37
|
+
})
|
|
38
|
+
if (!wizardResult) return
|
|
39
|
+
parsed.options = wizardResult.options
|
|
40
|
+
threadSummaries = wizardResult.threadSummaries
|
|
41
|
+
estimate = wizardResult.estimate
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sinceEpochSeconds =
|
|
45
|
+
parsed.options.days && parsed.options.days > 0
|
|
46
|
+
? Math.floor(Date.now() / 1000 - parsed.options.days * 24 * 60 * 60)
|
|
47
|
+
: null
|
|
48
|
+
const cacheDir = parsed.options.cacheDir
|
|
49
|
+
? path.resolve(parsed.options.cacheDir)
|
|
50
|
+
: undefined
|
|
51
|
+
|
|
52
|
+
if (!threadSummaries) {
|
|
53
|
+
progress.startStage(parsed.options, getUiText(parsed.options.lang).loadingIndex)
|
|
54
|
+
threadSummaries = await collectThreadSummaries({
|
|
55
|
+
codexHome,
|
|
56
|
+
sinceEpochSeconds,
|
|
57
|
+
limit: parsed.options.limit,
|
|
58
|
+
includeArchived: parsed.options.includeArchived,
|
|
59
|
+
includeSubagents: parsed.options.includeSubagents,
|
|
60
|
+
cacheDir,
|
|
61
|
+
})
|
|
62
|
+
progress.completeStage(parsed.options, getUiText(parsed.options.lang).loadingIndex)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!estimate) {
|
|
66
|
+
progress.startStage(parsed.options, getUiText(parsed.options.lang).estimating)
|
|
67
|
+
estimate = await estimateLlmAnalysisCost({
|
|
68
|
+
threadSummaries,
|
|
69
|
+
options: parsed.options,
|
|
70
|
+
})
|
|
71
|
+
progress.completeStage(parsed.options, getUiText(parsed.options.lang).estimating)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!parsed.options.stdoutJson) {
|
|
75
|
+
process.stdout.write(`${renderEstimateSummary(estimate, parsed.options.lang)}\n\n`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (parsed.options.estimateOnly) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
progress.startStage(parsed.options, getUiText(parsed.options.lang).generating)
|
|
83
|
+
const llmResult = await generateLlmInsights({
|
|
84
|
+
threadSummaries,
|
|
85
|
+
options: {
|
|
86
|
+
...parsed.options,
|
|
87
|
+
onProgress: event => progress.updateFromEvent(parsed.options, event),
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
progress.completeStage(parsed.options, getUiText(parsed.options.lang).generating)
|
|
91
|
+
|
|
92
|
+
const report = buildReport(llmResult.reportThreads, {
|
|
93
|
+
codexHome,
|
|
94
|
+
days: parsed.options.days,
|
|
95
|
+
lang: parsed.options.lang,
|
|
96
|
+
threadPreviewLimit: parsed.options.preview,
|
|
97
|
+
insightsOverride: llmResult.insights,
|
|
98
|
+
facets: llmResult.facets,
|
|
99
|
+
})
|
|
100
|
+
report.analysisMode = 'llm'
|
|
101
|
+
report.provider = parsed.options.provider
|
|
102
|
+
report.analysisUsage = llmResult.analysisUsage
|
|
103
|
+
|
|
104
|
+
progress.startStage(parsed.options, getUiText(parsed.options.lang).writingFiles)
|
|
105
|
+
const { jsonPath, htmlPath } = await writeReportFiles(report, {
|
|
106
|
+
outDir,
|
|
107
|
+
jsonPath: parsed.options.jsonPath ? path.resolve(parsed.options.jsonPath) : undefined,
|
|
108
|
+
htmlPath: parsed.options.htmlPath ? path.resolve(parsed.options.htmlPath) : undefined,
|
|
109
|
+
})
|
|
110
|
+
progress.completeStage(parsed.options, getUiText(parsed.options.lang).writingFiles)
|
|
111
|
+
|
|
112
|
+
if (parsed.options.stdoutJson) {
|
|
113
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
process.stdout.write(`${renderTerminalSummary(report)}\n\n`)
|
|
118
|
+
process.stdout.write(`JSON: ${jsonPath}\n`)
|
|
119
|
+
process.stdout.write(`HTML: ${htmlPath}\n`)
|
|
120
|
+
|
|
121
|
+
const shouldOpen = resolveShouldOpenReport(parsed.options)
|
|
122
|
+
if (shouldOpen) {
|
|
123
|
+
progress.startStage(parsed.options, getUiText(parsed.options.lang).openingBrowser)
|
|
124
|
+
const opened = await openReportInBrowser(htmlPath)
|
|
125
|
+
if (opened) {
|
|
126
|
+
progress.completeStage(parsed.options, getUiText(parsed.options.lang).openingBrowser)
|
|
127
|
+
process.stdout.write(`${getUiText(parsed.options.lang).openedInBrowser}\n`)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
progress.failStage(parsed.options, getUiText(parsed.options.lang).openingBrowser)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
process.stdout.write(`${getUiText(parsed.options.lang).openHint}: ${formatOpenHint(htmlPath)}\n`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseArgs(argv) {
|
|
137
|
+
let command = null
|
|
138
|
+
const options = {
|
|
139
|
+
codexHome: null,
|
|
140
|
+
outDir: null,
|
|
141
|
+
jsonPath: null,
|
|
142
|
+
htmlPath: null,
|
|
143
|
+
days: 30,
|
|
144
|
+
limit: 50,
|
|
145
|
+
preview: 50,
|
|
146
|
+
provider: 'codex-cli',
|
|
147
|
+
codexBin: null,
|
|
148
|
+
apiBase: null,
|
|
149
|
+
apiKey: null,
|
|
150
|
+
facetModel: null,
|
|
151
|
+
facetEffort: null,
|
|
152
|
+
fastSectionModel: null,
|
|
153
|
+
fastSectionEffort: null,
|
|
154
|
+
insightModel: null,
|
|
155
|
+
insightEffort: null,
|
|
156
|
+
cacheDir: null,
|
|
157
|
+
facetLimit: 20,
|
|
158
|
+
lang: detectSystemLanguage(),
|
|
159
|
+
includeArchived: false,
|
|
160
|
+
includeSubagents: false,
|
|
161
|
+
stdoutJson: false,
|
|
162
|
+
estimateOnly: false,
|
|
163
|
+
openReport: null,
|
|
164
|
+
yes: false,
|
|
165
|
+
nonInteractive: false,
|
|
166
|
+
}
|
|
167
|
+
let help = false
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
170
|
+
const arg = argv[i]
|
|
171
|
+
|
|
172
|
+
if (!arg.startsWith('-') && command === null) {
|
|
173
|
+
command = arg
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (arg === '-h' || arg === '--help') {
|
|
178
|
+
help = true
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if (arg === '--codex-home') {
|
|
182
|
+
options.codexHome = requireValue(argv, ++i, '--codex-home')
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
if (arg === '--out-dir') {
|
|
186
|
+
options.outDir = requireValue(argv, ++i, '--out-dir')
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
if (arg === '--json-path') {
|
|
190
|
+
options.jsonPath = requireValue(argv, ++i, '--json-path')
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (arg === '--html-path') {
|
|
194
|
+
options.htmlPath = requireValue(argv, ++i, '--html-path')
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
if (arg === '--days') {
|
|
198
|
+
options.days = toPositiveInt(requireValue(argv, ++i, '--days'), '--days')
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
if (arg === '--limit') {
|
|
202
|
+
options.limit = toPositiveInt(requireValue(argv, ++i, '--limit'), '--limit')
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
if (arg === '--preview') {
|
|
206
|
+
options.preview = toPositiveInt(requireValue(argv, ++i, '--preview'), '--preview')
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (arg === '--provider') {
|
|
210
|
+
options.provider = requireValue(argv, ++i, '--provider')
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
if (arg === '--codex-bin') {
|
|
214
|
+
options.codexBin = requireValue(argv, ++i, '--codex-bin')
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
if (arg === '--api-base') {
|
|
218
|
+
options.apiBase = requireValue(argv, ++i, '--api-base')
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (arg === '--api-key') {
|
|
222
|
+
options.apiKey = requireValue(argv, ++i, '--api-key')
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (arg === '--facet-model') {
|
|
226
|
+
options.facetModel = requireValue(argv, ++i, '--facet-model')
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
if (arg === '--facet-effort') {
|
|
230
|
+
options.facetEffort = requireValue(argv, ++i, '--facet-effort')
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
if (arg === '--fast-section-model') {
|
|
234
|
+
options.fastSectionModel = requireValue(argv, ++i, '--fast-section-model')
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
if (arg === '--fast-section-effort') {
|
|
238
|
+
options.fastSectionEffort = requireValue(argv, ++i, '--fast-section-effort')
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
if (arg === '--insight-model') {
|
|
242
|
+
options.insightModel = requireValue(argv, ++i, '--insight-model')
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (arg === '--insight-effort') {
|
|
246
|
+
options.insightEffort = requireValue(argv, ++i, '--insight-effort')
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
if (arg === '--cache-dir') {
|
|
250
|
+
options.cacheDir = requireValue(argv, ++i, '--cache-dir')
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
if (arg === '--lang') {
|
|
254
|
+
options.lang = normalizeLang(requireValue(argv, ++i, '--lang'))
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
if (arg === '--facet-limit') {
|
|
258
|
+
options.facetLimit = toPositiveInt(requireValue(argv, ++i, '--facet-limit'), '--facet-limit')
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
if (arg === '--include-archived') {
|
|
262
|
+
options.includeArchived = true
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (arg === '--include-subagents') {
|
|
266
|
+
options.includeSubagents = true
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
if (arg === '--stdout-json') {
|
|
270
|
+
options.stdoutJson = true
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (arg === '--estimate-only') {
|
|
274
|
+
options.estimateOnly = true
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
if (arg === '--open') {
|
|
278
|
+
options.openReport = true
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
if (arg === '--no-open') {
|
|
282
|
+
options.openReport = false
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
if (arg === '--yes') {
|
|
286
|
+
options.yes = true
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
if (arg === '--non-interactive') {
|
|
290
|
+
options.nonInteractive = true
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
throw new Error(`Unknown argument: ${arg}`)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!['codex-cli', 'openai'].includes(options.provider)) {
|
|
298
|
+
throw new Error(`Invalid provider "${options.provider}". Expected codex-cli or openai.`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return { command, options, help }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function requireValue(argv, index, flag) {
|
|
305
|
+
const value = argv[index]
|
|
306
|
+
if (!value) {
|
|
307
|
+
throw new Error(`${flag} requires a value`)
|
|
308
|
+
}
|
|
309
|
+
return value
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function toPositiveInt(value, flag) {
|
|
313
|
+
const number = Number.parseInt(value, 10)
|
|
314
|
+
if (!Number.isFinite(number) || number < 0) {
|
|
315
|
+
throw new Error(`${flag} must be a non-negative integer`)
|
|
316
|
+
}
|
|
317
|
+
return number
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function printHelp() {
|
|
321
|
+
process.stdout.write(`codex-session-insights
|
|
322
|
+
|
|
323
|
+
Usage:
|
|
324
|
+
codex-session-insights report [options]
|
|
325
|
+
codex-session-insights [options]
|
|
326
|
+
|
|
327
|
+
Options:
|
|
328
|
+
--codex-home <path> Override the Codex data directory (default: $CODEX_HOME or ~/.codex)
|
|
329
|
+
--out-dir <path> Directory for generated report files (default: ~/.codex/usage-data)
|
|
330
|
+
--json-path <path> Exact path for report.json
|
|
331
|
+
--html-path <path> Exact path for report.html
|
|
332
|
+
--days <n> Only include threads updated in the last N days (default: 30)
|
|
333
|
+
--limit <n> Max threads to scan from the thread registry (default: 50)
|
|
334
|
+
--preview <n> Number of threads to embed in the HTML report (default: 50)
|
|
335
|
+
--provider <name> Model provider: codex-cli or openai (default: codex-cli)
|
|
336
|
+
--codex-bin <path> Override the Codex CLI binary path for provider=codex-cli
|
|
337
|
+
--api-key <key> OpenAI API key override for provider=openai
|
|
338
|
+
--api-base <url> Responses API base URL override for provider=openai
|
|
339
|
+
--facet-model <name> Model for per-thread facet extraction
|
|
340
|
+
--facet-effort <level> Reasoning effort for facet extraction
|
|
341
|
+
--fast-section-model <name>
|
|
342
|
+
Model for lower-risk report sections
|
|
343
|
+
--fast-section-effort <level>
|
|
344
|
+
Reasoning effort for lower-risk sections
|
|
345
|
+
--insight-model <name> Model for final report generation
|
|
346
|
+
--insight-effort <level> Reasoning effort for higher-risk sections
|
|
347
|
+
--facet-limit <n> Max uncached substantive threads to analyze (default: 20)
|
|
348
|
+
--cache-dir <path> Cache directory for session-meta and facet caches
|
|
349
|
+
--lang <code> Report language: en or zh-CN (default: system language)
|
|
350
|
+
--include-archived Include archived threads
|
|
351
|
+
--include-subagents Include sub-agent threads spawned from parent threads
|
|
352
|
+
--estimate-only Print estimated analysis token usage and exit
|
|
353
|
+
--stdout-json Print the JSON report to stdout instead of a terminal summary
|
|
354
|
+
--open Force opening report.html in your browser after generation
|
|
355
|
+
--no-open Do not auto-open report.html after generation
|
|
356
|
+
--yes Run immediately without interactive confirmation
|
|
357
|
+
--non-interactive Disable TTY wizard mode
|
|
358
|
+
-h, --help Show this help
|
|
359
|
+
`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function renderEstimateSummary(estimate, lang = 'en') {
|
|
363
|
+
const ui = getUiText(lang)
|
|
364
|
+
const lines = []
|
|
365
|
+
lines.push(ui.analysisEstimateTitle)
|
|
366
|
+
lines.push(
|
|
367
|
+
`${formatMillionTokens(estimate.estimatedRange.low)} ${ui.toWord} ${formatMillionTokens(estimate.estimatedRange.high)} ${ui.likelyWord}`,
|
|
368
|
+
)
|
|
369
|
+
lines.push(
|
|
370
|
+
`${ui.plannedCallsLabel}=${formatInteger(estimate.estimatedCalls)} | ${ui.substantiveThreadsLabel}=${formatInteger(estimate.candidateThreads)} | ${ui.uncachedFacetsLabel}=${formatInteger(estimate.uncachedFacetThreads)} | ${ui.longTranscriptsLabel}=${formatInteger(estimate.longTranscriptThreads)}`,
|
|
371
|
+
)
|
|
372
|
+
lines.push(
|
|
373
|
+
`${ui.inputEstimateLabel}≈${formatMillionTokens(estimate.estimatedInputTokens)} | ${ui.outputEstimateLabel}≈${formatMillionTokens(estimate.estimatedOutputTokens)}`,
|
|
374
|
+
)
|
|
375
|
+
return lines.join('\n')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function formatInteger(value) {
|
|
379
|
+
return new Intl.NumberFormat('en-US').format(Math.round(Number(value || 0)))
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function formatMillionTokens(value) {
|
|
383
|
+
const millions = Number(value || 0) / 1_000_000
|
|
384
|
+
if (millions >= 1) return `${millions.toFixed(2)}M tokens`
|
|
385
|
+
return `${(Number(value || 0) / 1_000).toFixed(1)}K tokens`
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function resolveShouldOpenReport(options) {
|
|
389
|
+
if (options.stdoutJson || options.estimateOnly) return false
|
|
390
|
+
if (typeof options.openReport === 'boolean') return options.openReport
|
|
391
|
+
if (!process.stdout.isTTY) return false
|
|
392
|
+
if (process.env.CI) return false
|
|
393
|
+
return true
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function shouldUseInteractiveMode(options) {
|
|
397
|
+
if (options.yes || options.nonInteractive || options.estimateOnly || options.stdoutJson) return false
|
|
398
|
+
if (process.env.CI) return false
|
|
399
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function openReportInBrowser(filePath) {
|
|
403
|
+
const command = getOpenCommand(filePath)
|
|
404
|
+
if (!command) return false
|
|
405
|
+
|
|
406
|
+
return new Promise(resolve => {
|
|
407
|
+
let settled = false
|
|
408
|
+
const child = spawn(command.bin, command.args, {
|
|
409
|
+
detached: true,
|
|
410
|
+
stdio: 'ignore',
|
|
411
|
+
})
|
|
412
|
+
child.once('error', () => {
|
|
413
|
+
if (settled) return
|
|
414
|
+
settled = true
|
|
415
|
+
resolve(false)
|
|
416
|
+
})
|
|
417
|
+
child.once('spawn', () => {
|
|
418
|
+
if (settled) return
|
|
419
|
+
settled = true
|
|
420
|
+
child.unref()
|
|
421
|
+
resolve(true)
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getOpenCommand(filePath) {
|
|
427
|
+
if (process.platform === 'darwin') {
|
|
428
|
+
return { bin: 'open', args: [filePath] }
|
|
429
|
+
}
|
|
430
|
+
if (process.platform === 'win32') {
|
|
431
|
+
return { bin: 'cmd', args: ['/c', 'start', '', filePath] }
|
|
432
|
+
}
|
|
433
|
+
if (process.platform === 'linux') {
|
|
434
|
+
return { bin: 'xdg-open', args: [filePath] }
|
|
435
|
+
}
|
|
436
|
+
return null
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function formatOpenHint(filePath) {
|
|
440
|
+
if (process.platform === 'darwin') return `open ${shellQuote(filePath)}`
|
|
441
|
+
if (process.platform === 'win32') return `start "" ${shellQuote(filePath)}`
|
|
442
|
+
if (process.platform === 'linux') return `xdg-open ${shellQuote(filePath)}`
|
|
443
|
+
return filePath
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function shellQuote(value) {
|
|
447
|
+
const text = String(value)
|
|
448
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(text)) return text
|
|
449
|
+
return `'${text.replace(/'/g, `'\\''`)}'`
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function runInteractiveWizard({ options, codexHome, defaultOutDir, progress }) {
|
|
453
|
+
let current = {
|
|
454
|
+
...options,
|
|
455
|
+
outDir: options.outDir ? path.resolve(options.outDir) : defaultOutDir,
|
|
456
|
+
lang: normalizeLang(options.lang),
|
|
457
|
+
}
|
|
458
|
+
let ui = getUiText(current.lang)
|
|
459
|
+
|
|
460
|
+
if (!hasCustomModelOverrides(current)) {
|
|
461
|
+
Object.assign(current, applyQualityPreset(current, DEFAULT_QUALITY_PRESET))
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
while (true) {
|
|
465
|
+
ui = getUiText(current.lang)
|
|
466
|
+
process.stdout.write(`${ui.wizardTitle}\n`)
|
|
467
|
+
const { threadSummaries, estimate } = await collectEstimateForOptions({
|
|
468
|
+
current,
|
|
469
|
+
codexHome,
|
|
470
|
+
ui,
|
|
471
|
+
progress,
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
process.stdout.write(`\n${renderPlanSummary(current, estimate, ui)}\n`)
|
|
475
|
+
process.stdout.write(`${ui.equivalentCommandLabel}\n${buildEquivalentCommand(current)}\n\n`)
|
|
476
|
+
|
|
477
|
+
const action = await promptChoice(
|
|
478
|
+
ui.confirmQuestion,
|
|
479
|
+
[
|
|
480
|
+
{ key: 'start', label: ui.startAnalysis },
|
|
481
|
+
{ key: 'adjust', label: ui.adjustSettings },
|
|
482
|
+
{ key: 'exit', label: ui.exitAction },
|
|
483
|
+
],
|
|
484
|
+
'start',
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
if (action === 'start') {
|
|
488
|
+
current.yes = true
|
|
489
|
+
return { options: current, threadSummaries, estimate }
|
|
490
|
+
}
|
|
491
|
+
if (action === 'exit') {
|
|
492
|
+
process.stdout.write(`${ui.cancelled}\n`)
|
|
493
|
+
return null
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
current = await runAdjustFlow(current, defaultOutDir, ui, {
|
|
497
|
+
allowQualityAdjust: !hasCustomModelOverrides(options),
|
|
498
|
+
})
|
|
499
|
+
process.stdout.write(`\n`)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function collectEstimateForOptions({ current, codexHome, ui, progress }) {
|
|
504
|
+
const sinceEpochSeconds =
|
|
505
|
+
current.days && current.days > 0
|
|
506
|
+
? Math.floor(Date.now() / 1000 - current.days * 24 * 60 * 60)
|
|
507
|
+
: null
|
|
508
|
+
const cacheDir = current.cacheDir ? path.resolve(current.cacheDir) : undefined
|
|
509
|
+
|
|
510
|
+
progress.startStage(current, ui.loadingIndex)
|
|
511
|
+
const threadSummaries = await collectThreadSummaries({
|
|
512
|
+
codexHome,
|
|
513
|
+
sinceEpochSeconds,
|
|
514
|
+
limit: current.limit,
|
|
515
|
+
includeArchived: current.includeArchived,
|
|
516
|
+
includeSubagents: current.includeSubagents,
|
|
517
|
+
cacheDir,
|
|
518
|
+
})
|
|
519
|
+
progress.completeStage(current, ui.loadingIndex)
|
|
520
|
+
|
|
521
|
+
progress.startStage(current, ui.estimating)
|
|
522
|
+
const estimate = await estimateLlmAnalysisCost({
|
|
523
|
+
threadSummaries,
|
|
524
|
+
options: current,
|
|
525
|
+
})
|
|
526
|
+
progress.completeStage(current, ui.estimating)
|
|
527
|
+
|
|
528
|
+
return { threadSummaries, estimate }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function runAdjustFlow(current, defaultOutDir, ui, config = {}) {
|
|
532
|
+
current.days = await promptScopeDays(current.days, ui)
|
|
533
|
+
Object.assign(current, await promptDepthPreset(current, ui))
|
|
534
|
+
current.lang = await promptLanguage(current.lang, ui)
|
|
535
|
+
ui = getUiText(current.lang)
|
|
536
|
+
current.outDir = await promptOutputDir(current.outDir, defaultOutDir, ui)
|
|
537
|
+
current.openReport = await promptYesNo(
|
|
538
|
+
ui.openBrowserQuestion,
|
|
539
|
+
resolveShouldOpenReport(current),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if (config.allowQualityAdjust) {
|
|
543
|
+
Object.assign(current, await promptQualityPreset(current, ui))
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return current
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function promptScopeDays(currentDays, ui) {
|
|
550
|
+
const preset = inferScopePreset(currentDays)
|
|
551
|
+
const scope = await promptChoice(
|
|
552
|
+
ui.scopeQuestion,
|
|
553
|
+
[
|
|
554
|
+
{ key: '7', label: ui.scope7 },
|
|
555
|
+
{ key: '30', label: ui.scope30 },
|
|
556
|
+
{ key: '90', label: ui.scope90 },
|
|
557
|
+
{ key: 'custom', label: ui.scopeCustom },
|
|
558
|
+
],
|
|
559
|
+
preset,
|
|
560
|
+
)
|
|
561
|
+
if (scope === 'custom') {
|
|
562
|
+
return promptIntegerInput(ui.customDaysQuestion, currentDays || 30)
|
|
563
|
+
}
|
|
564
|
+
return Number(scope)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function promptDepthPreset(current, ui) {
|
|
568
|
+
const preset = inferDepthPreset(current)
|
|
569
|
+
const choice = await promptChoice(
|
|
570
|
+
ui.depthQuestion,
|
|
571
|
+
[
|
|
572
|
+
{ key: 'conservative', label: ui.depthConservative },
|
|
573
|
+
{ key: 'standard', label: ui.depthStandard },
|
|
574
|
+
{ key: 'deep', label: ui.depthDeep },
|
|
575
|
+
{ key: 'custom', label: ui.depthCustom },
|
|
576
|
+
],
|
|
577
|
+
preset,
|
|
578
|
+
)
|
|
579
|
+
if (choice === 'custom') {
|
|
580
|
+
const limit = await promptIntegerInput(ui.limitQuestion, current.limit)
|
|
581
|
+
const facetLimit = await promptIntegerInput(ui.facetLimitQuestion, current.facetLimit)
|
|
582
|
+
return { limit, facetLimit, preset: 'custom' }
|
|
583
|
+
}
|
|
584
|
+
return { ...applyScopePreset(current, choice), preset: choice }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function promptLanguage(currentLang, ui) {
|
|
588
|
+
return promptChoice(
|
|
589
|
+
ui.languageQuestion,
|
|
590
|
+
[
|
|
591
|
+
{ key: 'en', label: 'English' },
|
|
592
|
+
{ key: 'zh-CN', label: '简体中文' },
|
|
593
|
+
],
|
|
594
|
+
normalizeLang(currentLang),
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function promptOutputDir(currentOutDir, defaultOutDir, ui) {
|
|
599
|
+
const answer = await input({
|
|
600
|
+
message: ui.outputDirQuestion,
|
|
601
|
+
default: currentOutDir || defaultOutDir,
|
|
602
|
+
})
|
|
603
|
+
return answer?.trim() ? path.resolve(answer.trim()) : currentOutDir || defaultOutDir
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function promptQualityPreset(current, ui) {
|
|
607
|
+
const preset = inferQualityPreset(current)
|
|
608
|
+
const choice = await promptChoice(
|
|
609
|
+
ui.qualityQuestion,
|
|
610
|
+
[
|
|
611
|
+
{ key: 'cheaper', label: ui.qualityCheaper },
|
|
612
|
+
{ key: 'balanced', label: ui.qualityBalanced },
|
|
613
|
+
{ key: 'higher', label: ui.qualityHigher },
|
|
614
|
+
],
|
|
615
|
+
preset,
|
|
616
|
+
)
|
|
617
|
+
return { ...applyQualityPreset(current, choice), qualityPreset: choice }
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function promptChoice(question, choices, defaultKey) {
|
|
621
|
+
return select({
|
|
622
|
+
message: question,
|
|
623
|
+
default: defaultKey,
|
|
624
|
+
choices: choices.map(choice => ({
|
|
625
|
+
value: choice.key,
|
|
626
|
+
name: choice.label,
|
|
627
|
+
})),
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function promptYesNo(question, defaultValue) {
|
|
632
|
+
return confirm({
|
|
633
|
+
message: question,
|
|
634
|
+
default: defaultValue,
|
|
635
|
+
})
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function promptIntegerInput(question, defaultValue) {
|
|
639
|
+
const answer = await input({
|
|
640
|
+
message: question,
|
|
641
|
+
default: String(defaultValue),
|
|
642
|
+
validate(value) {
|
|
643
|
+
const number = Number.parseInt(String(value).trim(), 10)
|
|
644
|
+
if (Number.isFinite(number) && number >= 0) return true
|
|
645
|
+
return 'Please enter a non-negative integer.'
|
|
646
|
+
},
|
|
647
|
+
})
|
|
648
|
+
return Number.parseInt(String(answer).trim(), 10)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function inferScopePreset(days) {
|
|
652
|
+
if (days === 7) return '7'
|
|
653
|
+
if (days === 90) return '90'
|
|
654
|
+
if (days === 30) return '30'
|
|
655
|
+
return 'custom'
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function inferDepthPreset(options) {
|
|
659
|
+
if (options.limit === 20 && options.facetLimit === 8) return 'conservative'
|
|
660
|
+
if (options.limit === 50 && options.facetLimit === 20) return 'standard'
|
|
661
|
+
if (options.limit === 200 && options.facetLimit === 50) return 'deep'
|
|
662
|
+
return 'custom'
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function inferQualityPreset(options) {
|
|
666
|
+
if (
|
|
667
|
+
options.facetModel === 'gpt-5.4-mini' &&
|
|
668
|
+
options.fastSectionModel === 'gpt-5.4-mini' &&
|
|
669
|
+
options.insightModel === 'gpt-5.4-mini'
|
|
670
|
+
) {
|
|
671
|
+
return 'cheaper'
|
|
672
|
+
}
|
|
673
|
+
if (
|
|
674
|
+
options.facetModel === 'gpt-5.4' &&
|
|
675
|
+
options.fastSectionModel === 'gpt-5.4' &&
|
|
676
|
+
options.insightModel === 'gpt-5.4'
|
|
677
|
+
) {
|
|
678
|
+
return 'higher'
|
|
679
|
+
}
|
|
680
|
+
return 'balanced'
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function applyScopePreset(options, preset) {
|
|
684
|
+
if (preset === 'conservative') return { ...options, limit: 20, facetLimit: 8 }
|
|
685
|
+
if (preset === 'deep') return { ...options, limit: 200, facetLimit: 50 }
|
|
686
|
+
return { ...options, limit: 50, facetLimit: 20 }
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function applyQualityPreset(options, preset) {
|
|
690
|
+
if (preset === 'cheaper') {
|
|
691
|
+
return {
|
|
692
|
+
...options,
|
|
693
|
+
facetModel: 'gpt-5.4-mini',
|
|
694
|
+
fastSectionModel: 'gpt-5.4-mini',
|
|
695
|
+
insightModel: 'gpt-5.4-mini',
|
|
696
|
+
facetEffort: 'low',
|
|
697
|
+
fastSectionEffort: 'low',
|
|
698
|
+
insightEffort: 'low',
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (preset === 'higher') {
|
|
702
|
+
return {
|
|
703
|
+
...options,
|
|
704
|
+
facetModel: 'gpt-5.4',
|
|
705
|
+
fastSectionModel: 'gpt-5.4',
|
|
706
|
+
insightModel: 'gpt-5.4',
|
|
707
|
+
facetEffort: 'low',
|
|
708
|
+
fastSectionEffort: 'medium',
|
|
709
|
+
insightEffort: 'high',
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
...options,
|
|
714
|
+
facetModel: 'gpt-5.4-mini',
|
|
715
|
+
fastSectionModel: 'gpt-5.4-mini',
|
|
716
|
+
insightModel: 'gpt-5.4',
|
|
717
|
+
facetEffort: 'low',
|
|
718
|
+
fastSectionEffort: 'low',
|
|
719
|
+
insightEffort: 'high',
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function hasCustomModelOverrides(options) {
|
|
724
|
+
return Boolean(
|
|
725
|
+
options.facetModel ||
|
|
726
|
+
options.fastSectionModel ||
|
|
727
|
+
options.insightModel ||
|
|
728
|
+
options.facetEffort ||
|
|
729
|
+
options.fastSectionEffort ||
|
|
730
|
+
options.insightEffort,
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function renderPlanSummary(options, estimate, ui) {
|
|
735
|
+
const lines = []
|
|
736
|
+
lines.push(ui.planSummaryTitle)
|
|
737
|
+
lines.push(
|
|
738
|
+
`${options.days} ${ui.daysLabel}, ${formatDepthPresetLabel(inferDepthPreset(options), ui)}, ${options.lang === 'zh-CN' ? '简体中文' : 'English'}`,
|
|
739
|
+
)
|
|
740
|
+
lines.push(`${ui.outputLabel}: ${options.outDir}`)
|
|
741
|
+
lines.push(`${ui.providerLabel}: ${options.provider}`)
|
|
742
|
+
lines.push('')
|
|
743
|
+
lines.push(renderEstimateSummary(estimate, options.lang))
|
|
744
|
+
return lines.join('\n')
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function formatDepthPresetLabel(preset, ui) {
|
|
748
|
+
if (preset === 'conservative') return ui.depthConservative
|
|
749
|
+
if (preset === 'deep') return ui.depthDeep
|
|
750
|
+
if (preset === 'custom') return ui.depthCustom
|
|
751
|
+
return ui.depthStandard
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function buildEquivalentCommand(options) {
|
|
755
|
+
const args = [
|
|
756
|
+
'codex-session-insights',
|
|
757
|
+
'report',
|
|
758
|
+
'--days',
|
|
759
|
+
String(options.days),
|
|
760
|
+
'--limit',
|
|
761
|
+
String(options.limit),
|
|
762
|
+
'--facet-limit',
|
|
763
|
+
String(options.facetLimit),
|
|
764
|
+
'--lang',
|
|
765
|
+
normalizeLang(options.lang),
|
|
766
|
+
'--yes',
|
|
767
|
+
]
|
|
768
|
+
if (typeof options.openReport === 'boolean') {
|
|
769
|
+
args.push(options.openReport ? '--open' : '--no-open')
|
|
770
|
+
}
|
|
771
|
+
if (options.includeArchived) args.push('--include-archived')
|
|
772
|
+
if (options.includeSubagents) args.push('--include-subagents')
|
|
773
|
+
if (options.outDir) args.push('--out-dir', shellQuote(options.outDir))
|
|
774
|
+
if (options.provider !== 'codex-cli') args.push('--provider', options.provider)
|
|
775
|
+
if (options.facetModel) args.push('--facet-model', options.facetModel)
|
|
776
|
+
if (options.fastSectionModel) args.push('--fast-section-model', options.fastSectionModel)
|
|
777
|
+
if (options.insightModel) args.push('--insight-model', options.insightModel)
|
|
778
|
+
if (options.facetEffort) args.push('--facet-effort', options.facetEffort)
|
|
779
|
+
if (options.fastSectionEffort) args.push('--fast-section-effort', options.fastSectionEffort)
|
|
780
|
+
if (options.insightEffort) args.push('--insight-effort', options.insightEffort)
|
|
781
|
+
return args.join(' ')
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function normalizeLang(value) {
|
|
785
|
+
if (!value) return 'en'
|
|
786
|
+
const normalized = String(value).trim()
|
|
787
|
+
if (
|
|
788
|
+
normalized === 'zh' ||
|
|
789
|
+
normalized === 'zh-CN' ||
|
|
790
|
+
normalized === 'zh-Hans' ||
|
|
791
|
+
normalized.startsWith('zh_') ||
|
|
792
|
+
normalized.startsWith('zh-')
|
|
793
|
+
) {
|
|
794
|
+
return 'zh-CN'
|
|
795
|
+
}
|
|
796
|
+
return 'en'
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function detectSystemLanguage() {
|
|
800
|
+
if (process.env.CODEX_REPORT_LANG) {
|
|
801
|
+
return normalizeLang(process.env.CODEX_REPORT_LANG)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale
|
|
805
|
+
const normalizedIntl = normalizeLang(intlLocale)
|
|
806
|
+
if (normalizedIntl === 'zh-CN') return normalizedIntl
|
|
807
|
+
|
|
808
|
+
const candidates = [process.env.LC_ALL, process.env.LC_MESSAGES, process.env.LANG].filter(Boolean)
|
|
809
|
+
|
|
810
|
+
for (const value of candidates) {
|
|
811
|
+
const normalized = normalizeLang(String(value).split('.')[0])
|
|
812
|
+
if (normalized === 'zh-CN') return normalized
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return 'en'
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function getUiText(lang) {
|
|
819
|
+
if (normalizeLang(lang) === 'zh-CN') {
|
|
820
|
+
return {
|
|
821
|
+
wizardTitle: '\nCodex Session Insights 配置向导\n',
|
|
822
|
+
loadingIndex: '正在读取线程索引...',
|
|
823
|
+
estimating: '正在预估分析成本...',
|
|
824
|
+
generating: '正在生成报告...',
|
|
825
|
+
writingFiles: '正在写入报告文件...',
|
|
826
|
+
openingBrowser: '正在打开浏览器...',
|
|
827
|
+
openedInBrowser: '已在浏览器中打开报告。',
|
|
828
|
+
openHint: '可用以下命令打开',
|
|
829
|
+
scopeQuestion: '选择分析时间范围:',
|
|
830
|
+
scope7: '最近 7 天',
|
|
831
|
+
scope30: '最近 30 天',
|
|
832
|
+
scope90: '最近 90 天',
|
|
833
|
+
scopeCustom: '自定义天数',
|
|
834
|
+
customDaysQuestion: '输入要分析的天数',
|
|
835
|
+
depthQuestion: '选择分析深度:',
|
|
836
|
+
depthConservative: '保守(20 threads / 8 facets)',
|
|
837
|
+
depthStandard: '标准(50 threads / 20 facets)',
|
|
838
|
+
depthDeep: '深度(200 threads / 50 facets)',
|
|
839
|
+
depthCustom: '自定义',
|
|
840
|
+
limitQuestion: '输入最大 thread 数',
|
|
841
|
+
facetLimitQuestion: '输入最大 facet 提取数',
|
|
842
|
+
languageQuestion: '选择报告语言:',
|
|
843
|
+
outputDirQuestion: '输出目录',
|
|
844
|
+
openBrowserQuestion: '生成后自动打开浏览器?',
|
|
845
|
+
qualityQuestion: '选择分析质量预设:',
|
|
846
|
+
qualityCheaper: '更省(尽量用 spark)',
|
|
847
|
+
qualityBalanced: '平衡',
|
|
848
|
+
qualityHigher: '更高质量(更多使用 gpt-5.4)',
|
|
849
|
+
yesDefault: '[Y/n]',
|
|
850
|
+
noDefault: '[y/N]',
|
|
851
|
+
invalidYesNo: '输入无效,使用默认值。',
|
|
852
|
+
confirmQuestion: '确认这次分析计划:',
|
|
853
|
+
startAnalysis: '开始分析',
|
|
854
|
+
adjustSettings: '重新调整设置',
|
|
855
|
+
exitAction: '退出',
|
|
856
|
+
cancelled: '已取消。',
|
|
857
|
+
equivalentCommandLabel: '等价命令:',
|
|
858
|
+
planSummaryTitle: '计划摘要',
|
|
859
|
+
analysisEstimateTitle: '分析预估',
|
|
860
|
+
toWord: '到',
|
|
861
|
+
likelyWord: '左右',
|
|
862
|
+
plannedCallsLabel: '预计调用数',
|
|
863
|
+
substantiveThreadsLabel: '有效线程',
|
|
864
|
+
uncachedFacetsLabel: '未缓存 facets',
|
|
865
|
+
longTranscriptsLabel: '长 transcript',
|
|
866
|
+
inputEstimateLabel: '输入',
|
|
867
|
+
outputEstimateLabel: '输出',
|
|
868
|
+
daysLabel: '天',
|
|
869
|
+
depthLabel: '深度',
|
|
870
|
+
outputLabel: '输出目录',
|
|
871
|
+
providerLabel: 'Provider',
|
|
872
|
+
facetProgress: '提取 facets',
|
|
873
|
+
sectionProgress: '生成 sections',
|
|
874
|
+
modelFallback: '模型降级',
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
wizardTitle: '\nCodex Session Insights Setup\n',
|
|
880
|
+
loadingIndex: 'Loading thread index...',
|
|
881
|
+
estimating: 'Estimating analysis cost...',
|
|
882
|
+
generating: 'Generating report...',
|
|
883
|
+
writingFiles: 'Writing report files...',
|
|
884
|
+
openingBrowser: 'Opening browser...',
|
|
885
|
+
openedInBrowser: 'Opened report in your browser.',
|
|
886
|
+
openHint: 'Open it with',
|
|
887
|
+
scopeQuestion: 'Choose analysis range:',
|
|
888
|
+
scope7: 'Last 7 days',
|
|
889
|
+
scope30: 'Last 30 days',
|
|
890
|
+
scope90: 'Last 90 days',
|
|
891
|
+
scopeCustom: 'Custom days',
|
|
892
|
+
customDaysQuestion: 'Enter number of days to analyze',
|
|
893
|
+
depthQuestion: 'Choose analysis depth:',
|
|
894
|
+
depthConservative: 'Conservative (20 threads / 8 facets)',
|
|
895
|
+
depthStandard: 'Standard (50 threads / 20 facets)',
|
|
896
|
+
depthDeep: 'Deep (200 threads / 50 facets)',
|
|
897
|
+
depthCustom: 'Custom',
|
|
898
|
+
limitQuestion: 'Enter max thread count',
|
|
899
|
+
facetLimitQuestion: 'Enter max facet extraction count',
|
|
900
|
+
languageQuestion: 'Choose report language:',
|
|
901
|
+
outputDirQuestion: 'Output directory',
|
|
902
|
+
openBrowserQuestion: 'Open the report in your browser after generation?',
|
|
903
|
+
qualityQuestion: 'Choose quality preset:',
|
|
904
|
+
qualityCheaper: 'Cheaper (more spark)',
|
|
905
|
+
qualityBalanced: 'Balanced',
|
|
906
|
+
qualityHigher: 'Higher quality (more gpt-5.4)',
|
|
907
|
+
yesDefault: '[Y/n]',
|
|
908
|
+
noDefault: '[y/N]',
|
|
909
|
+
invalidYesNo: 'Invalid input, using default.',
|
|
910
|
+
confirmQuestion: 'Confirm this analysis plan:',
|
|
911
|
+
startAnalysis: 'Start analysis',
|
|
912
|
+
adjustSettings: 'Adjust settings',
|
|
913
|
+
exitAction: 'Exit',
|
|
914
|
+
cancelled: 'Cancelled.',
|
|
915
|
+
equivalentCommandLabel: 'Equivalent command:',
|
|
916
|
+
planSummaryTitle: 'Plan Summary',
|
|
917
|
+
analysisEstimateTitle: 'Analysis Estimate',
|
|
918
|
+
toWord: 'to',
|
|
919
|
+
likelyWord: 'likely',
|
|
920
|
+
plannedCallsLabel: 'planned calls',
|
|
921
|
+
substantiveThreadsLabel: 'substantive threads',
|
|
922
|
+
uncachedFacetsLabel: 'uncached facets',
|
|
923
|
+
longTranscriptsLabel: 'long transcripts',
|
|
924
|
+
inputEstimateLabel: 'input',
|
|
925
|
+
outputEstimateLabel: 'output',
|
|
926
|
+
daysLabel: 'days',
|
|
927
|
+
depthLabel: 'depth',
|
|
928
|
+
outputLabel: 'Output',
|
|
929
|
+
providerLabel: 'Provider',
|
|
930
|
+
facetProgress: 'Extracting facets',
|
|
931
|
+
sectionProgress: 'Generating sections',
|
|
932
|
+
modelFallback: 'Model fallback',
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function logStage(options, message) {
|
|
937
|
+
if (options.stdoutJson) return
|
|
938
|
+
process.stdout.write(`${message}\n`)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function createProgressUi(initialOptions = {}) {
|
|
942
|
+
const spinner =
|
|
943
|
+
process.stdout.isTTY && !initialOptions.stdoutJson && !process.env.CI
|
|
944
|
+
? ora({ isSilent: false })
|
|
945
|
+
: null
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
startStage(options, message) {
|
|
949
|
+
if (spinner) {
|
|
950
|
+
spinner.start(message)
|
|
951
|
+
return
|
|
952
|
+
}
|
|
953
|
+
logStage(options, message)
|
|
954
|
+
},
|
|
955
|
+
completeStage(options, message) {
|
|
956
|
+
if (spinner) {
|
|
957
|
+
spinner.succeed(message)
|
|
958
|
+
return
|
|
959
|
+
}
|
|
960
|
+
logStage(options, message)
|
|
961
|
+
},
|
|
962
|
+
failStage(options, message) {
|
|
963
|
+
if (spinner) {
|
|
964
|
+
spinner.fail(message)
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
logStage(options, message)
|
|
968
|
+
},
|
|
969
|
+
updateFromEvent(options, event) {
|
|
970
|
+
if (!spinner || !event) return
|
|
971
|
+
const ui = getUiText(options.lang)
|
|
972
|
+
if (event.kind === 'facets:planned') {
|
|
973
|
+
spinner.text = `${ui.facetProgress}: 0/${event.total}`
|
|
974
|
+
return
|
|
975
|
+
}
|
|
976
|
+
if (event.kind === 'facets:progress') {
|
|
977
|
+
spinner.text = `${ui.facetProgress}: ${event.completed}/${event.total}`
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
if (event.kind === 'sections:planned') {
|
|
981
|
+
spinner.text = `${ui.sectionProgress}: 0/${event.total}`
|
|
982
|
+
return
|
|
983
|
+
}
|
|
984
|
+
if (event.kind === 'sections:progress') {
|
|
985
|
+
spinner.text = `${ui.sectionProgress}: ${event.completed}/${event.total} (${event.section})`
|
|
986
|
+
return
|
|
987
|
+
}
|
|
988
|
+
if (event.kind === 'model:fallback') {
|
|
989
|
+
spinner.text = `${ui.modelFallback}: ${event.fromModel} -> ${event.toModel}`
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
export const __test = {
|
|
996
|
+
shouldUseInteractiveMode,
|
|
997
|
+
applyScopePreset,
|
|
998
|
+
applyQualityPreset,
|
|
999
|
+
buildEquivalentCommand,
|
|
1000
|
+
normalizeLang,
|
|
1001
|
+
detectSystemLanguage,
|
|
1002
|
+
}
|