cc-viewer 1.7.6 → 1.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +20 -1
  2. package/cli.js +41 -31
  3. package/dist/assets/App-Bs6-c4Ov.css +1 -0
  4. package/dist/assets/App-DtsIVPu6.js +2 -0
  5. package/dist/assets/{MdxEditorPanel-BQ5MgI4D.js → MdxEditorPanel-DWPAEnpm.js} +1 -1
  6. package/dist/assets/Mobile-DHwMZG4N.js +1 -0
  7. package/dist/assets/ProxyStatsModal-D9lTz0-T.js +1 -0
  8. package/dist/assets/ProxyStatsModal-Dz5GuoUa.css +1 -0
  9. package/dist/assets/abnfDiagram-VRR7QNED-9ax220Vv.js +1 -0
  10. package/dist/assets/arc-Cshrl52z.js +1 -0
  11. package/dist/assets/architectureDiagram-ZJ3FMSHR-QtERolgF.js +1 -0
  12. package/dist/assets/blockDiagram-677ZJIJ3-BaCd4R0Z.js +1 -0
  13. package/dist/assets/{c4Diagram-AHTNJAMY-C7JB9PqP.js → c4Diagram-LMCZKHZV-C2QvkF2i.js} +1 -1
  14. package/dist/assets/{channel-BnzX9zBS.js → channel-ClOiNc35.js} +1 -1
  15. package/dist/assets/{chunk-QZHKN3VN-CNEAHnZV.js → chunk-2Q5K7J3B-BvZ7-Ft4.js} +1 -1
  16. package/dist/assets/{chunk-YZCP3GAM-BOlts_Dm.js → chunk-32BRIVSS-Dg3PlJIt.js} +1 -1
  17. package/dist/assets/{chunk-FMBD7UC4-CGL9Lo51.js → chunk-5VM5RSS4-CMJWLQsQ.js} +1 -1
  18. package/dist/assets/chunk-EX3LRPZG-CmoC-M2G.js +1 -0
  19. package/dist/assets/{chunk-4BX2VUAB-dVKiUtqe.js → chunk-JWPE2WC7-D5RR93-U.js} +1 -1
  20. package/dist/assets/chunk-MOJQB5TN-JXgBLjCY.js +1 -0
  21. package/dist/assets/chunk-RYQCIY6F-gT-v276m.js +1 -0
  22. package/dist/assets/chunk-V7JOEXUC-Cl-E_fxl.js +1 -0
  23. package/dist/assets/{chunk-EDXVE4YY-sk9g7szp.js → chunk-VR4S4FIN-BR0zG1Ep.js} +1 -1
  24. package/dist/assets/{chunk-55IACEB6-BZuKI6JQ.js → chunk-XXDRQBXY-Blw-tThR.js} +1 -1
  25. package/dist/assets/classDiagram-OUVF2IWQ-ClkcbuHu.js +1 -0
  26. package/dist/assets/classDiagram-v2-EOCWNBFH-ClkcbuHu.js +1 -0
  27. package/dist/assets/{cose-bilkent-S5V4N54A-DH4_YiGi.js → cose-bilkent-JH36ORCC-BnDDyyuN.js} +1 -1
  28. package/dist/assets/cynefin-VYW2F7L2-Ch8_Ihra.js +15 -0
  29. package/dist/assets/cynefinDiagram-TSTJHNR4-CuX_xTSU.js +1 -0
  30. package/dist/assets/cytoscape.esm-D0POlJZ1.js +9 -0
  31. package/dist/assets/dagre-VKFMJZFB-DHDO7DUw.js +1 -0
  32. package/dist/assets/diagram-FQU43EPY-CxDVpL25.js +1 -0
  33. package/dist/assets/diagram-G47NLZAW-DHX7L6pT.js +1 -0
  34. package/dist/assets/diagram-NH7WQ7WH-HlwtIdWA.js +1 -0
  35. package/dist/assets/diagram-OA4YK3LP-BnTSu9CG.js +1 -0
  36. package/dist/assets/diagram-WEI45ONY-C712tBJk.js +1 -0
  37. package/dist/assets/ebnfDiagram-CCIWWBDH-DzMcH3cz.js +1 -0
  38. package/dist/assets/erDiagram-Q63AITRT-0fyhQG92.js +1 -0
  39. package/dist/assets/flowDiagram-23GEKE2U-lgfs10IO.js +1 -0
  40. package/dist/assets/{ganttDiagram-T4ZO3ILL-BL_XQ958.js → ganttDiagram-NO4QXBWP-_4nnZ3ta.js} +1 -1
  41. package/dist/assets/gitGraphDiagram-IHSO6WYX-DrgEKJkZ.js +1 -0
  42. package/dist/assets/graph-CnJdn47S.js +1 -0
  43. package/dist/assets/{index-CzPHj3iF.js → index-7VeS_xrl.js} +1 -1
  44. package/dist/assets/{index-BgW_nP_0.js → index-BJ9Xm7_G.js} +1 -1
  45. package/dist/assets/{index-B3-JUU7J.js → index-BiYKFU6w.js} +1 -1
  46. package/dist/assets/index-By8aqIvN.js +2 -0
  47. package/dist/assets/{index-Do5xNTOT.js → index-CBix0zSu.js} +1 -1
  48. package/dist/assets/{index-DDlAMOiV.js → index-CPAQ767L.js} +1 -1
  49. package/dist/assets/{index-LjVy-Pzg.js → index-DuUUHxox.js} +1 -1
  50. package/dist/assets/{index-vImWXiMM.js → index-_pxC-jz_.js} +1 -1
  51. package/dist/assets/infoDiagram-FWYZ7A6U-Cki8RIbq.js +1 -0
  52. package/dist/assets/{ishikawaDiagram-UXIWVN3A-D3odpv9_.js → ishikawaDiagram-FXEZZL3T-DHHu_UGe.js} +1 -1
  53. package/dist/assets/{journeyDiagram-VCZTEJTY-Dj3NaOWF.js → journeyDiagram-5HDEW3XC-Bx06Sp3y.js} +1 -1
  54. package/dist/assets/{jszip.min-GmVScWAV.js → jszip.min-Bn5J9HN-.js} +1 -1
  55. package/dist/assets/{kanban-definition-6JOO6SKY-GKjJaLwD.js → kanban-definition-HUTT4EX6-BuEuKkQp.js} +1 -1
  56. package/dist/assets/layout-CCUE9ZRw.js +1 -0
  57. package/dist/assets/{linear-CLk9nvqn.js → linear-BJRZNsdX.js} +1 -1
  58. package/dist/assets/map-DQeg2hXV.js +1 -0
  59. package/dist/assets/mermaid.core-DH-sQJgr.js +7 -0
  60. package/dist/assets/{mindmap-definition-QFDTVHPH-DgDD6Jbu.js → mindmap-definition-LN4V7U3C-DYtgJ2f4.js} +1 -1
  61. package/dist/assets/pegDiagram-2B236MQR-BVN6OIEN.js +1 -0
  62. package/dist/assets/pieDiagram-ENE6RG2P-mVeQtQTD.js +1 -0
  63. package/dist/assets/quadrantDiagram-ABIIQ3AL-Croqf1qH.js +1 -0
  64. package/dist/assets/railroadDiagram-RFXS5EU6-jkGuNfMY.js +1 -0
  65. package/dist/assets/{requirementDiagram-MS252O5E-CtBOCtbk.js → requirementDiagram-TGXJPOKE-Cwb2FU3N.js} +1 -1
  66. package/dist/assets/sankeyDiagram-HTMAVEWB-D-HWOuX0.js +1 -0
  67. package/dist/assets/seqResourceLoaders-C-pxCjNh.js +2 -0
  68. package/dist/assets/sequenceDiagram-DBY2YBRQ-Bh3XgPxy.js +1 -0
  69. package/dist/assets/sizeCapture-X5ZJPWSS-C_acEkFf.js +1 -0
  70. package/dist/assets/stateDiagram-2N3HPSRC-CmbRj6R7.js +1 -0
  71. package/dist/assets/stateDiagram-v2-6OUMAXLB-ClrX-Ofc.js +1 -0
  72. package/dist/assets/swimlanes-5IMT3BWC-CIfoRrLM.js +2 -0
  73. package/dist/assets/swimlanesDiagram-G3AALYLV-DZuW0-dQ.js +1 -0
  74. package/dist/assets/{timeline-definition-GMOUNBTQ-B5cPasy_.js → timeline-definition-FHXFAJF6-CK8MkCLz.js} +1 -1
  75. package/dist/assets/vendor-antd-DNUTLr77.js +17 -0
  76. package/dist/assets/{vendor-codemirror-_NbrtdQc.js → vendor-codemirror-6ck0hLRU.js} +1 -1
  77. package/dist/assets/vendor-markdown-DBlfs4dR.js +3 -0
  78. package/dist/assets/vendor-mdxeditor-DT0rad22.js +2 -0
  79. package/dist/assets/vendor-qrcode-CPmWG6hm.js +6 -0
  80. package/dist/assets/vendor-virtuoso-YwK7i7L6.js +1 -0
  81. package/dist/assets/{vennDiagram-DHZGUBPP-a_ybZDST.js → vennDiagram-L72KCM5P-CWA2Gdi_.js} +1 -1
  82. package/dist/assets/wardleyDiagram-EHGQE667-U22kgZ8v.js +1 -0
  83. package/dist/assets/xychartDiagram-FW5EYKEG-BiGsMUKC.js +1 -0
  84. package/dist/index.html +4 -4
  85. package/findcc.js +281 -5
  86. package/package.json +1 -1
  87. package/server/lib/proxy-stats.js +75 -32
  88. package/server/pty-manager.js +3 -8
  89. package/server/routes/preferences.js +71 -1
  90. package/server/system-prompt-templates/presets/index.json +8 -0
  91. package/server/system-prompt-templates/presets/kimi-k3.md +68 -0
  92. package/src/utils/helpers.js +6 -1
  93. package/dist/assets/App-3VM471BO.js +0 -2
  94. package/dist/assets/App-CCDFxj11.css +0 -1
  95. package/dist/assets/Mobile-wwcXmnrU.js +0 -1
  96. package/dist/assets/UnifiedProxyRetryPage-BpcKIn9N.js +0 -1
  97. package/dist/assets/UnifiedProxyRetryPage-CFz4vOH5.css +0 -1
  98. package/dist/assets/_baseUniq-yiNE5Ckw.js +0 -1
  99. package/dist/assets/arc-M-eBYYbN.js +0 -1
  100. package/dist/assets/architectureDiagram-Q4EWVU46-CtweeLKi.js +0 -1
  101. package/dist/assets/blockDiagram-DXYQGD6D-DCVQ-go_.js +0 -1
  102. package/dist/assets/chunk-4TB4RGXK-CQjD9-fD.js +0 -1
  103. package/dist/assets/chunk-OYMX7WX6-DaN0AdQD.js +0 -1
  104. package/dist/assets/classDiagram-6PBFFD2Q-CumOMdO7.js +0 -1
  105. package/dist/assets/classDiagram-v2-HSJHXN6E-CumOMdO7.js +0 -1
  106. package/dist/assets/clone-C3SZDzs1.js +0 -1
  107. package/dist/assets/cytoscape.esm-CT1mZavR.js +0 -9
  108. package/dist/assets/dagre-KV5264BT-RqusStYI.js +0 -1
  109. package/dist/assets/diagram-5BDNPKRD-8haVbBnt.js +0 -1
  110. package/dist/assets/diagram-G4DWMVQ6-CsrJNUhT.js +0 -1
  111. package/dist/assets/diagram-MMDJMWI5-1luPOM38.js +0 -1
  112. package/dist/assets/diagram-TYMM5635-Dad1XR3W.js +0 -1
  113. package/dist/assets/erDiagram-SMLLAGMA-Br6-kbp9.js +0 -1
  114. package/dist/assets/flowDiagram-DWJPFMVM-DuX1mbA7.js +0 -1
  115. package/dist/assets/gitGraphDiagram-UUTBAWPF-DzvW5FEs.js +0 -1
  116. package/dist/assets/graph-CbUmu0Oe.js +0 -1
  117. package/dist/assets/index-BjNLFJ0z.js +0 -2
  118. package/dist/assets/infoDiagram-42DDH7IO-DGsgRji7.js +0 -1
  119. package/dist/assets/layout-Bhk66H0F.js +0 -1
  120. package/dist/assets/mermaid.core-DH-GfeNS.js +0 -7
  121. package/dist/assets/min-DQftOGfo.js +0 -1
  122. package/dist/assets/pieDiagram-DEJITSTG-jy1yFaxA.js +0 -1
  123. package/dist/assets/quadrantDiagram-34T5L4WZ-BdvMwrSD.js +0 -1
  124. package/dist/assets/sankeyDiagram-XADWPNL6-DEFlQp-h.js +0 -1
  125. package/dist/assets/seqResourceLoaders-CDRtUSxI.js +0 -2
  126. package/dist/assets/sequenceDiagram-FGHM5R23-0F2R8Ky0.js +0 -1
  127. package/dist/assets/stateDiagram-FHFEXIEX-D4xOwcSW.js +0 -1
  128. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DSf_BSF8.js +0 -1
  129. package/dist/assets/vendor-antd-DeqwrDxf.js +0 -17
  130. package/dist/assets/vendor-markdown-DOJHsAxX.js +0 -3
  131. package/dist/assets/vendor-mdxeditor-DG0Nyxw5.js +0 -2
  132. package/dist/assets/vendor-qrcode-B3HN--HO.js +0 -6
  133. package/dist/assets/vendor-virtuoso-BvrezPQx.js +0 -1
  134. package/dist/assets/wardley-RL74JXVD-CKla3KF8.js +0 -1
  135. package/dist/assets/wardleyDiagram-NUSXRM2D-BKke2aih.js +0 -1
  136. package/dist/assets/xychartDiagram-5P7HB3ND-CozWtcdf.js +0 -1
@@ -9,6 +9,7 @@
9
9
  import { mkdirSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
  import { AsyncWriteQueue } from './async-write-queue.js';
12
+ import { reportSwallowed } from './error-report.js';
12
13
 
13
14
  // ── Detail record schema ──────────────────────────────────────────────
14
15
  // Appends one JSON line after each proxied LLM API request completes. Fields align with llm-retry-proxy's retry records.
@@ -120,7 +121,11 @@ export function appendRecord(filePath, record, onDone) {
120
121
  try {
121
122
  mkdirSync(dir, { recursive: true });
122
123
  _dirsEnsured.add(dir);
123
- } catch { /* No permission — the queued append will surface the failure as a silent no-op */ }
124
+ } catch (err) {
125
+ // No permission / read-only fs — the queued append will surface the failure
126
+ // as a silent no-op. Record it so the cause isn't fully invisible.
127
+ reportSwallowed('proxyStats.mkdir', err);
128
+ }
124
129
  }
125
130
  _statsWriteQueue.appendTo(filePath, JSON.stringify(record) + '\n', onDone);
126
131
  }
@@ -147,9 +152,9 @@ export function emitProxyStatsUpdate(fileName) {
147
152
  try {
148
153
  _statsUpdateListener(fileName);
149
154
  } catch (err) {
150
- if (process.env.CCV_DEBUG) {
151
- console.error('[CC-Viewer Proxy] proxy stats notify failed:', err?.message);
152
- }
155
+ // A throwing listener would silently break stats propagation (the SSE push,
156
+ // recompute trigger, etc.). Record it instead of swallowing silently.
157
+ reportSwallowed('proxyStats.emit', err);
153
158
  }
154
159
  }
155
160
 
@@ -196,6 +201,37 @@ export function computeStreak(records) {
196
201
  return { current: { type: curType || 'success', count: curCount }, worstFailure };
197
202
  }
198
203
 
204
+ /**
205
+ * Retry-burden bucket definitions: turns the raw retry-count distribution into
206
+ * actionable buckets (0 / 1-5 / 6-20 / 21-50 / >50). Aligned with llm-retry-proxy's
207
+ * renderBurdenChart buckets. `key` is the stable i18n id (ui.proxyStats.retryBurdenBuckets.<key>).
208
+ *
209
+ * `range` is [min, max] with `max: null` meaning "open ended". Deliberately not
210
+ * `Infinity`: this table's shape is mirrored into the stats JSON, and
211
+ * JSON.stringify(Infinity) is `null` anyway — spelling it null keeps the
212
+ * in-process value and the serialized value identical.
213
+ *
214
+ * Exposed for tests that want to assert the frozen bucket contract without re-typing
215
+ * the boundaries.
216
+ */
217
+ export const RETRY_BURDEN_BUCKETS = [
218
+ { key: '0', range: [0, 0] },
219
+ { key: '1_5', range: [1, 5] },
220
+ { key: '6_20', range: [6, 20] },
221
+ { key: '21_50', range: [21, 50] },
222
+ { key: 'over50', range: [51, null] },
223
+ ];
224
+
225
+ // Bump the bucket whose range covers `retries`. Buckets are ordered low→high
226
+ // and non-overlapping, so the first match wins; returns void (mutates `counts`).
227
+ // A null upper bound is open ended.
228
+ function bumpBurden(counts, retries) {
229
+ for (const b of counts) {
230
+ const [min, max] = b.range;
231
+ if (retries >= min && (max === null || retries <= max)) { b.count++; return; }
232
+ }
233
+ }
234
+
199
235
  /**
200
236
  * Aggregate a batch of detail records into a stats structure. Called by stats-worker, also used directly by /api/proxy-stats.
201
237
  *
@@ -233,6 +269,8 @@ export function aggregateRecords(records, opts = {}) {
233
269
  const byModelMap = new Map();
234
270
  const byPathMap = new Map();
235
271
  const byProfileMap = new Map();
272
+ // Retry-burden buckets bumped inline during the main loop (avoids a second O(n) pass).
273
+ const retryBurdenCounts = RETRY_BURDEN_BUCKETS.map(b => ({ ...b, count: 0 }));
236
274
  let slowest = null;
237
275
  let fastest = null;
238
276
 
@@ -249,21 +287,22 @@ export function aggregateRecords(records, opts = {}) {
249
287
  }
250
288
  durations.push(dur);
251
289
  retryDist.set(retries, (retryDist.get(retries) || 0) + 1);
290
+ bumpBurden(retryBurdenCounts, retries);
252
291
  for (const c of (r.retry_codes || [])) {
253
292
  retryCodeCounts.set(c, (retryCodeCounts.get(c) || 0) + 1);
254
293
  }
255
294
  // by model
256
295
  const model = r.model || '(unknown)';
257
296
  if (!byModelMap.has(model)) byModelMap.set(model, newBucket());
258
- accBucket(byModelMap.get(model), retries, ok, dur);
297
+ accBucket(byModelMap.get(model), retries, ok, dur, r.retry_codes);
259
298
  // by path
260
299
  const path = r.path || '/';
261
300
  if (!byPathMap.has(path)) byPathMap.set(path, newBucket());
262
- accBucket(byPathMap.get(path), retries, ok, dur);
301
+ accBucket(byPathMap.get(path), retries, ok, dur, r.retry_codes);
263
302
  // by profile
264
303
  const profileKey = r.profile_id || 'default';
265
304
  if (!byProfileMap.has(profileKey)) byProfileMap.set(profileKey, { ...newBucket(), profile_name: r.profile_name || 'Default' });
266
- accBucket(byProfileMap.get(profileKey), retries, ok, dur);
305
+ accBucket(byProfileMap.get(profileKey), retries, ok, dur, r.retry_codes);
267
306
  // slowest / fastest (fastest only counts successes with dur>0)
268
307
  const candidate = { ts: r.ts, path, model, attempts: r.attempts, retries, duration_ms: dur, final_status: r.final_status, retry_codes: r.retry_codes || [] };
269
308
  if (!slowest || dur > slowest.duration_ms) slowest = candidate;
@@ -301,10 +340,11 @@ export function aggregateRecords(records, opts = {}) {
301
340
  },
302
341
  byModel: mapToSortedArray(byModelMap, 'model'),
303
342
  byPath: mapToSortedArray(byPathMap, 'path').slice(0, 10), // Top 10 path
304
- byProfile: mapToSortedArrayWithExtra(byProfileMap, 'profile_id', 'profile_name'),
343
+ byProfile: mapToSortedArray(byProfileMap, 'profile_id', 'profile_name'),
305
344
  retryDistribution: [...retryDist.entries()]
306
345
  .sort((a, b) => a[0] - b[0])
307
346
  .map(([retries, count]) => ({ retries, count })),
347
+ retryBurden: retryBurdenCounts,
308
348
  retryCodes: [...retryCodeCounts.entries()]
309
349
  .sort((a, b) => b[1] - a[1])
310
350
  .map(([code, count]) => ({ code, count })),
@@ -315,10 +355,10 @@ export function aggregateRecords(records, opts = {}) {
315
355
  }
316
356
 
317
357
  function newBucket() {
318
- return { requests: 0, retries: 0, succeeded: 0, firstOk: 0, failed: 0, durations: [] };
358
+ return { requests: 0, retries: 0, succeeded: 0, firstOk: 0, failed: 0, durations: [], retryCodeCounts: new Map() };
319
359
  }
320
360
 
321
- function accBucket(b, retries, ok, dur) {
361
+ function accBucket(b, retries, ok, dur, retryCodes) {
322
362
  b.requests++;
323
363
  b.retries += retries;
324
364
  if (ok) {
@@ -328,46 +368,48 @@ function accBucket(b, retries, ok, dur) {
328
368
  b.failed++;
329
369
  }
330
370
  b.durations.push(dur);
371
+ for (const c of (retryCodes || [])) {
372
+ if (Number.isFinite(c)) b.retryCodeCounts.set(c, (b.retryCodeCounts.get(c) || 0) + 1);
373
+ }
331
374
  }
332
375
 
333
- function mapToSortedArray(map, keyName) {
334
- const arr = [];
335
- for (const [k, b] of map.entries()) {
336
- b.durations.sort((a, c) => a - c);
337
- const entry = {
338
- [keyName]: k,
339
- requests: b.requests,
340
- retries: b.retries,
341
- succeeded: b.succeeded,
342
- firstOk: b.firstOk,
343
- failed: b.failed,
344
- availabilityPct: b.requests ? round2(b.succeeded / b.requests * 100) : 0,
345
- upstreamAvailabilityPct: b.requests ? round2(b.firstOk / b.requests * 100) : 0,
346
- p95Ms: round3(percentile(b.durations, 0.95)),
347
- };
348
- arr.push(entry);
349
- }
350
- arr.sort((a, b) => b.requests - a.requests);
351
- return arr;
376
+ // Finalize a bucket's per-bucket retry-code map into top-5 array + dominant fail code.
377
+ function finalizeRetryCodes(b) {
378
+ const arr = [...(b.retryCodeCounts?.entries() || [])]
379
+ .sort((a, c) => c[1] - a[1])
380
+ .slice(0, 5)
381
+ .map(([code, count]) => ({ code, count }));
382
+ const top = arr[0] || null;
383
+ return {
384
+ retryCodeCounts: arr,
385
+ dominantFailStatus: top ? top.code : null,
386
+ dominantFailCount: top ? top.count : 0,
387
+ };
352
388
  }
353
389
 
354
- // byProfile-specific: the bucket carries an extra profile_name field, included in the output
355
- function mapToSortedArrayWithExtra(map, keyName, extraName) {
390
+ // Flatten a dimension's bucket map into a sorted entry array.
391
+ // `extraName` (optional, used by byProfile) emits an extra `[extraName]: bucket[extraName] || k`
392
+ // field — the only structural difference between the byModel/byPath and byProfile outputs.
393
+ function mapToSortedArray(map, keyName, extraName) {
356
394
  const arr = [];
357
395
  for (const [k, b] of map.entries()) {
358
396
  b.durations.sort((a, c) => a - c);
397
+ const fin = finalizeRetryCodes(b);
359
398
  const entry = {
360
399
  [keyName]: k,
361
- [extraName]: b[extraName] || k,
362
400
  requests: b.requests,
363
401
  retries: b.retries,
364
402
  succeeded: b.succeeded,
365
403
  firstOk: b.firstOk,
366
404
  failed: b.failed,
405
+ retryCodeCounts: fin.retryCodeCounts,
406
+ dominantFailStatus: fin.dominantFailStatus,
407
+ dominantFailCount: fin.dominantFailCount,
367
408
  availabilityPct: b.requests ? round2(b.succeeded / b.requests * 100) : 0,
368
409
  upstreamAvailabilityPct: b.requests ? round2(b.firstOk / b.requests * 100) : 0,
369
410
  p95Ms: round3(percentile(b.durations, 0.95)),
370
411
  };
412
+ if (extraName) entry[extraName] = b[extraName] || k;
371
413
  arr.push(entry);
372
414
  }
373
415
  arr.sort((a, b) => b.requests - a.requests);
@@ -386,6 +428,7 @@ function emptyStats() {
386
428
  byPath: [],
387
429
  byProfile: [],
388
430
  retryDistribution: [],
431
+ retryBurden: [],
389
432
  retryCodes: [],
390
433
  slowest: null,
391
434
  fastest: null,
@@ -231,14 +231,9 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
231
231
  }
232
232
 
233
233
  const env = { ...process.env };
234
- // CodeFuse owns and versions the Claude binary used by `cfuse --ccv`. Keep
235
- // that managed 2.1.x binary from invoking Claude Code's independent updater;
236
- // upgrades must come from CodeFuse so the enterprise-approved version cannot
237
- // be silently replaced during a CCV session.
238
- const normalizedClaudePath = String(claudePath || '').replace(/\\/g, '/');
239
- if (normalizedClaudePath.includes('/.codefuse/fuse/engine/bin/claude/')) {
240
- env.DISABLE_AUTOUPDATER = '1';
241
- }
234
+ // CCV owns executable selection. Never let the selected Claude replace itself
235
+ // behind that configuration (especially on enterprise allowlisted machines).
236
+ env.DISABLE_AUTOUPDATER = '1';
242
237
  env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${proxyPort}`;
243
238
  env.CCV_PROXY_MODE = '1'; // 告诉 interceptor.js 不要再启动 server
244
239
  env.CCV_LOG_DIR = LOG_DIR; // 让 fork 出的 Claude Code 进程找到同一份 profile.json 等资源
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
- import { LOG_DIR, setLogDir, getClaudeConfigDir } from '../../findcc.js';
5
+ import { LOG_DIR, setLogDir, getClaudeConfigDir, discoverClaudeExecutables, resolveExplicitClaudePath, CLAUDE_EXECUTABLE_PREF_KEY } from '../../findcc.js';
6
6
  import { PROFILE_PATH, _defaultConfig, getActiveProfileId, setActiveProfileForWorkspace, _loadProxyProfile, RETRY_CONFIG_PATH, _retryConfigState, _loadRetryConfigState } from '../interceptor.js';
7
7
  import { migrateProxyProfileList } from '../lib/interceptor-core.js';
8
8
  import { DEFAULT_RETRY_CONFIG, validateRetryConfig, resolveRetryConfig } from '../lib/proxy-retry.js';
@@ -25,6 +25,22 @@ function stripImConfigs(obj) {
25
25
  if (obj) for (const id of listPlatforms()) delete obj[id];
26
26
  }
27
27
 
28
+ // Loopback identifies the socket peer, not the browser page that initiated a
29
+ // request. Since the server intentionally has permissive CORS for legacy APIs,
30
+ // sensitive executable-selection endpoints need their own same-origin guard.
31
+ function isSameOriginBrowserRequest(req, parsedUrl) {
32
+ const origin = req.headers?.origin;
33
+ if (!origin) return true; // CLI/native clients do not send Origin.
34
+ if (req.headers?.['sec-fetch-site'] === 'cross-site') return false;
35
+ try {
36
+ const originUrl = new URL(origin);
37
+ if (parsedUrl?.origin) return originUrl.origin.toLowerCase() === parsedUrl.origin.toLowerCase();
38
+ return originUrl.host.toLowerCase() === String(req.headers?.host || '').toLowerCase();
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
28
44
  // /theme 选择器特征:选项文案高特异、不太可能出现在普通生成输出里(ESC 兜底的门控签名)
29
45
  const THEME_PICKER_RE = /Auto \(match terminal\)|colorblind-friendly/;
30
46
 
@@ -41,6 +57,9 @@ function preferencesGet(req, res, parsedUrl, isLocal, deps) {
41
57
  // 全局 auth 与每个项目的 authByProject 覆盖都要剥离(后者同样含明文密码)。
42
58
  delete prefs.auth;
43
59
  delete prefs.authByProject;
60
+ if (!isLocal || !isSameOriginBrowserRequest(req, parsedUrl)) {
61
+ delete prefs[CLAUDE_EXECUTABLE_PREF_KEY];
62
+ }
44
63
  stripImConfigs(prefs); // dingtalk / feishu / … — admin-only, never to a LAN client
45
64
  // 项目独立配置(多人共用一台 server 时按项目隔离偏好):非本机(LAN)客户端若当前项目有 fork,
46
65
  // 解析出该项目的有效偏好;本机(admin)始终看全局。forks blob 绝不下发,仅以 _projectPrefsKeys
@@ -99,6 +118,30 @@ function preferencesPost(req, res, parsedUrl, isLocal, deps) {
99
118
  // 任意项目的覆盖,绕过 admin 门禁。
100
119
  delete incoming.auth;
101
120
  delete incoming.authByProject;
121
+ // Claude executable selection controls which local program CCV executes.
122
+ // It is machine-global and therefore writable only by the loopback admin,
123
+ // never by a remote/project-scoped preferences client.
124
+ if (!isLocal) {
125
+ delete incoming[CLAUDE_EXECUTABLE_PREF_KEY];
126
+ } else if (Object.prototype.hasOwnProperty.call(incoming, CLAUDE_EXECUTABLE_PREF_KEY)) {
127
+ const value = incoming[CLAUDE_EXECUTABLE_PREF_KEY];
128
+ if (!isSameOriginBrowserRequest(req, parsedUrl)) {
129
+ res.writeHead(403, { 'Content-Type': 'application/json' });
130
+ res.end(JSON.stringify({ error: 'Same-origin request required' }));
131
+ return;
132
+ }
133
+ if (typeof value === 'string' && value.trim()) {
134
+ const resolved = resolveExplicitClaudePath(value.trim().slice(0, 4096));
135
+ if (!resolved) {
136
+ res.writeHead(422, { 'Content-Type': 'application/json' });
137
+ res.end(JSON.stringify({ error: 'Claude executable path is not launchable' }));
138
+ return;
139
+ }
140
+ incoming[CLAUDE_EXECUTABLE_PREF_KEY] = resolved.path;
141
+ } else {
142
+ incoming[CLAUDE_EXECUTABLE_PREF_KEY] = null;
143
+ }
144
+ }
102
145
  // prefsByProject(项目独立配置 fork)只能经 admin-only 的 /api/project-prefs/* 修改;元字段
103
146
  // (_isLocal/_projectScoped/…)仅 GET 回包,绝不让它们经 /api/preferences 落盘污染。
104
147
  delete incoming.prefsByProject;
@@ -168,6 +211,9 @@ function preferencesPost(req, res, parsedUrl, isLocal, deps) {
168
211
  delete prefs.auth;
169
212
  delete prefs.authByProject;
170
213
  delete prefs.prefsByProject; // fork blob 绝不回显(与 GET 一致)
214
+ if (!isLocal || !isSameOriginBrowserRequest(req, parsedUrl)) {
215
+ delete prefs[CLAUDE_EXECUTABLE_PREF_KEY];
216
+ }
171
217
  stripImConfigs(prefs);
172
218
  prefs.logDir = LOG_DIR;
173
219
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -181,6 +227,29 @@ function preferencesPost(req, res, parsedUrl, isLocal, deps) {
181
227
  });
182
228
  }
183
229
 
230
+ function claudeExecutablesGet(req, res, parsedUrl, isLocal, deps) {
231
+ if (!isLocal || !isSameOriginBrowserRequest(req, parsedUrl)) {
232
+ res.writeHead(403, { 'Content-Type': 'application/json' });
233
+ res.end(JSON.stringify({ error: isLocal ? 'Same-origin request required' : 'Loopback only' }));
234
+ return;
235
+ }
236
+ let configuredPath = null;
237
+ try {
238
+ const prefs = readPrefsRaw(deps.getPrefsFile());
239
+ const value = prefs?.[CLAUDE_EXECUTABLE_PREF_KEY];
240
+ if (typeof value === 'string' && value.trim()) configuredPath = value.trim();
241
+ } catch { }
242
+ const candidates = discoverClaudeExecutables({ configuredPath }).map(({
243
+ path, source, version, isNpmVersion,
244
+ }) => ({ path, source, version, isNpmVersion }));
245
+ res.writeHead(200, { 'Content-Type': 'application/json' });
246
+ res.end(JSON.stringify({
247
+ configuredPath,
248
+ effectivePath: process.env.CCV_CLAUDE_EXECUTABLE || null,
249
+ candidates,
250
+ }));
251
+ }
252
+
184
253
  function claudeSettingsGet(req, res, parsedUrl, isLocal, deps) {
185
254
  res.writeHead(200, { 'Content-Type': 'application/json' });
186
255
  const fileEnv = deps.claudeSettings.env || {};
@@ -481,6 +550,7 @@ async function ccswitchImportPost(req, res, _parsedUrl, isLocal, deps) {
481
550
  export const preferencesRoutes = [
482
551
  { method: 'GET', match: 'exact', path: '/api/preferences', handler: preferencesGet },
483
552
  { method: 'POST', match: 'exact', path: '/api/preferences', handler: preferencesPost },
553
+ { method: 'GET', match: 'exact', path: '/api/claude-executables', handler: claudeExecutablesGet },
484
554
  { method: 'GET', match: 'exact', path: '/api/claude-settings', handler: claudeSettingsGet },
485
555
  { method: 'POST', match: 'exact', path: '/api/claude-settings', handler: claudeSettingsPost },
486
556
  { method: 'GET', match: 'exact', path: '/api/proxy-profiles', handler: proxyProfilesGet },
@@ -41,6 +41,14 @@
41
41
  "description": "Coding agent prompt tuned for Kimi K2.7 Code: deliver exactly what was asked (no over-engineering), act through tools, explain briefly.",
42
42
  "match": "kimi-k2.7-code",
43
43
  "defaultMode": "override"
44
+ },
45
+ {
46
+ "id": "kimi-k3",
47
+ "title": "Kimi K3",
48
+ "file": "kimi-k3.md",
49
+ "description": "Coding agent prompt tuned for Kimi K3: deliver exactly what was asked (no over-engineering), act through tools, explain briefly.",
50
+ "match": "kimi-k3",
51
+ "defaultMode": "override"
44
52
  }
45
53
  ]
46
54
  }
@@ -0,0 +1,68 @@
1
+ <!--
2
+ Preset: kimi-k3 (category: Global)
3
+ Self-contained template: a tuned preamble plus its own dynamic sections
4
+ (a boundary marker, an OS-only # Environment, and a verbatim # Memory; no Git).
5
+ Edit this file directly. Forked from the Kimi K2.7 Code preset and tuned for
6
+ Kimi K3: keep scope tight, act through tools, and explain briefly.
7
+ -->
8
+
9
+ You are ${model.name}, an interactive coding agent that helps users with software engineering tasks by taking action. Use the instructions below and the tools available to you to make real changes in the user's project.
10
+
11
+ IMPORTANT: Assist with defensive software engineering work. Refuse requests to deploy, facilitate, or hide malware, credential theft, destructive behavior, or other cyber abuse.
12
+ IMPORTANT: Never generate or guess URLs unless you are confident they help the user with programming. Prefer URLs the user provides or ones found in local files.
13
+
14
+ # Doing tasks
15
+ - Read the relevant files before acting or answering; ground every claim and change in code you have actually looked at.
16
+ - When a request could be read either as a question or as a change to make, treat it as a task and carry it out. When the user clearly asks a question or how to approach something, answer that first.
17
+ - Deliver exactly what was asked and nothing more: no unrequested CLI wrappers, configuration options, logging, progress output, or abstractions. This is very important to your performance.
18
+ - Never assume a library or framework is available — check the project's manifest or neighboring files before using it.
19
+ - Do not introduce security vulnerabilities (injection, XSS, path traversal, and the rest of the OWASP top 10); fix insecure code you write immediately.
20
+ - Iterate to green: run the relevant tests or code path, read the failure, fix the cause, and run again.
21
+ - When an approach is blocked, try a different one before handing the problem back to the user.
22
+
23
+ # Using tools
24
+ - A change shown only in your reply does not exist on disk — create and edit files with tools, never by pasting code into the conversation.
25
+ - Do not narrate tool calls; the calls themselves show the user what you are doing.
26
+ - Send independent tool calls together in one response instead of one at a time.
27
+ - Track multi-step work explicitly and mark each step done as you finish it.
28
+ - Tool results and user messages may include <system-reminder> tags. They carry information from the system, not from the user.
29
+
30
+ # Executing actions with care
31
+ Consider the reversibility and blast radius of each action. Local, reversible actions (editing files, running tests) are fine to take freely. For hard-to-reverse or shared-system actions — deleting files or branches, force-pushing, sending messages, posting to external services — confirm with the user first. Never run git mutations (commit, push, reset, rebase) unless the user explicitly asks, and re-confirm each time even if the user approved one earlier. Investigate unexpected state before overwriting it.
32
+
33
+ # Tone and style
34
+ - Be thorough in your actions, not in your explanations: report what changed and where, and stop.
35
+ - Keep text output brief and direct; lead with the answer or action. No filler, and no emojis unless the user asks.
36
+ - Reference code with the `file_path:line_number` pattern.
37
+ - Always respond in the same language as the user, using the `${environment.lang}` locale when the language is not otherwise clear.
38
+
39
+ __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__
40
+
41
+ # Environment
42
+ - Platform: ${os.platform}
43
+ - OS Version: ${os.version}
44
+ - Architecture: ${os.arch}
45
+ - Shell: ${os.shell}
46
+
47
+ # Memory
48
+
49
+ You have a persistent file-based memory at `${memory.dir}`. This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence). Each memory is one file holding one fact, with frontmatter:
50
+
51
+ ```markdown
52
+ ---
53
+ name: <short-kebab-case-slug>
54
+ description: <one-line summary — used to decide relevance during recall>
55
+ metadata:
56
+ type: user | feedback | project | reference
57
+ ---
58
+
59
+ <the fact; for feedback/project, follow with **Why:** and **How to apply:** lines. Link related memories with [[their-name]].>
60
+ ```
61
+
62
+ In the body, link to related memories with `[[name]]`, where `name` is the other memory's `name:` slug. Link liberally — a `[[name]]` that doesn't match an existing memory yet is fine; it marks something worth writing later, not an error.
63
+
64
+ `user` — who the user is (role, expertise, preferences). `feedback` — guidance the user has given on how you should work, both corrections and confirmed approaches; include the why. `project` — ongoing work, goals, or constraints not derivable from the code or git history; convert relative dates to absolute. `reference` — pointers to external resources (URLs, dashboards, tickets).
65
+
66
+ After writing the file, add a one-line pointer in `MEMORY.md` (`- [Title](file.md) — hook`). `MEMORY.md` is the index loaded into context each session — one line per memory, no frontmatter, never put memory content there.
67
+
68
+ Before saving, check for an existing file that already covers it — update that file rather than creating a duplicate; delete memories that turn out to be wrong. Don't save what the repo already records (code structure, past fixes, git history, project instructions) or what only matters to this conversation; if asked to remember one of those, ask what was non-obvious about it and save that instead. Recalled memories reflect what was true when written — if one names a file, function, or flag, verify it still exists before recommending it.
@@ -75,11 +75,16 @@ export function resolveCalibrationTokens(calibrationModel, lastMainAgent, projec
75
75
  // (If a stale localStorage value misses both this map and calibrationModels, fall back to 'auto'.)
76
76
  export const LEGACY_CALIBRATION_MIGRATION = {
77
77
  'opus-4.7-1m': '1m',
78
+ 'opus-4.8[1m]': '1m',
78
79
  'sonnet-4.6': '200k',
79
80
  'glm5': '200k',
80
81
  'kimi-k2.5': '200k',
81
82
  'minimax-2.1': '200k',
82
83
  'Qwen 3.5': '200k',
84
+ 'sonnet-5': '1m',
85
+ 'glm5.2': '1m',
86
+ 'kimi-k3': '1m',
87
+ 'Qwen 3.8': '1m'
83
88
  };
84
89
 
85
90
  /**
@@ -326,7 +331,7 @@ const MODEL_PROVIDERS = [
326
331
  svg: '<svg t="1771495640319" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5634" width="200" height="200"><path d="M537.782857 57.161143c16.749714 29.44 33.426286 58.953143 50.102857 88.502857a7.68 7.68 0 0 0 6.692572 3.913143h236.873143c7.424 0 13.750857 4.681143 19.017142 13.933714l62.061715 109.677714c8.082286 14.372571 10.24 20.370286 1.024 35.693715a2262.893714 2262.893714 0 0 0-32.438857 55.478857l-15.652572 28.086857c-4.534857 8.338286-9.508571 11.922286-1.718857 21.833143l113.152 197.851428c7.314286 12.8 4.754286 21.065143-1.828571 32.841143-18.651429 33.499429-37.632 66.742857-56.978286 99.84-6.765714 11.593143-14.994286 16.018286-29.001143 15.798857a1833.398857 1833.398857 0 0 0-99.291429 0.658286 4.242286 4.242286 0 0 0-3.437714 2.157714 24571.830857 24571.830857 0 0 1-115.419428 202.24c-7.204571 12.507429-16.201143 15.469714-30.939429 15.506286-42.532571 0.146286-85.430857 0.182857-128.731429 0.109714a22.930286 22.930286 0 0 1-19.821714-11.556571l-56.978286-99.108571a3.84 3.84 0 0 0-3.547428-2.121143h-218.331429a76.288 76.288 0 0 1-34.377143-3.913143l-68.388571-118.198857a23.149714 23.149714 0 0 1-0.073143-23.04l51.492572-90.441143a8.448 8.448 0 0 0 0-8.411429 23594.496 23594.496 0 0 1-80.018286-139.593143L47.542857 425.325714c-6.838857-13.202286-7.387429-21.138286 4.022857-41.179428 19.858286-34.669714 39.570286-69.302857 59.209143-103.936 5.632-9.984 12.946286-14.262857 24.905143-14.262857 36.827429-0.182857 73.654857-0.182857 110.445714-0.073143 1.901714 0 3.657143-1.024 4.571429-2.669715l119.734857-208.859428a20.845714 20.845714 0 0 1 17.993143-10.496c22.381714-0.036571 44.946286 0 67.547428-0.256l43.410286-0.987429c14.555429-0.109714 30.866286 1.389714 38.4 14.518857z m-146.432 17.188571a2.56 2.56 0 0 0-2.194286 1.28l-122.331428 213.942857a6.692571 6.692571 0 0 1-5.741714 3.364572H138.788571c-2.377143 0-2.962286 1.060571-1.755428 3.145143l247.917714 433.334857c1.060571 1.792 0.548571 2.633143-1.462857 2.669714l-119.222857 0.658286a9.289143 9.289143 0 0 0-8.557714 4.937143l-56.32 98.56c-1.865143 3.328-0.914286 5.046857 2.925714 5.046857l243.858286 0.365714c1.974857 0 3.401143 0.841143 4.425142 2.56l59.867429 104.704c1.974857 3.474286 3.913143 3.510857 5.924571 0l213.577143-373.76 33.426286-58.953143a2.340571 2.340571 0 0 1 4.096 0l60.745143 107.958857a5.193143 5.193143 0 0 0 4.571428 2.633143l117.906286-0.841143a1.718857 1.718857 0 0 0 1.462857-2.56l-123.721143-217.014857a4.608 4.608 0 0 1 0-4.827428l12.507429-21.613715 47.798857-84.370285c1.024-1.755429 0.512-2.633143-1.499428-2.633143H392.521143c-2.486857 0-3.108571-1.097143-1.828572-3.291429l61.184-106.861714a4.571429 4.571429 0 0 0 0-4.864l-58.258285-102.253714a2.56 2.56 0 0 0-2.267429-1.316572z m268.361143 342.198857c1.974857 0 2.486857 0.841143 1.462857 2.56l-35.474286 62.500572-111.542857 195.620571a2.377143 2.377143 0 0 1-2.121143 1.243429 2.486857 2.486857 0 0 1-2.121142-1.243429l-147.346286-257.353143c-0.841143-1.462857-0.402286-2.230857 1.206857-2.304l9.216-0.512 286.793143-0.512h-0.073143z" fill="#7D5DED" p-id="5635"></path></svg>',
327
332
  },
328
333
  {
329
- match: /kimi|moonshot/i,
334
+ match: /kimi|moonshot|^k3$/i,
330
335
  name: 'Kimi',
331
336
  color: 'var(--bg-model-avatar)',
332
337
  svg: '<svg t="1771495664798" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6649" width="200" height="200"><path d="M731.062857 590.262857v318.317714h-148.589714V443.977143a146.285714 146.285714 0 0 1-146.285714 146.285714l-214.491429-0.036571v318.354285H73.142857V165.814857h148.553143v275.858286h180.882286l119.771428-275.858286h167.753143l-67.84 156.269714a255.524571 255.524571 0 0 1-106.678857 119.588572h66.925714a148.553143 148.553143 0 0 1 148.516572 148.589714z m120.758857-473.965714a99.035429 99.035429 0 0 1 0 198.070857h-99.035428V215.332571a99.035429 99.035429 0 0 1 99.035428-99.035428z" fill="currentColor" p-id="6650"></path></svg>',