free-coding-models 0.3.9 β†’ 0.3.12

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @file src/product-flags.js
3
+ * @description Product-level copy for temporarily unavailable surfaces.
4
+ *
5
+ * @details
6
+ * πŸ“– The proxy bridge is being rebuilt from scratch. The main TUI still
7
+ * πŸ“– shows a clear status line so users know the missing integration is
8
+ * πŸ“– intentional instead of silently broken.
9
+ *
10
+ * @exports PROXY_DISABLED_NOTICE
11
+ */
12
+
13
+ // πŸ“– Public note rendered in the main TUI footer and reused in CLI/runtime guards.
14
+ export const PROXY_DISABLED_NOTICE = 'ℹ️ Proxy is temporarily disabled while we rebuild it into a much more stable bridge for external tools.'
@@ -10,7 +10,6 @@
10
10
  * - Overlay viewport management (scrolling, clamping, visibility)
11
11
  * - Table viewport calculation
12
12
  * - Sorting with pinned favorites and recommendations
13
- * - Proxy status rendering
14
13
  *
15
14
  * 🎯 Key features:
16
15
  * - Emoji-aware display width calculation without external dependencies
@@ -19,7 +18,6 @@
19
18
  * - Overlay viewport helpers (clamp, slice, scroll target visibility)
20
19
  * - Table viewport calculation with scroll indicators
21
20
  * - Sorting with pinned favorites/recommendations at top
22
- * - Proxy status formatting
23
21
  *
24
22
  * β†’ Functions:
25
23
  * - `stripAnsi`: Remove ANSI color codes to estimate visible text width
@@ -32,13 +30,12 @@
32
30
  * - `sliceOverlayLines`: Slice lines to viewport and pad with blanks
33
31
  * - `calculateViewport`: Compute visible slice of model rows
34
32
  * - `sortResultsWithPinnedFavorites`: Sort with pinned items at top
35
- * - `renderProxyStatusLine`: Format proxy status for footer display
36
33
  * - `adjustScrollOffset`: Clamp scrollOffset so cursor stays visible
37
34
  *
38
35
  * πŸ“¦ Dependencies:
39
36
  * - chalk: Terminal colors and formatting
40
37
  * - ../src/constants.js: OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES
41
- * - ../src/utils.js: sortResults, getProxyStatusInfo
38
+ * - ../src/utils.js: sortResults
42
39
  *
43
40
  * βš™οΈ Configuration:
44
41
  * - OVERLAY_PANEL_WIDTH: Fixed width for overlay panels (from constants.js)
@@ -50,7 +47,7 @@
50
47
 
51
48
  import chalk from 'chalk'
52
49
  import { OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES } from './constants.js'
53
- import { sortResults, getProxyStatusInfo } from './utils.js'
50
+ import { sortResults } from './utils.js'
54
51
 
55
52
  // πŸ“– stripAnsi: Remove ANSI color/control sequences to estimate visible text width before padding.
56
53
  // πŸ“– Strips CSI sequences (SGR colors) and OSC sequences (hyperlinks).
@@ -189,35 +186,6 @@ export function sortResultsWithPinnedFavorites(results, sortColumn, sortDirectio
189
186
  return [...bothRows, ...recommendedRows, ...favoriteRows, ...nonSpecialRows]
190
187
  }
191
188
 
192
- // ─── Proxy status rendering ───────────────────────────────────────────────────
193
-
194
- // πŸ“– renderProxyStatusLine: Maps proxyStartupStatus + active proxy into a chalk-coloured footer line.
195
- // πŸ“– Always returns a non-empty string (no hidden states) so the footer row is always present.
196
- // πŸ“– Delegates state classification to the pure getProxyStatusInfo helper (testable in utils.js).
197
- export function renderProxyStatusLine(proxyStartupStatus, proxyInstance, proxyEnabled = false) {
198
- const activeStatus = typeof proxyInstance?.getStatus === 'function' ? proxyInstance.getStatus() : null
199
- const hasLiveProxy = Boolean(proxyInstance) && activeStatus?.running !== false
200
- const info = getProxyStatusInfo(proxyStartupStatus, hasLiveProxy, proxyEnabled)
201
- const neonGreen = chalk.rgb(57, 255, 20)
202
- switch (info.state) {
203
- case 'starting':
204
- return chalk.dim(' ') + chalk.yellow('⟳ Proxy') + chalk.dim(' starting…')
205
- case 'running': {
206
- const resolvedPort = info.port ?? activeStatus?.port ?? activeStatus?.listeningPort ?? null
207
- const resolvedAccountCount = info.accountCount ?? activeStatus?.accountCount ?? proxyInstance?._accounts?.length ?? null
208
- const portPart = resolvedPort ? chalk.dim(` :${resolvedPort}`) : ''
209
- const acctPart = resolvedAccountCount != null ? chalk.dim(` Β· ${resolvedAccountCount} account${resolvedAccountCount === 1 ? '' : 's'}`) : ''
210
- return chalk.dim(' ') + neonGreen('πŸ”€ Proxy running') + portPart + acctPart
211
- }
212
- case 'failed':
213
- return chalk.dim(' ') + chalk.red('πŸ”€ Proxy Stopped') + chalk.dim(` β€” ${info.reason}`)
214
- case 'configured':
215
- return chalk.dim(' ') + chalk.cyan('πŸ”€ Proxy configured') + chalk.dim(' β€” OpenCode rotation')
216
- default:
217
- return chalk.dim(' ') + chalk.red('πŸ”€ Proxy Stopped')
218
- }
219
- }
220
-
221
189
  // ─── Scroll offset adjustment ──────────────────────────────────────────────────
222
190
 
223
191
  // πŸ“– adjustScrollOffset: Clamp scrollOffset so cursor is always within the visible viewport window.
@@ -12,14 +12,12 @@
12
12
  * - Hotkey-aware header lettering so highlighted letters always match live sort/filter keys
13
13
  * - Emoji-aware padding via padEndDisplay for aligned verdict/status cells
14
14
  * - Viewport clipping with above/below indicators
15
- * - Smart badges (mode, tier filter, origin filter, profile)
16
- * - Footer J badge: green "Proxy On" / red "Proxy Off" indicator with direct overlay access
15
+ * - Smart badges (mode, tier filter, origin filter)
17
16
  * - Install-endpoints shortcut surfaced directly in the footer hints
18
17
  * - Full-width red outdated-version banner when a newer npm release is known
19
18
  * - Distinct auth-failure vs missing-key health labels so configured providers stay honest
20
19
  *
21
20
  * β†’ Functions:
22
- * - `setActiveProxy` β€” Provide the active proxy instance for footer status rendering
23
21
  * - `renderTable` β€” Render the full TUI table as a string (no side effects)
24
22
  *
25
23
  * πŸ“¦ Dependencies:
@@ -28,7 +26,7 @@
28
26
  * - ../src/tier-colors.js: TIER_COLOR
29
27
  * - ../src/utils.js: getAvg, getVerdict, getUptime, getStabilityScore
30
28
  * - ../src/ping.js: usagePlaceholderForProvider
31
- * - ../src/render-helpers.js: calculateViewport, sortResultsWithPinnedFavorites, renderProxyStatusLine, padEndDisplay
29
+ * - ../src/render-helpers.js: calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay
32
30
  *
33
31
  * @see bin/free-coding-models.js β€” main entry point that calls renderTable
34
32
  */
@@ -43,6 +41,7 @@ import { usagePlaceholderForProvider } from './ping.js'
43
41
  import { formatTokenTotalCompact } from './token-usage-reader.js'
44
42
  import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
45
43
  import { getToolMeta } from './tool-metadata.js'
44
+ import { PROXY_DISABLED_NOTICE } from './product-flags.js'
46
45
 
47
46
  const ACTIVE_FILTER_BG_BY_TIER = {
48
47
  'S+': [57, 255, 20],
@@ -84,16 +83,8 @@ export const PROVIDER_COLOR = {
84
83
  iflow: [220, 231, 117],
85
84
  }
86
85
 
87
- // πŸ“– Active proxy reference for footer status line (set by bin/free-coding-models.js).
88
- let activeProxyRef = null
89
-
90
- // πŸ“– setActiveProxy: Store active proxy instance for renderTable footer line.
91
- export function setActiveProxy(proxyInstance) {
92
- activeProxyRef = proxyInstance
93
- }
94
-
95
86
  // ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
96
- export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, proxyEnabled = false, startupLatestVersion = null, versionAlertsEnabled = true, disableWidthsWarning = false) {
87
+ export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, disableWidthsWarning = false) {
97
88
  // πŸ“– Filter out hidden models for display
98
89
  const visibleResults = results.filter(r => !r.hidden)
99
90
 
@@ -172,11 +163,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
172
163
  }
173
164
  }
174
165
 
175
- // πŸ“– Profile badge β€” shown when a named profile is active (Shift+P to cycle, Shift+S to save)
176
- let profileBadge = ''
177
- if (activeProfile) {
178
- profileBadge = chalk.bold.rgb(200, 150, 255)(` [πŸ“‹ ${activeProfile}]`)
179
- }
166
+
180
167
 
181
168
  // πŸ“– Column widths (generous spacing with margins)
182
169
  const W_RANK = 6
@@ -230,7 +217,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
230
217
  const sorted = sortResultsWithPinnedFavorites(visibleResults, sortColumn, sortDirection)
231
218
 
232
219
  const lines = [
233
- ` ${chalk.cyanBright.bold(`πŸš€ free-coding-models v${LOCAL_VERSION}`)}${modeBadge}${pingControlBadge}${tierBadge}${originBadge}${profileBadge}${chalk.reset('')} ` +
220
+ ` ${chalk.cyanBright.bold(`πŸš€ free-coding-models v${LOCAL_VERSION}`)}${modeBadge}${pingControlBadge}${tierBadge}${originBadge}${chalk.reset('')} ` +
234
221
  chalk.dim('πŸ“¦ ') + chalk.cyanBright.bold(`${completedPings}/${totalVisible}`) + chalk.dim(' ') +
235
222
  chalk.greenBright(`βœ… ${up}`) + chalk.dim(' up ') +
236
223
  chalk.yellow(`⏳ ${timeout}`) + chalk.dim(' timeout ') +
@@ -565,7 +552,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
565
552
  const usageCell = ''
566
553
 
567
554
  // πŸ“– Used column β€” total historical prompt+completion tokens consumed for this
568
- // πŸ“– exact provider/model pair, loaded once from request-log.jsonl at startup.
555
+ // πŸ“– exact provider/model pair, loaded from the local usage snapshot file at startup.
569
556
  const tokenTotal = Number(r.totalTokens) || 0
570
557
  const tokensCell = tokenTotal > 0
571
558
  ? chalk.rgb(120, 210, 255)(formatTokenTotalCompact(tokenTotal).padEnd(W_TOKENS))
@@ -590,12 +577,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
590
577
  lines.push(chalk.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
591
578
  }
592
579
 
593
- // πŸ“– Profile save inline prompt β€” shown when Shift+S is pressed, replaces spacer line
594
- if (profileSaveMode) {
595
- lines.push(chalk.bgRgb(40, 20, 60)(` πŸ“‹ Save profile as: ${chalk.cyanBright(profileSaveBuffer + '▏')} ${chalk.dim('Enter save β€’ Esc cancel')}`))
596
- } else {
597
- lines.push('')
598
- }
580
+ lines.push('')
599
581
  // πŸ“– Footer hints keep only navigation and secondary actions now that the
600
582
  // πŸ“– active tool target is already visible in the header badge.
601
583
  const hotkey = (keyLabel, text) => chalk.yellow(keyLabel) + chalk.dim(text)
@@ -604,8 +586,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
604
586
  const activeHotkey = (keyLabel, text, bg = [57, 255, 20], fg = [0, 0, 0]) => chalk.bgRgb(...bg).rgb(...fg)(` ${keyLabel}${text} `)
605
587
  // πŸ“– Line 1: core navigation + filtering shortcuts
606
588
  lines.push(
607
- (proxyEnabled ? activeHotkey('J', ' πŸ“‘ FCM Proxy V2 On') : activeHotkey('J', ' πŸ“‘ FCM Proxy V2 Off', [180, 30, 30], [255, 255, 255])) +
608
- chalk.dim(` β€’ `) +
609
589
  hotkey('F', ' Toggle Favorite') +
610
590
  chalk.dim(` β€’ `) +
611
591
  (tierFilterMode > 0
@@ -618,17 +598,13 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
618
598
  chalk.dim(` β€’ `) +
619
599
  (hideUnconfiguredModels ? activeHotkey('E', ' Configured Models Only') : hotkey('E', ' Configured Models Only')) +
620
600
  chalk.dim(` β€’ `) +
621
- hotkey('X', ' Token Logs') +
622
- chalk.dim(` β€’ `) +
623
601
  hotkey('P', ' Settings') +
624
602
  chalk.dim(` β€’ `) +
625
603
  hotkey('K', ' Help')
626
604
  )
627
- // πŸ“– Line 2: profiles, install flow, recommend, proxy shortcut, feedback, and extended hints.
605
+ // πŸ“– Line 2: install flow, recommend, feedback, and extended hints.
628
606
  lines.push(
629
607
  chalk.dim(` `) +
630
- hotkey('⇧P', ' Cycle profile') + chalk.dim(` β€’ `) +
631
- hotkey('⇧S', ' Save profile') + chalk.dim(` β€’ `) +
632
608
  hotkey('Y', ' Install endpoints') + chalk.dim(` β€’ `) +
633
609
  hotkey('Q', ' Smart Recommend') + chalk.dim(` β€’ `) +
634
610
  hotkey('I', ' Feedback, bugs & requests')
@@ -665,6 +641,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
665
641
  lines.push(chalk.bgRed.white.bold(paddedBanner))
666
642
  }
667
643
 
644
+ // πŸ“– Stable release notice: keep the bridge rebuild status explicit in the main UI
645
+ // πŸ“– so users do not go hunting for hidden controls that are disabled on purpose.
646
+ const bridgeNotice = chalk.magentaBright.italic(` ${PROXY_DISABLED_NOTICE}`)
647
+ lines.push(bridgeNotice)
648
+
668
649
  // πŸ“– Append \x1b[K (erase to EOL) to each line so leftover chars from previous
669
650
  // πŸ“– frames are cleared. Then pad with blank cleared lines to fill the terminal,
670
651
  // πŸ“– preventing stale content from lingering at the bottom after resize.
package/src/testfcm.js CHANGED
@@ -18,13 +18,14 @@
18
18
  * β†’ `hasConfiguredKey` β€” decide whether a config entry really contains an API key
19
19
  * β†’ `createTestfcmRunId` β€” build a stable timestamp-based run id for artifacts
20
20
  * β†’ `extractJsonPayload` β€” recover JSON mode output even when logs prefix stdout
21
+ * β†’ `pickTestfcmSelectionIndex` β€” pick the most promising preflight row before sending Enter
21
22
  * β†’ `detectTranscriptFindings` β€” map raw tool output to actionable failure findings
22
23
  * β†’ `classifyToolTranscript` β€” classify a run as passed, failed, or inconclusive
23
24
  * β†’ `buildFixTasks` β€” convert findings into concrete follow-up work items
24
25
  * β†’ `buildTestfcmReport` β€” render the final Markdown report written under `task/`
25
26
  *
26
27
  * @exports TESTFCM_TOOL_SPECS, normalizeTestfcmToolName, resolveTestfcmToolSpec
27
- * @exports hasConfiguredKey, createTestfcmRunId, extractJsonPayload
28
+ * @exports hasConfiguredKey, createTestfcmRunId, extractJsonPayload, pickTestfcmSelectionIndex
28
29
  * @exports detectTranscriptFindings, classifyToolTranscript, buildFixTasks
29
30
  * @exports buildTestfcmReport
30
31
  */
@@ -35,39 +36,15 @@ export const TESTFCM_TOOL_SPECS = {
35
36
  label: 'Crush',
36
37
  command: 'crush',
37
38
  flag: '--crush',
38
- prefersProxy: true,
39
+ prefersProxy: false,
39
40
  configPaths: ['.config/crush/crush.json'],
40
41
  },
41
- codex: {
42
- mode: 'codex',
43
- label: 'Codex CLI',
44
- command: 'codex',
45
- flag: '--codex',
46
- prefersProxy: true,
47
- configPaths: [],
48
- },
49
- 'claude-code': {
50
- mode: 'claude-code',
51
- label: 'Claude Code',
52
- command: 'claude',
53
- flag: '--claude-code',
54
- prefersProxy: true,
55
- configPaths: [],
56
- },
57
- gemini: {
58
- mode: 'gemini',
59
- label: 'Gemini CLI',
60
- command: 'gemini',
61
- flag: '--gemini',
62
- prefersProxy: true,
63
- configPaths: ['.gemini/settings.json'],
64
- },
65
42
  goose: {
66
43
  mode: 'goose',
67
44
  label: 'Goose',
68
45
  command: 'goose',
69
46
  flag: '--goose',
70
- prefersProxy: true,
47
+ prefersProxy: false,
71
48
  configPaths: ['.config/goose/config.yaml'],
72
49
  },
73
50
  aider: {
@@ -121,13 +98,17 @@ export const TESTFCM_TOOL_SPECS = {
121
98
  }
122
99
 
123
100
  const TESTFCM_TOOL_ALIASES = {
124
- claude: 'claude-code',
125
- claudecode: 'claude-code',
126
- codexcli: 'codex',
127
101
  opencodecli: 'opencode',
128
102
  }
129
103
 
130
104
  const TRANSCRIPT_FINDING_RULES = [
105
+ {
106
+ id: 'terminal_too_small',
107
+ title: 'PTY width warning blocked the TUI flow',
108
+ severity: 'high',
109
+ regex: /please maximize your terminal|terminal is too small|reduce font size or maximize width/i,
110
+ task: 'Run `/testfcm` with the width warning disabled in the isolated config or force a wider PTY before sending Enter.',
111
+ },
131
112
  {
132
113
  id: 'tool_missing',
133
114
  title: 'Tool binary missing',
@@ -140,21 +121,14 @@ const TRANSCRIPT_FINDING_RULES = [
140
121
  title: 'Invalid or missing API auth',
141
122
  severity: 'high',
142
123
  regex: /invalid api|bad api key|incorrect api key|authentication failed|unauthorized|forbidden|missing api key|no api key|anthropic_auth_token|401\b|403\b/i,
143
- task: 'Validate the provider key used by the selected model, then re-run `/testfcm` and inspect the proxy request log for the failing request.',
124
+ task: 'Validate the provider key used by the selected model, then re-run `/testfcm` and inspect the generated tool config and transcript.',
144
125
  },
145
126
  {
146
127
  id: 'rate_limited',
147
128
  title: 'Provider rate limited',
148
129
  severity: 'medium',
149
130
  regex: /rate limit|too many requests|quota exceeded|429\b/i,
150
- task: 'Retry with another configured provider or inspect the retry-after and cooldown handling in the proxy/tool launch flow.',
151
- },
152
- {
153
- id: 'proxy_failure',
154
- title: 'Proxy startup or routing failure',
155
- severity: 'high',
156
- regex: /failed to start proxy|proxy mode .* required|selected model may not exist|routing reload is taking longer than expected|proxy launch is blocked/i,
157
- task: 'Inspect `request-log.jsonl`, proxy startup messages, and the isolated config to verify the tool can reach the local FCM proxy.',
131
+ task: 'Retry with another configured provider or inspect cooldown handling in the direct launcher flow.',
158
132
  },
159
133
  {
160
134
  id: 'tool_launch_failed',
@@ -211,7 +185,9 @@ export function hasConfiguredKey(value) {
211
185
  }
212
186
 
213
187
  /**
214
- * πŸ“– Build an artifact-friendly run id such as `20260316-184512`.
188
+ * πŸ“– Build an artifact-friendly run id such as `20260316-184512-123`.
189
+ * πŸ“– Milliseconds keep concurrent `/testfcm` runs from clobbering each other's
190
+ * πŸ“– reports and isolated HOME directories when they start in the same second.
215
191
  *
216
192
  * @param {Date} [date]
217
193
  * @returns {string}
@@ -219,10 +195,11 @@ export function hasConfiguredKey(value) {
219
195
  export function createTestfcmRunId(date = new Date()) {
220
196
  const iso = date.toISOString()
221
197
  return iso
222
- .replace(/\.\d{3}Z$/, '')
198
+ .replace(/Z$/, '')
223
199
  .replace(/:/g, '')
224
200
  .replace(/-/g, '')
225
201
  .replace('T', '-')
202
+ .replace(/\.(\d{3})$/, '-$1')
226
203
  }
227
204
 
228
205
  /**
@@ -246,6 +223,76 @@ export function extractJsonPayload(text) {
246
223
  return null
247
224
  }
248
225
 
226
+ /**
227
+ * πŸ“– Pick the best row to highlight before the runner presses Enter.
228
+ * πŸ“– The TUI and `--json` share the same sorted result order, so picking the
229
+ * πŸ“– first clearly healthy row from the preflight is a cheap way to avoid
230
+ * πŸ“– wasting E2E runs on obviously dead or auth-failing models.
231
+ *
232
+ * @param {Array<{ label?: string, status?: string, httpCode?: string }>} results
233
+ * @param {{ preferProxy?: boolean }} [options]
234
+ * @returns {number}
235
+ */
236
+ export function pickTestfcmSelectionIndex(results, options = {}) {
237
+ if (!Array.isArray(results) || results.length === 0) return 0
238
+
239
+ if (options.preferProxy === true) {
240
+ const groups = new Map()
241
+
242
+ for (let index = 0; index < results.length; index++) {
243
+ const row = results[index]
244
+ const label = typeof row?.label === 'string' ? row.label.trim() : ''
245
+ if (!label) continue
246
+
247
+ if (!groups.has(label)) {
248
+ groups.set(label, {
249
+ rows: [],
250
+ hasUp: false,
251
+ hasAuthFailure: false,
252
+ hasRateLimit: false,
253
+ hasNotFound: false,
254
+ })
255
+ }
256
+
257
+ const group = groups.get(label)
258
+ const httpCode = String(row?.httpCode || '')
259
+ group.rows.push({ index, row })
260
+ if (row?.status === 'up') group.hasUp = true
261
+ if (row?.status === 'auth_error' || httpCode === '401' || httpCode === '403') group.hasAuthFailure = true
262
+ if (httpCode === '429') group.hasRateLimit = true
263
+ if (httpCode === '404') group.hasNotFound = true
264
+ }
265
+
266
+ for (const row of results) {
267
+ const label = typeof row?.label === 'string' ? row.label.trim() : ''
268
+ const group = groups.get(label)
269
+ if (!group?.hasUp) continue
270
+ if (group.hasAuthFailure || group.hasRateLimit || group.hasNotFound) continue
271
+ const target = group.rows.find((entry) => entry.row?.status === 'up')
272
+ if (target) return target.index
273
+ }
274
+
275
+ for (const row of results) {
276
+ const label = typeof row?.label === 'string' ? row.label.trim() : ''
277
+ const group = groups.get(label)
278
+ if (!group?.hasUp || group.hasAuthFailure) continue
279
+ const target = group.rows.find((entry) => entry.row?.status === 'up')
280
+ if (target) return target.index
281
+ }
282
+ }
283
+
284
+ const exactUpIndex = results.findIndex((row) => row?.status === 'up' && String(row?.httpCode || '') === '200')
285
+ if (exactUpIndex >= 0) return exactUpIndex
286
+
287
+ const upIndex = results.findIndex((row) => row?.status === 'up')
288
+ if (upIndex >= 0) return upIndex
289
+
290
+ const okCodeIndex = results.findIndex((row) => String(row?.httpCode || '') === '200')
291
+ if (okCodeIndex >= 0) return okCodeIndex
292
+
293
+ return 0
294
+ }
295
+
249
296
  /**
250
297
  * πŸ“– Detect known failure patterns in the raw tool transcript.
251
298
  *
@@ -409,14 +456,14 @@ export function buildTestfcmReport(input) {
409
456
  }
410
457
  lines.push('')
411
458
 
412
- lines.push('## Request Log Summary')
459
+ lines.push('## Runtime Diagnostics')
413
460
  lines.push('')
414
461
  if (requestLogSummary.length > 0) {
415
462
  for (const entry of requestLogSummary) {
416
463
  lines.push(`- ${entry}`)
417
464
  }
418
465
  } else {
419
- lines.push('- No proxy request log entry was captured for this run.')
466
+ lines.push('- No extra runtime diagnostic summary was captured for this run.')
420
467
  }
421
468
  lines.push('')
422
469
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file token-usage-reader.js
3
- * @description Reads historical token usage from request-log.jsonl and aggregates it by exact provider + model pair.
3
+ * @description Reads historical token usage snapshots and aggregates them by exact provider + model pair.
4
4
  *
5
5
  * @details
6
6
  * The TUI already shows live latency and quota state, but that does not tell
@@ -9,27 +9,22 @@
9
9
  * `provider::model -> totalTokens` map for table display.
10
10
  *
11
11
  * Why this exists:
12
- * - `token-stats.json` keeps convenience aggregates, but not the exact
13
- * provider+model sum needed for the new table column.
14
- * - `request-log.jsonl` is the source of truth because every proxied request
15
- * records prompt and completion token counts with provider context.
12
+ * - `token-stats.json` is the only remaining source of truth for historical
13
+ * totals now that the older JSONL accounting pipeline has been removed.
16
14
  * - Startup-only parsing keeps runtime overhead negligible during TUI redraws.
17
15
  *
18
16
  * @functions
19
17
  * β†’ `buildProviderModelTokenKey` β€” creates a stable aggregation key
20
- * β†’ `loadTokenUsageByProviderModel` β€” reads request-log.jsonl and returns total tokens by provider+model
18
+ * β†’ `loadTokenUsageByProviderModel` β€” reads token snapshots and returns total tokens by provider+model
21
19
  * β†’ `formatTokenTotalCompact` β€” renders totals as raw ints or compact K / M strings with 2 decimals
22
20
  *
23
21
  * @exports buildProviderModelTokenKey, loadTokenUsageByProviderModel, formatTokenTotalCompact
24
- *
25
- * @see src/log-reader.js
26
22
  * @see src/render-table.js
27
23
  */
28
24
 
29
25
  import { readFileSync, existsSync } from 'node:fs'
30
26
  import { join } from 'node:path'
31
27
  import { homedir } from 'node:os'
32
- import { loadRecentLogs } from './log-reader.js'
33
28
 
34
29
  const DEFAULT_DATA_DIR = join(homedir(), '.free-coding-models')
35
30
  const STATS_FILE = join(DEFAULT_DATA_DIR, 'token-stats.json')
@@ -40,23 +35,11 @@ export function buildProviderModelTokenKey(providerKey, modelId) {
40
35
  return `${providerKey}::${modelId}`
41
36
  }
42
37
 
43
- // πŸ“– loadTokenUsageByProviderModel prioritizes token-stats.json for accurate
44
- // πŸ“– historical totals. If missing, it falls back to parsing the bounded log history.
45
- export function loadTokenUsageByProviderModel({ logFile, statsFile = STATS_FILE, limit = 50_000 } = {}) {
46
- // πŸ“– If a custom logFile is provided (Test Mode), ONLY use that file.
47
- if (logFile) {
48
- const testTotals = {}
49
- const rows = loadRecentLogs({ logFile, limit })
50
- for (const row of rows) {
51
- const key = buildProviderModelTokenKey(row.provider, row.model)
52
- testTotals[key] = (testTotals[key] || 0) + (Number(row.tokens) || 0)
53
- }
54
- return testTotals
55
- }
56
-
38
+ // πŸ“– loadTokenUsageByProviderModel reads the aggregated stats file produced by
39
+ // πŸ“– the quota/accounting pipeline. Missing or malformed files are treated as empty.
40
+ export function loadTokenUsageByProviderModel({ statsFile = STATS_FILE } = {}) {
57
41
  const totals = {}
58
42
 
59
- // πŸ“– Phase 1: Try to load from the aggregated stats file (canonical source for totals)
60
43
  try {
61
44
  if (existsSync(statsFile)) {
62
45
  const stats = JSON.parse(readFileSync(statsFile, 'utf8'))
@@ -78,24 +61,12 @@ export function loadTokenUsageByProviderModel({ logFile, statsFile = STATS_FILE,
78
61
  }
79
62
  }
80
63
  }
81
- } catch (err) {
82
- // πŸ“– Silently fall back to log parsing if stats file is corrupt or unreadable
83
- }
84
-
85
- // πŸ“– Phase 2: Supplement with recent log entries if totals are still empty
86
- // πŸ“– (e.g. fresh install or token-stats.json deleted)
87
- if (Object.keys(totals).length === 0) {
88
- const rows = loadRecentLogs({ limit })
89
- for (const row of rows) {
90
- const key = buildProviderModelTokenKey(row.provider, row.model)
91
- totals[key] = (totals[key] || 0) + (Number(row.tokens) || 0)
92
- }
93
- }
64
+ } catch {}
94
65
 
95
66
  return totals
96
67
  }
97
68
 
98
- // πŸ“– formatTokenTotalCompact keeps token counts readable in both the table and log view:
69
+ // πŸ“– formatTokenTotalCompact keeps token counts readable in the table:
99
70
  // πŸ“– 0-999 => raw integer, 1k-999k => N.NNk, 1m+ => N.NNM.
100
71
  export function formatTokenTotalCompact(totalTokens) {
101
72
  const safeTotal = Number(totalTokens) || 0