cc-viewer 1.6.271 → 1.6.273

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 (186) hide show
  1. package/cli.js +44 -43
  2. package/concepts/ar/GlobalSettings.md +2 -2
  3. package/concepts/ar/MainAgent.md +1 -1
  4. package/concepts/ar/ProxySwitch.md +1 -1
  5. package/concepts/da/GlobalSettings.md +2 -2
  6. package/concepts/da/MainAgent.md +1 -1
  7. package/concepts/da/ProxySwitch.md +1 -1
  8. package/concepts/de/GlobalSettings.md +2 -2
  9. package/concepts/de/MainAgent.md +1 -1
  10. package/concepts/de/ProxySwitch.md +1 -1
  11. package/concepts/en/GlobalSettings.md +2 -2
  12. package/concepts/en/MainAgent.md +1 -1
  13. package/concepts/en/ProxySwitch.md +1 -1
  14. package/concepts/es/GlobalSettings.md +2 -2
  15. package/concepts/es/MainAgent.md +1 -1
  16. package/concepts/es/ProxySwitch.md +1 -1
  17. package/concepts/fr/GlobalSettings.md +2 -2
  18. package/concepts/fr/MainAgent.md +1 -1
  19. package/concepts/fr/ProxySwitch.md +1 -1
  20. package/concepts/it/GlobalSettings.md +2 -2
  21. package/concepts/it/MainAgent.md +1 -1
  22. package/concepts/it/ProxySwitch.md +1 -1
  23. package/concepts/ja/GlobalSettings.md +2 -2
  24. package/concepts/ja/MainAgent.md +1 -1
  25. package/concepts/ja/ProxySwitch.md +1 -1
  26. package/concepts/ko/GlobalSettings.md +2 -2
  27. package/concepts/ko/MainAgent.md +1 -1
  28. package/concepts/ko/ProxySwitch.md +1 -1
  29. package/concepts/no/GlobalSettings.md +2 -2
  30. package/concepts/no/MainAgent.md +1 -1
  31. package/concepts/no/ProxySwitch.md +1 -1
  32. package/concepts/pl/GlobalSettings.md +2 -2
  33. package/concepts/pl/MainAgent.md +1 -1
  34. package/concepts/pl/ProxySwitch.md +1 -1
  35. package/concepts/pt-BR/GlobalSettings.md +2 -2
  36. package/concepts/pt-BR/MainAgent.md +1 -1
  37. package/concepts/pt-BR/ProxySwitch.md +1 -1
  38. package/concepts/ru/GlobalSettings.md +2 -2
  39. package/concepts/ru/MainAgent.md +1 -1
  40. package/concepts/ru/ProxySwitch.md +1 -1
  41. package/concepts/th/GlobalSettings.md +2 -2
  42. package/concepts/th/MainAgent.md +1 -1
  43. package/concepts/th/ProxySwitch.md +1 -1
  44. package/concepts/tr/GlobalSettings.md +2 -2
  45. package/concepts/tr/MainAgent.md +1 -1
  46. package/concepts/tr/ProxySwitch.md +1 -1
  47. package/concepts/uk/GlobalSettings.md +2 -2
  48. package/concepts/uk/MainAgent.md +1 -1
  49. package/concepts/uk/ProxySwitch.md +1 -1
  50. package/concepts/zh/GlobalSettings.md +2 -2
  51. package/concepts/zh/MainAgent.md +1 -1
  52. package/concepts/zh/ProxySwitch.md +1 -1
  53. package/concepts/zh-TW/GlobalSettings.md +2 -2
  54. package/concepts/zh-TW/MainAgent.md +1 -1
  55. package/concepts/zh-TW/ProxySwitch.md +1 -1
  56. package/dist/assets/App-DLdA05Yx.js +1 -0
  57. package/dist/assets/App-TGGslOeT.css +1 -0
  58. package/dist/assets/{MdxEditorPanel-B7oOvR3k.js → MdxEditorPanel-D2Wt6kg-.js} +1 -1
  59. package/dist/assets/Mobile-Dhkz2rBB.js +1 -0
  60. package/dist/assets/{_baseUniq-Dgkw4IXM.js → _baseUniq-CPJrFyUF.js} +1 -1
  61. package/dist/assets/{arc-AiHQLijx.js → arc-BLBrFElt.js} +1 -1
  62. package/dist/assets/{architectureDiagram-Q4EWVU46-CPRvAIHK.js → architectureDiagram-Q4EWVU46-CbnBsMiQ.js} +1 -1
  63. package/dist/assets/{blockDiagram-DXYQGD6D-CK2cwrfX.js → blockDiagram-DXYQGD6D-0mYr6-Fl.js} +1 -1
  64. package/dist/assets/{c4Diagram-AHTNJAMY-BP-UBbgv.js → c4Diagram-AHTNJAMY-CS7vcr0z.js} +1 -1
  65. package/dist/assets/{channel-Ny3Nm_-t.js → channel-CF3zZzSR.js} +1 -1
  66. package/dist/assets/{chunk-4BX2VUAB-DdsULqPZ.js → chunk-4BX2VUAB-1FZYtnJ7.js} +1 -1
  67. package/dist/assets/{chunk-4TB4RGXK-BDSjQHh0.js → chunk-4TB4RGXK-COs1qui5.js} +1 -1
  68. package/dist/assets/{chunk-55IACEB6-DrKr3wBa.js → chunk-55IACEB6-p77Qw3wN.js} +1 -1
  69. package/dist/assets/{chunk-EDXVE4YY-o_0SUbAB.js → chunk-EDXVE4YY-5qaIrQKg.js} +1 -1
  70. package/dist/assets/{chunk-FMBD7UC4-Ca_AgqWi.js → chunk-FMBD7UC4-DmCR8mDZ.js} +1 -1
  71. package/dist/assets/{chunk-OYMX7WX6-CyWWbq5o.js → chunk-OYMX7WX6-D6xDfgW3.js} +1 -1
  72. package/dist/assets/{chunk-QZHKN3VN-5rXHErSL.js → chunk-QZHKN3VN-B6cmUU0N.js} +1 -1
  73. package/dist/assets/{chunk-YZCP3GAM-DznXBadU.js → chunk-YZCP3GAM-l-OyOqnn.js} +1 -1
  74. package/dist/assets/classDiagram-6PBFFD2Q-BiCYgTHO.js +1 -0
  75. package/dist/assets/classDiagram-v2-HSJHXN6E-BiCYgTHO.js +1 -0
  76. package/dist/assets/clone-ChTCnPsO.js +1 -0
  77. package/dist/assets/{cose-bilkent-S5V4N54A-BhGyix0v.js → cose-bilkent-S5V4N54A-CEzaS8XS.js} +1 -1
  78. package/dist/assets/{dagre-KV5264BT-CzzHxIvc.js → dagre-KV5264BT-B3U2njWW.js} +1 -1
  79. package/dist/assets/{diagram-5BDNPKRD-tu3BXl0c.js → diagram-5BDNPKRD-BRSDbyBr.js} +1 -1
  80. package/dist/assets/{diagram-G4DWMVQ6-C6WkK7sj.js → diagram-G4DWMVQ6-BZfOi9B7.js} +1 -1
  81. package/dist/assets/{diagram-MMDJMWI5-DBeD_WW-.js → diagram-MMDJMWI5-CWpb3Cg0.js} +1 -1
  82. package/dist/assets/{diagram-TYMM5635-BXUyHHJ4.js → diagram-TYMM5635-CTJyBSVj.js} +1 -1
  83. package/dist/assets/{erDiagram-SMLLAGMA-Bye5tnW2.js → erDiagram-SMLLAGMA-CHtTRd5S.js} +1 -1
  84. package/dist/assets/{flowDiagram-DWJPFMVM-C3pYOs38.js → flowDiagram-DWJPFMVM-Bda8X-WJ.js} +1 -1
  85. package/dist/assets/{ganttDiagram-T4ZO3ILL-DxXkI_FW.js → ganttDiagram-T4ZO3ILL-CPfnGu5V.js} +1 -1
  86. package/dist/assets/{gitGraphDiagram-UUTBAWPF-nsxsXsGX.js → gitGraphDiagram-UUTBAWPF-B5QxesQg.js} +1 -1
  87. package/dist/assets/{graph-Da-Z9hB7.js → graph-ChltdhTU.js} +1 -1
  88. package/dist/assets/{index-4gmR7Eun.js → index-B7lK5fJz.js} +1 -1
  89. package/dist/assets/{index-C8w5Sxw3.js → index-BK4sui_O.js} +1 -1
  90. package/dist/assets/{index-CA8JGh5J.js → index-C2fhupP6.js} +1 -1
  91. package/dist/assets/{index-CLfbZzwF.js → index-C3dxIBgt.js} +2 -2
  92. package/dist/assets/{index-D7XF7UJ8.js → index-C5PA4OJg.js} +1 -1
  93. package/dist/assets/{index-C0PhJcXG.js → index-CfEkC3bc.js} +1 -1
  94. package/dist/assets/{index-Brh2V8V0.js → index-DyGa-jNv.js} +1 -1
  95. package/dist/assets/{index-CsuhosSl.js → index-yClPXlMf.js} +1 -1
  96. package/dist/assets/{infoDiagram-42DDH7IO-Ca9j90t5.js → infoDiagram-42DDH7IO-CD4TS4O2.js} +1 -1
  97. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DhjV0XPD.js → ishikawaDiagram-UXIWVN3A-jhiYabQ7.js} +1 -1
  98. package/dist/assets/{journeyDiagram-VCZTEJTY-CRSHLZPV.js → journeyDiagram-VCZTEJTY-BDzIXxxt.js} +1 -1
  99. package/dist/assets/{jszip.min-CcCCdMNW.js → jszip.min-CuGGBMI4.js} +1 -1
  100. package/dist/assets/{kanban-definition-6JOO6SKY-Bg0CUwgc.js → kanban-definition-6JOO6SKY-D34MYATQ.js} +1 -1
  101. package/dist/assets/{layout-CWNu13XT.js → layout-D6sLAapX.js} +1 -1
  102. package/dist/assets/{linear-Dcmw1639.js → linear-CFV4P-wn.js} +1 -1
  103. package/dist/assets/{mermaid.core-1heNIJ5f.js → mermaid.core-YmJi7T-s.js} +2 -2
  104. package/dist/assets/{min-CC9CkAxn.js → min-akJQqRMn.js} +1 -1
  105. package/dist/assets/{mindmap-definition-QFDTVHPH-Gr0ex_Ny.js → mindmap-definition-QFDTVHPH-w5zaJyrN.js} +1 -1
  106. package/dist/assets/{pieDiagram-DEJITSTG-D7P3sUJY.js → pieDiagram-DEJITSTG-B6EM4Ow6.js} +1 -1
  107. package/dist/assets/{quadrantDiagram-34T5L4WZ-Bov3lcpV.js → quadrantDiagram-34T5L4WZ-2TmBxFy-.js} +1 -1
  108. package/dist/assets/{requirementDiagram-MS252O5E-BcLptaOU.js → requirementDiagram-MS252O5E-EOjvbxUy.js} +1 -1
  109. package/dist/assets/{sankeyDiagram-XADWPNL6-B2qAUsON.js → sankeyDiagram-XADWPNL6-BmTQD5eT.js} +1 -1
  110. package/dist/assets/seqResourceLoaders-Dov_BuQp.js +2 -0
  111. package/dist/assets/{seqResourceLoaders-N07Gfom9.css → seqResourceLoaders-DwpfKCub.css} +2 -2
  112. package/dist/assets/{sequenceDiagram-FGHM5R23-Do62Uz-a.js → sequenceDiagram-FGHM5R23-CLjtqA1D.js} +1 -1
  113. package/dist/assets/{stateDiagram-FHFEXIEX-Wu8aqa8C.js → stateDiagram-FHFEXIEX-Cbq90iZ8.js} +1 -1
  114. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BfYq7Jgo.js → stateDiagram-v2-QKLJ7IA2-BKDg-3t_.js} +1 -1
  115. package/dist/assets/{timeline-definition-GMOUNBTQ-DYfz5xD6.js → timeline-definition-GMOUNBTQ-f3kEDay0.js} +1 -1
  116. package/dist/assets/{vendor-antd-BG1SvzuN.js → vendor-antd-5xE7sz6B.js} +1 -1
  117. package/dist/assets/{vendor-codemirror-8NDhydlF.js → vendor-codemirror-ib-jPbXC.js} +1 -1
  118. package/dist/assets/{vendor-mdxeditor-BB4hhpxM.js → vendor-mdxeditor-BdKMdw6O.js} +2 -2
  119. package/dist/assets/{vendor-qrcode-DMsNGQ10.js → vendor-qrcode-vKlE-WYu.js} +1 -1
  120. package/dist/assets/{vendor-virtuoso-BUT96ALa.js → vendor-virtuoso-DOIfjLfU.js} +1 -1
  121. package/dist/assets/{vennDiagram-DHZGUBPP-CGr-cc7e.js → vennDiagram-DHZGUBPP-0GDSFVnH.js} +1 -1
  122. package/dist/assets/{wardley-RL74JXVD-BN899vMf.js → wardley-RL74JXVD-B2rj5j7G.js} +1 -1
  123. package/dist/assets/{wardleyDiagram-NUSXRM2D-xUsI1E7h.js → wardleyDiagram-NUSXRM2D-COVTciJP.js} +1 -1
  124. package/dist/assets/{xychartDiagram-5P7HB3ND-woQrslzB.js → xychartDiagram-5P7HB3ND-DXoLnGxX.js} +1 -1
  125. package/dist/index.html +4 -4
  126. package/findcc.js +26 -7
  127. package/interceptor.js +11 -1061
  128. package/package.json +6 -12
  129. package/plugins/.gitkeep +0 -0
  130. package/server/_paths.js +35 -0
  131. package/server/interceptor.js +1057 -0
  132. package/{lib → server/lib}/approval-modal-prefs.js +2 -1
  133. package/{lib → server/lib}/ask-store.js +51 -10
  134. package/server/lib/cli-inject.js +81 -0
  135. package/{lib → server/lib}/context-watcher.js +41 -1
  136. package/{lib → server/lib}/delta-reconstructor.js +1 -0
  137. package/{lib → server/lib}/enrich-plan-input.js +2 -2
  138. package/{lib → server/lib}/ensure-hooks.js +76 -12
  139. package/{lib → server/lib}/extract-plugin-name.mjs +1 -1
  140. package/{lib → server/lib}/file-access-policy.js +2 -2
  141. package/{lib → server/lib}/jsonl-archive.js +1 -1
  142. package/{lib → server/lib}/plugin-loader.js +5 -4
  143. package/{lib → server/lib}/plugin-manager.js +1 -1
  144. package/{lib → server/lib}/sdk-manager.js +1 -1
  145. package/{lib → server/lib}/session-transcript-reader.js +1 -1
  146. package/{lib → server/lib}/terminal-env.js +1 -1
  147. package/{lib → server/lib}/tools-xml-formatter.js +2 -1
  148. package/{lib → server/lib}/updater.js +3 -2
  149. package/{lib → server/lib}/voice-pack-events.js +4 -3
  150. package/{lib → server/lib}/voice-pack-manager.js +4 -8
  151. package/{proxy.js → server/proxy.js} +1 -1
  152. package/{pty-manager.js → server/pty-manager.js} +19 -5
  153. package/{scratch-pty-manager.js → server/scratch-pty-manager.js} +16 -4
  154. package/server/server.js +5467 -0
  155. package/{workspace-registry.js → server/workspace-registry.js} +2 -2
  156. package/server.js +5 -5459
  157. package/dist/assets/App-DOYmReD4.css +0 -1
  158. package/dist/assets/App-LEYaH-CM.js +0 -1
  159. package/dist/assets/Mobile-Cwf21Pmq.js +0 -1
  160. package/dist/assets/classDiagram-6PBFFD2Q-CLYcbnwx.js +0 -1
  161. package/dist/assets/classDiagram-v2-HSJHXN6E-CLYcbnwx.js +0 -1
  162. package/dist/assets/clone-5GFhU8Pv.js +0 -1
  163. package/dist/assets/seqResourceLoaders-CmFg0jyW.js +0 -2
  164. /package/{i18n.js → server/i18n.js} +0 -0
  165. /package/{lib → server/lib}/ask-bridge.js +0 -0
  166. /package/{lib → server/lib}/ask-constants.js +0 -0
  167. /package/{lib → server/lib}/ccv-editor.js +0 -0
  168. /package/{lib → server/lib}/claude-md-discovery.js +0 -0
  169. /package/{lib → server/lib}/file-api.js +0 -0
  170. /package/{lib → server/lib}/git-diff.js +0 -0
  171. /package/{lib → server/lib}/interceptor-core.js +0 -0
  172. /package/{lib → server/lib}/kv-cache-analyzer.js +0 -0
  173. /package/{lib → server/lib}/log-management.js +0 -0
  174. /package/{lib → server/lib}/log-stream.js +0 -0
  175. /package/{lib → server/lib}/log-watcher.js +0 -0
  176. /package/{lib → server/lib}/perm-bridge.js +0 -0
  177. /package/{lib → server/lib}/proxy-env.js +0 -0
  178. /package/{lib → server/lib}/proxy-errors.js +0 -0
  179. /package/{lib → server/lib}/sdk-adapter.js +0 -0
  180. /package/{lib → server/lib}/skills-api.js +0 -0
  181. /package/{lib → server/lib}/sse-backpressure.js +0 -0
  182. /package/{lib → server/lib}/stats-worker.js +0 -0
  183. /package/{lib → server/lib}/team-runtime.js +0 -0
  184. /package/{lib → server/lib}/turn-end-bridge.js +0 -0
  185. /package/{lib → server/lib}/user-profile.js +0 -0
  186. /package/{lib → server/lib}/zip-safety.js +0 -0
@@ -1,10 +1,11 @@
1
+ // CLIENT-SAFE: no node deps. Imported by src/ — do not add fs/process/node: imports.
1
2
  // Approval-modal preferences merge logic.
2
3
  //
3
4
  // Lives here (not in voice-pack-manager) because it merges *generic* approvalModal
4
5
  // fields (modalEnabled, soundEnabled, notifyOnlyWhenHidden) plus the voicePack subtree.
5
6
  // voice-pack-manager.js stays focused on the file/audio backing store.
6
7
  //
7
- // Both server.js (handles POST /api/preferences) and src/AppBase.jsx (hydrate +
8
+ // Both server/server.js (handles POST /api/preferences) and src/AppBase.jsx (hydrate +
8
9
  // handleVoicePackChange) use this so the merge contract is single-sourced.
9
10
 
10
11
  import { EVENT_KEYS } from './voice-pack-events.js';
@@ -9,11 +9,11 @@
9
9
  // - 启动时 hydrate 出的 entry 的 res 字段为 null(旧连接已死),
10
10
  // 等待新的 ask-bridge 重新 POST 同 toolUseId 时复用(已在 server.js:2727 实现)
11
11
  // 或浏览器通过 /api/pending-asks 拉取展示。
12
- import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, openSync, closeSync, unlinkSync } from 'node:fs';
12
+ import { readFileSync, writeFileSync, writeSync, existsSync, mkdirSync, statSync, openSync, closeSync, unlinkSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { randomBytes } from 'node:crypto';
15
15
  import { renameSyncWithRetry } from './file-api.js';
16
- import { LOG_DIR } from '../findcc.js';
16
+ import { LOG_DIR } from '../../findcc.js';
17
17
 
18
18
  const SCHEMA_VERSION = 1;
19
19
 
@@ -28,6 +28,46 @@ function sleep(ms) {
28
28
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
29
29
  }
30
30
 
31
+ // Lock body 含 owner pid,让其它进程精确判断 owner 是否仍活着 —— 避免 mtime-only stale
32
+ // 判定在 Electron 多 Tab + 慢盘场景下误删活跃锁(持锁方 fn 跑 >5s 时旧实现会被偷锁)。
33
+ // Body 不可读(老格式 / openSync 与 writeSync 之间的瞬间 race)时退回 mtime 阈值兜底。
34
+ function readLockOwnerPid(path) {
35
+ try {
36
+ const raw = readFileSync(path, 'utf-8');
37
+ if (!raw) return null;
38
+ const obj = JSON.parse(raw);
39
+ if (obj && Number.isInteger(obj.pid)) return obj.pid;
40
+ } catch {}
41
+ return null;
42
+ }
43
+
44
+ function isPidAlive(pid) {
45
+ if (!Number.isInteger(pid) || pid <= 0) return false;
46
+ try {
47
+ process.kill(pid, 0);
48
+ return true;
49
+ } catch (err) {
50
+ // ESRCH = 进程不存在;EPERM = 存在但不同 user(仍算活)
51
+ return err && err.code === 'EPERM';
52
+ }
53
+ }
54
+
55
+ function isLockStale(path, mtimeFallbackMs) {
56
+ const pid = readLockOwnerPid(path);
57
+ if (pid !== null) {
58
+ // 自己 pid 见到 = 必然 stale(理论不可能:try/finally 保证 unlink;
59
+ // 若发生 → 视作可回收,避免 2 秒死锁)
60
+ if (pid === process.pid) return true;
61
+ return !isPidAlive(pid);
62
+ }
63
+ try {
64
+ const stats = statSync(path);
65
+ return Date.now() - stats.mtimeMs > mtimeFallbackMs;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
31
71
  function withLock(fn) {
32
72
  mkdirSync(LOG_DIR, { recursive: true });
33
73
  const deadline = Date.now() + 2000;
@@ -35,18 +75,19 @@ function withLock(fn) {
35
75
  while (true) {
36
76
  try {
37
77
  const fd = openSync(getLockFile(), 'wx');
38
- closeSync(fd);
78
+ try {
79
+ writeSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
80
+ } finally {
81
+ closeSync(fd);
82
+ }
39
83
  break;
40
84
  } catch (err) {
41
85
  if (err?.code === 'EEXIST') {
42
86
  if (Date.now() < deadline) {
43
- try {
44
- const stats = statSync(getLockFile());
45
- if (Date.now() - stats.mtimeMs > STALE_THRESHOLD) {
46
- try { unlinkSync(getLockFile()); } catch {}
47
- continue;
48
- }
49
- } catch {}
87
+ if (isLockStale(getLockFile(), STALE_THRESHOLD)) {
88
+ try { unlinkSync(getLockFile()); } catch {}
89
+ continue;
90
+ }
50
91
  sleep(25);
51
92
  continue;
52
93
  }
@@ -0,0 +1,81 @@
1
+ // claude-code cli.js 注入/卸载逻辑 — 抽自 cc-viewer 根 cli.js 以便单元测试。
2
+ //
3
+ // 注入语义:在 @anthropic-ai/claude-code/cli.js 顶部插一段
4
+ // // >>> Start CC Viewer Web Service >>>
5
+ // <INJECT_IMPORT>
6
+ // // <<< Start CC Viewer Web Service <<<
7
+ // 让 claude 启动时先 evaluate cc-viewer 的 interceptor。
8
+ //
9
+ // 升级路径:INJECT_IMPORT 形态本身允许演进(如 relative path → bare specifier)。
10
+ // `LEGACY_INJECT_IMPORTS` 记录历史值;任何旧 marker block 在下次 `ccv -logger`
11
+ // 时会被重写为当前 INJECT_BLOCK。
12
+ //
13
+ // EOL 策略(务必保持,否则 Windows cli.js 注入后 git/编辑器抱怨混合行尾):
14
+ // - 注入:检测**原文件主导 EOL**(CRLF/LF),用同种 EOL `join` lines 写回。
15
+ // - INJECT_BLOCK **内部** 行分隔硬编码 `\n` —— 不参数化也不跟随原文件 EOL:
16
+ // * buildInjectBlockRegex 用 `\r?\n` 匹配,可同时识别两种形式的历史 marker;
17
+ // * 改成参数化 EOL 会破坏 LEGACY 形式的回归匹配(老 marker 是 `\n` 写入的)。
18
+ // - 因此注入后 CRLF 文件**会含混合 EOL**(块内 LF + 块外 CRLF),这是已知的、
19
+ // 被 test/cli-inject.test.js 'CRLF 文件注入后原 CRLF 部分被保留' 用例固化的行为。
20
+
21
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
22
+
23
+ export const INJECT_START = '// >>> Start CC Viewer Web Service >>>';
24
+ export const INJECT_END = '// <<< Start CC Viewer Web Service <<<';
25
+
26
+ export function buildInjectBlock(injectImport) {
27
+ return `${INJECT_START}\n${injectImport}\n${INJECT_END}`;
28
+ }
29
+
30
+ function escapeRegex(s) {
31
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ }
33
+
34
+ // 匹配 inject block(任意 INJECT_IMPORT 形式 —— 当前的 + 历史的)。
35
+ export function buildInjectBlockRegex(injectImport, legacyInjectImports) {
36
+ const allImports = [injectImport, ...legacyInjectImports];
37
+ const alt = allImports.map(escapeRegex).join('|');
38
+ return new RegExp(`${escapeRegex(INJECT_START)}\\r?\\n(?:${alt})\\r?\\n${escapeRegex(INJECT_END)}\\r?\\n?`, 'g');
39
+ }
40
+
41
+ // 注入 / 升级 cli.js。
42
+ // 返回值:
43
+ // - 'injected' 首次注入
44
+ // - 'exists' 已注入且与当前 INJECT_BLOCK 完全一致(幂等)
45
+ // - 'updated' 含 INJECT_START 但 marker 不一致 → 重写为新 INJECT_BLOCK(升级路径)
46
+ export function injectCliJsAt(cliPath, injectImport, legacyInjectImports) {
47
+ const injectBlock = buildInjectBlock(injectImport);
48
+ const content = readFileSync(cliPath, 'utf-8');
49
+ if (content.includes(INJECT_START)) {
50
+ if (content.includes(injectBlock)) return 'exists';
51
+ const regex = buildInjectBlockRegex(injectImport, legacyInjectImports);
52
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
53
+ const updated = content.replace(regex, injectBlock + eol);
54
+ if (updated !== content) {
55
+ writeFileSync(cliPath, updated);
56
+ return 'updated';
57
+ }
58
+ return 'exists';
59
+ }
60
+ // 保留主导 EOL:若原文件 CRLF,注入完仍 CRLF(否则 split('\n')+join('\n') 会把 Win 文件
61
+ // 一次性转 LF,git/编辑器抱怨混合行尾且对哈希签名敏感的脚本可能失效)。
62
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
63
+ const lines = content.split(/\r?\n/);
64
+ lines.splice(2, 0, injectBlock);
65
+ writeFileSync(cliPath, lines.join(eol));
66
+ return 'injected';
67
+ }
68
+
69
+ // 卸载 cli.js 注入。返回 'removed' | 'clean' | 'not_found' | 'error'.
70
+ export function removeCliJsInjectionAt(cliPath, injectImport, legacyInjectImports) {
71
+ try {
72
+ if (!existsSync(cliPath)) return 'not_found';
73
+ const content = readFileSync(cliPath, 'utf-8');
74
+ if (!content.includes(INJECT_START)) return 'clean';
75
+ const regex = buildInjectBlockRegex(injectImport, legacyInjectImports);
76
+ writeFileSync(cliPath, content.replace(regex, ''));
77
+ return 'removed';
78
+ } catch {
79
+ return 'error';
80
+ }
81
+ }
@@ -1,9 +1,13 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { getClaudeConfigDir } from '../findcc.js';
3
+ import { homedir } from 'node:os';
4
+ import { getClaudeConfigDir } from '../../findcc.js';
4
5
 
5
6
  export const CONTEXT_WINDOW_FILE = join(getClaudeConfigDir(), 'context-window.json');
6
7
  export const CLAUDE_SETTINGS_FILE = join(getClaudeConfigDir(), 'settings.json');
8
+ // ~/.claude.json 是 claude code 的主配置(非 settings.json),存 projects[cwd].lastModelUsage
9
+ // 等。两者命名相似但层级和内容完全不同,不要混用。
10
+ export const CLAUDE_USER_CONFIG_FILE = join(homedir(), '.claude.json');
7
11
 
8
12
  // Startup cache: read once, never re-read unless model changes
9
13
  let _startupModelBase = null; // e.g. 'opus-4-6'
@@ -62,6 +66,42 @@ export function getContextSizeForModel(apiModelName) {
62
66
  return 200000;
63
67
  }
64
68
 
69
+ /**
70
+ * 读 ~/.claude.json 里 projects[cwd].lastModelUsage,挑出 cwd 下"用得最多/最显式"的模型。
71
+ * 给 cc-viewer UI 血条 calibration 在启动期(lastMainAgent 仅有 haiku init ping 时)
72
+ * 提供一个比"auto → 200K"更贴合 claude 自己默认行为的兜底。
73
+ *
74
+ * lastModelUsage 结构:{ [modelId]: { costUSD, inputTokens, outputTokens, ... } }
75
+ * 没有 timestamp 字段(claude code 只累加 usage 不打时间戳),所以"最近"用以下代理:
76
+ * 1) 去掉 haiku-*(辅助模型,从来不是主 model)
77
+ * 2) 任一带 [1m] 后缀 → 直接返回(用户显式 opt-in 1M context 的强信号)
78
+ * 3) 否则按 costUSD 倒序,取第一(用得最多 ≈ 当前主用)
79
+ *
80
+ * 任何 IO / 解析异常返回 null;调用方应当作"没找到偏好"处理(auto 走冷启动 1M)。
81
+ *
82
+ * @param {string} cwd - 绝对路径,必须与 claude 写入 ~/.claude.json projects key 完全一致
83
+ * @param {string} [filePath] - 可选注入文件路径,默认 CLAUDE_USER_CONFIG_FILE;单测用
84
+ * @returns {string|null} model id(含 [1m] 后缀,例 "claude-opus-4-7[1m]")或 null
85
+ */
86
+ export function readClaudeProjectModel(cwd, filePath = CLAUDE_USER_CONFIG_FILE) {
87
+ try {
88
+ if (!cwd || typeof cwd !== 'string') return null;
89
+ if (!existsSync(filePath)) return null;
90
+ const raw = readFileSync(filePath, 'utf-8');
91
+ const data = JSON.parse(raw);
92
+ const lmu = data?.projects?.[cwd]?.lastModelUsage;
93
+ if (!lmu || typeof lmu !== 'object') return null;
94
+ const entries = Object.entries(lmu).filter(([k]) => typeof k === 'string' && !/haiku/i.test(k));
95
+ if (!entries.length) return null;
96
+ const withOneM = entries.find(([k]) => /\[1m\]/i.test(k));
97
+ if (withOneM) return withOneM[0];
98
+ entries.sort((a, b) => (b[1]?.costUSD || 0) - (a[1]?.costUSD || 0));
99
+ return entries[0][0];
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
65
105
  /**
66
106
  * Build a context_window SSE event payload from API usage data.
67
107
  * @param {object} usage - API response usage object
@@ -1,3 +1,4 @@
1
+ // CLIENT-SAFE: no node deps. Imported by src/ — do not add fs/process/node: imports.
1
2
  /**
2
3
  * Delta Reconstructor — 增量日志重建模块
3
4
  *
@@ -47,7 +47,7 @@ function findEmptyExitPlanModeBlocks(content, out) {
47
47
  * - Header 名规范:interceptor 走 fetch Headers.entries() 全小写(WHATWG 规范),
48
48
  * 只查小写键即可。
49
49
  * - In-place mutation by design:用 Object.assign(blk.input, patch) 而非
50
- * `blk.input = {...}`。增量重建器 (lib/delta-reconstructor.js) 会让同一 tool_use
50
+ * `blk.input = {...}`。增量重建器 (server/lib/delta-reconstructor.js) 会让同一 tool_use
51
51
  * block 在多个 entry 间共享对象引用;in-place mutate 让后续 entry 的「Object.keys
52
52
  * (inp).length === 0」预检自动跳过——正是我们想要的 (相同 plan,不重新查盘)。
53
53
  * 注意:此优化仅在 SSE / live 路径(共享对象)生效;REST 路径 raw → JSON.parse
@@ -93,7 +93,7 @@ export function enrichEntry(entry) {
93
93
  /**
94
94
  * 服务端三处接入点共用:raw 字符串预过滤 → 命中才 parse + enrich + stringify。
95
95
  *
96
- * 设计原则:保持 lib/log-stream.js 的「原始字符串透传」哲学,只对真正需要补全的
96
+ * 设计原则:保持 server/lib/log-stream.js 的「原始字符串透传」哲学,只对真正需要补全的
97
97
  * 条目做 parse / stringify,其它一律按 raw 透传。
98
98
  *
99
99
  * @param {string} raw - 一条日志条目的原始 JSON 字符串
@@ -2,15 +2,12 @@
2
2
  * Register AskUserQuestion and permission approval hooks into ~/.claude/settings.json.
3
3
  * Shared between cli.js and electron/tab-worker.js.
4
4
  */
5
- import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
6
- import { resolve, dirname } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
8
7
  import { randomBytes } from 'node:crypto';
9
- import { getClaudeConfigDir } from '../findcc.js';
10
-
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = dirname(__filename);
13
- const rootDir = resolve(__dirname, '..');
8
+ import { getClaudeConfigDir } from '../../findcc.js';
9
+ import { SERVER_LIB } from '../_paths.js';
10
+ import { renameSyncWithRetry } from './file-api.js';
14
11
 
15
12
  // Marker stamped on hook command strings so a future `cc-viewer cleanup-hooks`
16
13
  // CLI (or the user manually) can identify entries owned by cc-viewer and remove
@@ -58,6 +55,67 @@ function _mergeHookObj(existing, desired) {
58
55
  return merged;
59
56
  }
60
57
 
58
+ // 从 cc-viewer-managed command 字符串里抽出 `node "<path>"` 的目标 path。
59
+ // 用于 stale 检测 + uninstall 清理。返回 null 表示格式不匹配(保守不动)。
60
+ function _extractNodeTargetPath(cmd) {
61
+ if (typeof cmd !== 'string') return null;
62
+ const m = cmd.match(/node\s+"([^"]+)"/);
63
+ return m ? m[1] : null;
64
+ }
65
+
66
+ // 识别那些含 cc-viewer-managed marker 但 command 路径已经 stale 的 entry —
67
+ // 用于升级路径(lib/ → server/lib/ 等)一次性主动清除老条目,避免 _hookObjEqual
68
+ // 字段级 merge 留下「半新半旧」状态。同 marker 的新 desired 会在主流程里重新插入。
69
+ //
70
+ // 策略:通用 existsSync(parsed path)。比硬编码 regex 鲁棒 —— 未来 server-side 再
71
+ // 怎么重组目录,只要老条目里的 path 不在了,就识别为 stale。无法解析 path 时保守跳过。
72
+ function _looksStaleManagedCommand(cmd) {
73
+ if (typeof cmd !== 'string' || !cmd.includes(CCV_HOOK_MARKER)) return false;
74
+ const target = _extractNodeTargetPath(cmd);
75
+ if (!target) return false;
76
+ return !existsSync(target);
77
+ }
78
+
79
+ function _purgeStaleManagedHooks(settings) {
80
+ let removed = 0;
81
+ for (const sectionKey of ['PreToolUse', 'Stop']) {
82
+ const arr = settings.hooks?.[sectionKey];
83
+ if (!Array.isArray(arr)) continue;
84
+ for (let i = arr.length - 1; i >= 0; i--) {
85
+ const entry = arr[i];
86
+ const hooks = entry?.hooks;
87
+ if (!Array.isArray(hooks)) continue;
88
+ const stale = hooks.some(h => _looksStaleManagedCommand(h?.command));
89
+ if (stale) {
90
+ arr.splice(i, 1);
91
+ removed += 1;
92
+ }
93
+ }
94
+ }
95
+ return removed;
96
+ }
97
+
98
+ // uninstall 专用:清除所有 cc-viewer-managed entry,不论 path 是否还存在。
99
+ // 调用方负责 atomic write-back。返回 removed 数量。
100
+ export function removeAllManagedHooks(settings) {
101
+ let removed = 0;
102
+ for (const sectionKey of ['PreToolUse', 'Stop']) {
103
+ const arr = settings.hooks?.[sectionKey];
104
+ if (!Array.isArray(arr)) continue;
105
+ for (let i = arr.length - 1; i >= 0; i--) {
106
+ const entry = arr[i];
107
+ const hooks = entry?.hooks;
108
+ if (!Array.isArray(hooks)) continue;
109
+ const managed = hooks.some(h => typeof h?.command === 'string' && h.command.includes(CCV_HOOK_MARKER));
110
+ if (managed) {
111
+ arr.splice(i, 1);
112
+ removed += 1;
113
+ }
114
+ }
115
+ }
116
+ return removed;
117
+ }
118
+
61
119
  export function ensureHooks() {
62
120
  try {
63
121
  const claudeDir = getClaudeConfigDir();
@@ -73,10 +131,13 @@ export function ensureHooks() {
73
131
  if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
74
132
 
75
133
  let changed = false;
134
+ // 先一次性清掉「path 已 stale 但还带 cc-viewer-managed marker」的老条目,
135
+ // 让下面的 push 流程直接插新条目,而不是走 _mergeHookObj 半新半旧的合并。
136
+ if (_purgeStaleManagedHooks(settings) > 0) changed = true;
76
137
 
77
138
  // AskUserQuestion hook → ask-bridge.js
78
139
  // Guard: only execute when CCVIEWER_PORT is set (i.e. launched by cc-viewer)
79
- const askBridgePath = resolve(rootDir, 'lib', 'ask-bridge.js');
140
+ const askBridgePath = resolve(SERVER_LIB, 'ask-bridge.js');
80
141
  const askCmd = `[ -n "$CCVIEWER_PORT" ] && node "${askBridgePath}" || true ${CCV_HOOK_MARKER}`;
81
142
  const askDesired = _buildHookObj(askCmd);
82
143
  const askExisting = settings.hooks.PreToolUse.find(h => h.matcher === 'AskUserQuestion');
@@ -95,7 +156,7 @@ export function ensureHooks() {
95
156
 
96
157
  // Permission approval hook → perm-bridge.js (matcher: "" = match all tools)
97
158
  // Guard: only execute when CCVIEWER_PORT is set (i.e. launched by cc-viewer)
98
- const permBridgePath = resolve(rootDir, 'lib', 'perm-bridge.js');
159
+ const permBridgePath = resolve(SERVER_LIB, 'perm-bridge.js');
99
160
  const permCmd = `[ -n "$CCVIEWER_PORT" ] && node "${permBridgePath}" || true ${CCV_HOOK_MARKER}`;
100
161
  const permMatcher = '';
101
162
  // Clean up legacy entries
@@ -132,7 +193,7 @@ export function ensureHooks() {
132
193
  // end of a user-prompt turn), so the voice-pack `turnEnd` event can play at the
133
194
  // right moment — not after every individual API call like the SSE streaming
134
195
  // signal would. Same `CCVIEWER_PORT` guard pattern as the other bridges.
135
- const turnEndBridgePath = resolve(rootDir, 'lib', 'turn-end-bridge.js');
196
+ const turnEndBridgePath = resolve(SERVER_LIB, 'turn-end-bridge.js');
136
197
  const turnEndCmd = `[ -n "$CCVIEWER_PORT" ] && node "${turnEndBridgePath}" || true ${CCV_HOOK_MARKER}`;
137
198
  // Stop hooks use matcher: '' (or unset) since there's no tool name to scope by.
138
199
  // Find any existing entry that already points at our bridge to update-in-place.
@@ -163,7 +224,10 @@ export function ensureHooks() {
163
224
  const tmpPath = `${settingsPath}.tmp.${process.pid}.${randomBytes(4).toString('hex')}`;
164
225
  try {
165
226
  writeFileSync(tmpPath, JSON.stringify(settings, null, 2));
166
- renameSync(tmpPath, settingsPath);
227
+ // renameSyncWithRetry 而非裸 renameSync:Windows 上 claude.exe / 编辑器
228
+ // 持 settings.json reader handle 时 rename 会抛 EBUSY 被 outer catch 吞掉
229
+ // 静默丢失更新;retry 后再失败再 throw。
230
+ renameSyncWithRetry(tmpPath, settingsPath);
167
231
  // 透明声明:修改用户全局 settings.json 是高风险操作,启动日志可见让用户能审计
168
232
  console.log(`[cc-viewer] updated ${settingsPath} (hook timeout=${HOOK_TIMEOUT_S}s)`);
169
233
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 在隔离子进程中提取插件 name,避免不安全的插件代码影响主进程。
3
- * 用法: node lib/extract-plugin-name.mjs <file-path>
3
+ * 用法: node server/lib/extract-plugin-name.mjs <file-path>
4
4
  * 输出: JSON { name: string } 到 stdout
5
5
  */
6
6
  import { pathToFileURL } from 'node:url';
@@ -8,12 +8,12 @@
8
8
  *
9
9
  * 关键合同:返回 `real`(realpath 解析后),调用方 MUST 用 real 读文件,杜绝 TOCTOU。
10
10
  *
11
- * 不替代写路径的业务校验(见 lib/file-api.js writeFileContent),但同样会被 endpoint 用作首道关卡。
11
+ * 不替代写路径的业务校验(见 server/lib/file-api.js writeFileContent),但同样会被 endpoint 用作首道关卡。
12
12
  */
13
13
  import { realpathSync } from 'node:fs';
14
14
  import { resolve, basename, sep, join } from 'node:path';
15
15
  import { homedir, platform, tmpdir } from 'node:os';
16
- import { getClaudeConfigDir } from '../findcc.js';
16
+ import { getClaudeConfigDir } from '../../findcc.js';
17
17
  import { loadWorkspaces } from '../workspace-registry.js';
18
18
 
19
19
  const osPlatform = platform();
@@ -15,7 +15,7 @@ const CACHE_ROOT_NAME = 'ccv-extract';
15
15
  const RENAME_RETRY = 3;
16
16
  const RENAME_DELAY_MS = 50;
17
17
  const CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
18
- // 与 lib/log-management.js 的 MAX_MERGE_SIZE 对齐:merge 上限 400MB,归档 / 读回 zip
18
+ // 与 server/lib/log-management.js 的 MAX_MERGE_SIZE 对齐:merge 上限 400MB,归档 / 读回 zip
19
19
  // 必须容纳这一规模,否则自家产物会被 zip-safety 默认 50MB 阈值拦住造成永久不可读。
20
20
  const ARCHIVE_MAX_BYTES = 400 * 1024 * 1024;
21
21
 
@@ -1,7 +1,8 @@
1
1
  import { readdirSync, existsSync, readFileSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
3
- import { fileURLToPath, pathToFileURL } from 'node:url';
4
- import { LOG_DIR } from '../findcc.js';
2
+ import { join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { LOG_DIR } from '../../findcc.js';
5
+ import { PLUGINS_DIR } from '../_paths.js';
5
6
 
6
7
  // 动态获取(LOG_DIR 可能在运行时被 setLogDir 修改)
7
8
  export function getPluginsDir() { return join(LOG_DIR, 'plugins'); }
@@ -77,7 +78,7 @@ export async function loadPlugins() {
77
78
  }
78
79
 
79
80
  // Load bundled plugins from package's plugins/ directory (user plugins take priority)
80
- const bundledDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'plugins');
81
+ const bundledDir = PLUGINS_DIR;
81
82
  if (existsSync(bundledDir)) {
82
83
  let bundledFiles;
83
84
  try {
@@ -37,7 +37,7 @@ export function uploadPlugins(pluginsDir, fileList) {
37
37
  * Install a plugin by downloading from a URL.
38
38
  * @param {string} pluginsDir - path to plugins directory
39
39
  * @param {string} fileUrl - URL to download from
40
- * @param {string} extractNameScript - path to lib/extract-plugin-name.mjs
40
+ * @param {string} extractNameScript - path to server/lib/extract-plugin-name.mjs
41
41
  * @returns {Promise<{filename: string}>} the saved filename
42
42
  * @throws {Error} on validation or download failure
43
43
  */
@@ -397,7 +397,7 @@ async function _handleCanUseTool(toolName, input, options) {
397
397
  } catch {}
398
398
  }
399
399
  // 24h — 与 hook 路径(server.js ASK_HOOK_TIMEOUT_MS)同源,履行"GUI 实质无超时"承诺。
400
- // 实际常量定义在 lib/ask-constants.js。
400
+ // 实际常量定义在 server/lib/ask-constants.js。
401
401
  const askTimeoutMs = ASK_TIMEOUT_MS;
402
402
  const askStartedAt = Date.now();
403
403
  if (_broadcastWs) {
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { existsSync, statSync, openSync, readSync, closeSync, readdirSync } from 'node:fs';
22
22
  import { join } from 'node:path';
23
- import { getClaudeConfigDir } from '../findcc.js';
23
+ import { getClaudeConfigDir } from '../../findcc.js';
24
24
 
25
25
  function projectsDir() {
26
26
  return process.env.CCV_PROJECTS_DIR || join(getClaudeConfigDir(), 'projects');
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { getClaudeConfigDir } from '../findcc.js';
4
+ import { getClaudeConfigDir } from '../../findcc.js';
5
5
 
6
6
  export const KEEP_CLAUDE_NO_FLICKER_ENV = 'CCV_KEEP_CLAUDE_CODE_NO_FLICKER';
7
7
 
@@ -1,3 +1,4 @@
1
+ // CLIENT-SAFE: no node deps. Imported by src/ — do not add fs/process/node: imports.
1
2
  /**
2
3
  * Serialize an Anthropic tool definition (name / description / input_schema)
3
4
  * into the XML-shaped text format the model sees on the server side.
@@ -7,7 +8,7 @@
7
8
  * approximation useful for inspection, not a canonical wire format.
8
9
  *
9
10
  * Single source of truth: src/utils/toolsXmlFormatter.js re-exports from here,
10
- * lib/kv-cache-analyzer.js imports from here.
11
+ * server/lib/kv-cache-analyzer.js imports from here.
11
12
  *
12
13
  * Note: free-text fields (name / description / enum) are NOT XML-escaped.
13
14
  * parseCachedTools relies on first-match semantics — the tool-level <name>
@@ -4,7 +4,8 @@ import { join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { dirname } from 'node:path';
6
6
  import { t } from '../i18n.js';
7
- import { getClaudeConfigDir } from '../findcc.js';
7
+ import { getClaudeConfigDir } from '../../findcc.js';
8
+ import { PACKAGE_JSON } from '../_paths.js';
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
@@ -47,7 +48,7 @@ const CACHE_FILE = join(CACHE_DIR, 'update-check.json');
47
48
  const CC_SETTINGS_FILE = join(getClaudeConfigDir(), 'settings.json');
48
49
 
49
50
  function getCurrentVersion() {
50
- const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
51
+ const pkg = JSON.parse(readFileSync(PACKAGE_JSON, 'utf-8'));
51
52
  return pkg.version;
52
53
  }
53
54
 
@@ -1,8 +1,9 @@
1
+ // CLIENT-SAFE: no node deps. Imported by src/ — do not add fs/process/node: imports.
1
2
  // Single source of truth for voice-pack event keys + their default bindings.
2
3
  //
3
4
  // Why a shared module: this list was previously duplicated across
4
- // - lib/voice-pack-manager.js (EVENT_KEYS for whitelist + reconcile)
5
- // - server.js (preferences merge / reconcile)
5
+ // - server/lib/voice-pack-manager.js (EVENT_KEYS for whitelist + reconcile)
6
+ // - server/server.js (preferences merge / reconcile)
6
7
  // - src/AppBase.jsx (initial state default)
7
8
  // - src/components/VoicePackSettings.jsx (UI rows + reset handler)
8
9
  // - scripts/gen-placeholder-voicepack.js (pattern table keys)
@@ -12,7 +13,7 @@
12
13
 
13
14
  // 注:timeoutWarning5min / timeoutWarning60s 已删除。AskUserQuestion 实质 24h 无超时后
14
15
  // 倒计时不再渲染(AskTimeoutCountdown.jsx isInfiniteTimeout → null),剩余时间预警事件失去意义。
15
- // 老用户 preferences.json 含这两个 key 由 lib/approval-modal-prefs.js _filterEvents 白名单
16
+ // 老用户 preferences.json 含这两个 key 由 server/lib/approval-modal-prefs.js _filterEvents 白名单
16
17
  // 自动 strip,零迁移工作量。孤儿 audio 文件留待 cleanup CLI(backlog)。
17
18
  export const EVENT_KEYS = [
18
19
  'planApproval',
@@ -11,12 +11,8 @@
11
11
  import { existsSync, mkdirSync, writeFileSync, unlinkSync, readdirSync, statSync, readFileSync, lstatSync } from 'node:fs';
12
12
  import { join, extname } from 'node:path';
13
13
  import { randomUUID } from 'node:crypto';
14
- import { fileURLToPath } from 'node:url';
15
- import { dirname } from 'node:path';
16
14
  import { EVENT_KEYS, DEFAULT_BINDINGS, BUNDLED_PACK_IDS } from './voice-pack-events.js';
17
-
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = dirname(__filename);
15
+ import { DIST_DIR, PUBLIC_DIR } from '../_paths.js';
20
16
 
21
17
  // Bundled pack lookup order, per packId:
22
18
  // 1. <repo>/dist/voice-packs/<packId>/ — production (Vite copies public/* into dist/, npm ships dist/ only)
@@ -25,8 +21,8 @@ const __dirname = dirname(__filename);
25
21
  // drop public/voice-packs/<id>/ with pack.json. No code path here needs editing.
26
22
  function bundledPackDirs(packId) {
27
23
  return [
28
- join(__dirname, '..', 'dist', 'voice-packs', packId),
29
- join(__dirname, '..', 'public', 'voice-packs', packId),
24
+ join(DIST_DIR, 'voice-packs', packId),
25
+ join(PUBLIC_DIR, 'voice-packs', packId),
30
26
  ];
31
27
  }
32
28
 
@@ -338,6 +334,6 @@ export function reconcileVoicePackPrefs(logDir, vp) {
338
334
  // Re-export shared defaults so consumers can pull everything from one module.
339
335
  export { DEFAULT_BINDINGS, BUNDLED_PACK_IDS };
340
336
 
341
- // mergeApprovalModalPrefs / mergeVoicePackInto moved to lib/approval-modal-prefs.js
337
+ // mergeApprovalModalPrefs / mergeVoicePackInto moved to server/lib/approval-modal-prefs.js
342
338
  //( — merge logic isn't voice-pack-specific). Import from
343
339
  // './approval-modal-prefs.js' directly.
@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
6
6
  import * as interceptor from './interceptor.js';
7
7
  import { setupInterceptor } from './interceptor.js';
8
8
  import { extractApiErrorMessage, formatProxyRequestError } from './lib/proxy-errors.js';
9
- import { getClaudeConfigDir } from './findcc.js';
9
+ import { getClaudeConfigDir } from '../findcc.js';
10
10
 
11
11
  // Setup interceptor to patch fetch
12
12
  setupInterceptor();
@@ -1,8 +1,9 @@
1
- import { resolveNativePath, LOG_DIR } from './findcc.js';
1
+ import { resolveNativePath, LOG_DIR } from '../findcc.js';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { chmodSync, statSync } from 'node:fs';
5
5
  import { platform, arch } from 'node:os';
6
+ import { createRequire } from 'node:module';
6
7
  import { prepareEmbeddedShellSpawn, stripClaudeNoFlickerUnlessOptedIn } from './lib/terminal-env.js';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
@@ -83,16 +84,29 @@ function flushBatch() {
83
84
  }
84
85
  }
85
86
 
87
+ // 走 createRequire().resolve 而非 join(__dirname, '..', 'node_modules', ...) ——
88
+ // pnpm / yarn workspace 把 node-pty hoist 到上级 node_modules 时相对路径会找不到,
89
+ // 静默 chmod 失败 → 运行 PTY 时 EACCES,且全程无 log 难排查。
86
90
  function fixSpawnHelperPermissions() {
91
+ const os = platform();
92
+ const cpu = arch();
93
+ const subPath = `node-pty/prebuilds/${os}-${cpu}/spawn-helper`;
94
+ let helperPath;
95
+ try {
96
+ const req = createRequire(import.meta.url);
97
+ helperPath = req.resolve(subPath);
98
+ } catch (err) {
99
+ // node-pty 没安装/没该平台 prebuild:放过,spawn 时会另报错
100
+ return;
101
+ }
87
102
  try {
88
- const os = platform();
89
- const cpu = arch();
90
- const helperPath = join(__dirname, 'node_modules', 'node-pty', 'prebuilds', `${os}-${cpu}`, 'spawn-helper');
91
103
  const stat = statSync(helperPath);
92
104
  if (!(stat.mode & 0o111)) {
93
105
  chmodSync(helperPath, stat.mode | 0o755);
94
106
  }
95
- } catch { }
107
+ } catch (err) {
108
+ console.warn('[cc-viewer] fixSpawnHelperPermissions failed:', helperPath, err?.message || err);
109
+ }
96
110
  }
97
111
 
98
112
  // Opus 4.7 默认不再返回 thinking;为所有非显式覆写的调用加上 summarized。