cc-viewer 1.7.2 → 1.7.4

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 (98) hide show
  1. package/cli.js +109 -20
  2. package/dist/assets/App-CKvCavmF.js +2 -0
  3. package/dist/assets/App-Dj37fluO.css +1 -0
  4. package/dist/assets/{MdxEditorPanel-BUubiE6Y.js → MdxEditorPanel-DeSJp_ct.js} +1 -1
  5. package/dist/assets/{Mobile-CKmfRRvo.js → Mobile-CWou-jtF.js} +1 -1
  6. package/dist/assets/{_baseUniq-D9_E5ZDp.js → _baseUniq-BK7Nhp_x.js} +1 -1
  7. package/dist/assets/{arc-D4iFrlhb.js → arc-Cm4HMXz6.js} +1 -1
  8. package/dist/assets/{architectureDiagram-Q4EWVU46-f1neo7hg.js → architectureDiagram-Q4EWVU46-Co8QCowN.js} +1 -1
  9. package/dist/assets/{blockDiagram-DXYQGD6D-DWi9e9tG.js → blockDiagram-DXYQGD6D-Cudecf7x.js} +1 -1
  10. package/dist/assets/{c4Diagram-AHTNJAMY-DAdj49A2.js → c4Diagram-AHTNJAMY-4JeFoCHX.js} +1 -1
  11. package/dist/assets/{channel-B3VthT0m.js → channel-K7eT7Hjr.js} +1 -1
  12. package/dist/assets/{chunk-4BX2VUAB-BrPcAHV3.js → chunk-4BX2VUAB-BBjl6EVW.js} +1 -1
  13. package/dist/assets/{chunk-4TB4RGXK-Bvopc1V9.js → chunk-4TB4RGXK-DBP9YnwO.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-Juw-7Z3i.js → chunk-55IACEB6--A_i9gwR.js} +1 -1
  15. package/dist/assets/{chunk-EDXVE4YY-DVYP9tpB.js → chunk-EDXVE4YY-UBsL-3qr.js} +1 -1
  16. package/dist/assets/{chunk-FMBD7UC4-VQ7VOXl3.js → chunk-FMBD7UC4-DBO0asmI.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-F8Rj4ZUZ.js → chunk-OYMX7WX6-DOCcEaE6.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-Dn7K3yi9.js → chunk-QZHKN3VN-B-j8QbQh.js} +1 -1
  19. package/dist/assets/{chunk-YZCP3GAM-UjO-KTsn.js → chunk-YZCP3GAM-B0WO1U-U.js} +1 -1
  20. package/dist/assets/classDiagram-6PBFFD2Q-Ba3tOmih.js +1 -0
  21. package/dist/assets/classDiagram-v2-HSJHXN6E-Ba3tOmih.js +1 -0
  22. package/dist/assets/clone-DjURvTeN.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-BDhNj8rq.js → cose-bilkent-S5V4N54A-DQgpss_a.js} +1 -1
  24. package/dist/assets/{dagre-KV5264BT-BzdcDuyt.js → dagre-KV5264BT-CCJBKmKv.js} +1 -1
  25. package/dist/assets/{diagram-5BDNPKRD-DP_SX65N.js → diagram-5BDNPKRD-DgUcnfkU.js} +1 -1
  26. package/dist/assets/{diagram-G4DWMVQ6-B-pBFw8Q.js → diagram-G4DWMVQ6-CRpVvexw.js} +1 -1
  27. package/dist/assets/{diagram-MMDJMWI5-6XSfN1eN.js → diagram-MMDJMWI5-C3IhpsM-.js} +1 -1
  28. package/dist/assets/{diagram-TYMM5635-DJvGxvpE.js → diagram-TYMM5635-BaVbsrwt.js} +1 -1
  29. package/dist/assets/{erDiagram-SMLLAGMA-CpqdpGYj.js → erDiagram-SMLLAGMA-BxRoBUR8.js} +1 -1
  30. package/dist/assets/{flowDiagram-DWJPFMVM-C9m7wjwI.js → flowDiagram-DWJPFMVM-Du2lIzoM.js} +1 -1
  31. package/dist/assets/{ganttDiagram-T4ZO3ILL-CU0XOfY3.js → ganttDiagram-T4ZO3ILL-DR1pDqLK.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-UUTBAWPF-ipyGULnP.js → gitGraphDiagram-UUTBAWPF-DHAmE20O.js} +1 -1
  33. package/dist/assets/{graph-BR2KmES9.js → graph-D8Yv0IU1.js} +1 -1
  34. package/dist/assets/{index-C-be29ey.js → index-AOmFG_I_.js} +1 -1
  35. package/dist/assets/{index-D5tSeMU7.js → index-B6ZWIxY0.js} +1 -1
  36. package/dist/assets/{index-D5YruOKD.js → index-Bq95HjYM.js} +1 -1
  37. package/dist/assets/{index--wgRW_OB.js → index-D0pvxpcl.js} +1 -1
  38. package/dist/assets/{index-DuZQ6FqR.js → index-D2c77__8.js} +1 -1
  39. package/dist/assets/{index-C16fiVhv.js → index-DgtUyP57.js} +1 -1
  40. package/dist/assets/{index-CjsQEQc2.js → index-Dpu92rw_.js} +2 -2
  41. package/dist/assets/{index-OWzSX2T8.js → index-cCB-lhEO.js} +1 -1
  42. package/dist/assets/{infoDiagram-42DDH7IO-BQWQjfFX.js → infoDiagram-42DDH7IO-BAqRIm2G.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js → ishikawaDiagram-UXIWVN3A-CzH4tezb.js} +1 -1
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-CMbB_4G9.js → journeyDiagram-VCZTEJTY-Qs4eiyuI.js} +1 -1
  45. package/dist/assets/{jszip.min-B6k2t9Ch.js → jszip.min-C0OccDsk.js} +1 -1
  46. package/dist/assets/{kanban-definition-6JOO6SKY-CImdlKN3.js → kanban-definition-6JOO6SKY-BAjl0gpV.js} +1 -1
  47. package/dist/assets/{layout-DQl4aw09.js → layout-Cf6cd9xE.js} +1 -1
  48. package/dist/assets/{linear-CLD3aPlK.js → linear-PzdCkji6.js} +1 -1
  49. package/dist/assets/mermaid.core-DV9745Lq.js +7 -0
  50. package/dist/assets/{min-_D86henJ.js → min-CHgWxbzS.js} +1 -1
  51. package/dist/assets/{mindmap-definition-QFDTVHPH-2JVcu1nk.js → mindmap-definition-QFDTVHPH-CHUjMdpi.js} +1 -1
  52. package/dist/assets/{pieDiagram-DEJITSTG-Dik-m5u7.js → pieDiagram-DEJITSTG-D_r2QC0f.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-34T5L4WZ-Bm5l40Jq.js → quadrantDiagram-34T5L4WZ-DPzGygdN.js} +1 -1
  54. package/dist/assets/{requirementDiagram-MS252O5E-Ck9w5rTj.js → requirementDiagram-MS252O5E-BA2ynnGb.js} +1 -1
  55. package/dist/assets/{sankeyDiagram-XADWPNL6-BbGVCFEf.js → sankeyDiagram-XADWPNL6-XHVvXuqU.js} +1 -1
  56. package/dist/assets/{seqResourceLoaders-DFrHmgnK.css → seqResourceLoaders-0RXZfUKp.css} +1 -1
  57. package/dist/assets/seqResourceLoaders-kBEzcKLh.js +2 -0
  58. package/dist/assets/{sequenceDiagram-FGHM5R23-vjvEdksk.js → sequenceDiagram-FGHM5R23-C47sA8YH.js} +1 -1
  59. package/dist/assets/{stateDiagram-FHFEXIEX-DTqMoiB2.js → stateDiagram-FHFEXIEX-BOmer839.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js → stateDiagram-v2-QKLJ7IA2-D-kVvTHA.js} +1 -1
  61. package/dist/assets/{timeline-definition-GMOUNBTQ-Cw8O-52d.js → timeline-definition-GMOUNBTQ-CTAKVZgs.js} +1 -1
  62. package/dist/assets/{vendor-antd-DI7JL-mE.js → vendor-antd-BuJ6oz45.js} +2 -2
  63. package/dist/assets/{vendor-codemirror-B9c49dtM.js → vendor-codemirror-D2KAW4ty.js} +1 -1
  64. package/dist/assets/{vendor-mdxeditor-B6cpBtIE.js → vendor-mdxeditor-BsFVVUcj.js} +2 -2
  65. package/dist/assets/{vendor-qrcode-Dn90-s-u.js → vendor-qrcode-BpxSq04x.js} +1 -1
  66. package/dist/assets/{vendor-virtuoso-CpdSKD_6.js → vendor-virtuoso-an4WvAvS.js} +1 -1
  67. package/dist/assets/{vennDiagram-DHZGUBPP-DCuqymb7.js → vennDiagram-DHZGUBPP-CuXzoroh.js} +1 -1
  68. package/dist/assets/{wardley-RL74JXVD-BJ20wJUU.js → wardley-RL74JXVD-CFCmxnck.js} +1 -1
  69. package/dist/assets/{wardleyDiagram-NUSXRM2D-Vz6US3qt.js → wardleyDiagram-NUSXRM2D-gfhDB9SR.js} +1 -1
  70. package/dist/assets/{xychartDiagram-5P7HB3ND-CzkLKrmi.js → xychartDiagram-5P7HB3ND-X-pj66qI.js} +1 -1
  71. package/dist/index.html +4 -4
  72. package/package.json +1 -1
  73. package/server/i18n.js +120 -55
  74. package/server/interceptor.js +72 -2
  75. package/server/lib/ccswitch-import.js +251 -0
  76. package/server/lib/ensure-hooks.js +28 -2
  77. package/server/lib/log-watcher.js +8 -0
  78. package/server/lib/pid-alive.js +15 -0
  79. package/server/lib/proxy-retry.js +716 -0
  80. package/server/lib/session-start-bridge.js +121 -0
  81. package/server/lib/settings-merge.js +170 -0
  82. package/server/lib/v2/live-feed.js +189 -48
  83. package/server/lib/v2/session-owner.js +135 -0
  84. package/server/lib/v2/session-select.js +15 -2
  85. package/server/lib/v2/v2-writer.js +181 -6
  86. package/server/proxy.js +115 -11
  87. package/server/pty-manager.js +28 -4
  88. package/server/routes/events.js +44 -0
  89. package/server/routes/preferences.js +184 -1
  90. package/server/server.js +15 -1
  91. package/src/utils/contentFilter.js +17 -1
  92. package/dist/assets/App-B224r-8M.js +0 -2
  93. package/dist/assets/App-dYPa5-df.css +0 -1
  94. package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +0 -1
  95. package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +0 -1
  96. package/dist/assets/clone-BQtvy9Ae.js +0 -1
  97. package/dist/assets/mermaid.core-CN35dvKJ.js +0 -7
  98. package/dist/assets/seqResourceLoaders-C-T_EMiO.js +0 -2
package/cli.js CHANGED
@@ -5,7 +5,7 @@ if (process.platform === 'win32' && !process.env.UV_THREADPOOL_SIZE) {
5
5
  process.env.UV_THREADPOOL_SIZE = '16';
6
6
  }
7
7
 
8
- import { readFileSync, writeFileSync, existsSync, realpathSync, unlinkSync, mkdirSync } from 'node:fs';
8
+ import { readFileSync, writeFileSync, existsSync, realpathSync, unlinkSync, mkdirSync, statSync } from 'node:fs';
9
9
  import { resolve, isAbsolute } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { homedir } from 'node:os';
@@ -15,6 +15,7 @@ import { INJECT_IMPORT, LEGACY_INJECT_IMPORTS, resolveCliPath, resolveNativePath
15
15
  import { ensureHooks, removeAllManagedHooks } from './server/lib/ensure-hooks.js';
16
16
  import { injectCliJsAt, removeCliJsInjectionAt, INJECT_START as _INJECT_START, INJECT_END as _INJECT_END, buildInjectBlock as _buildInjectBlock } from './server/lib/cli-inject.js';
17
17
  import { normalizeBasePath } from './server/lib/base-path.js';
18
+ import { mergeSettingsIntoArgs } from './server/lib/settings-merge.js';
18
19
  import { createHardenedCleanup, installWinKeypressFallback } from './server/lib/term-signals.js';
19
20
 
20
21
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
@@ -59,6 +60,14 @@ function reportClaudeNotFound(cliPathHint) {
59
60
  }
60
61
  }
61
62
 
63
+ // Tombstone written by --uninstall; blocks the shell hook's background self-heal
64
+ // (`ccv -logger --self-heal`) from silently re-installing the integration.
65
+ // Anchored to the config dir (not LOG_DIR, which honors --log-dir/CCV_LOG_DIR
66
+ // overrides) so --uninstall and the hook always resolve the same path.
67
+ function getUninstallFlagPath() {
68
+ return resolve(getClaudeConfigDir(), 'cc-viewer', 'uninstalled.flag');
69
+ }
70
+
62
71
  function getShellConfigPath() {
63
72
  const shell = process.env.SHELL || '';
64
73
  if (shell.includes('zsh')) return resolve(homedir(), '.zshrc');
@@ -93,6 +102,12 @@ function buildShellHook(isNative) {
93
102
  '--help', '-h',
94
103
  ];
95
104
 
105
+ // Guard shared by both variants: if ccv was uninstalled, the wrapper removes
106
+ // itself from the live shell and falls back to the real claude binary.
107
+ // `hash -r` first — otherwise a stale command-hash entry in a long-lived
108
+ // shell lets `command -v ccv` false-pass once after the binary is deleted.
109
+ const ccvGoneGuard = `hash -r 2>/dev/null; command -v ccv >/dev/null 2>&1 || { unset -f claude; command claude "$@"; return; }`;
110
+
96
111
  if (isNative) {
97
112
  return `${SHELL_HOOK_START}
98
113
  claude() {
@@ -102,6 +117,8 @@ claude() {
102
117
  command claude "$@"
103
118
  return
104
119
  fi
120
+ # If ccv is gone (uninstalled), drop this stale wrapper and use claude directly
121
+ ${ccvGoneGuard}
105
122
  # Pass through certain commands directly without ccv interception
106
123
  case "$1" in
107
124
  ${passthroughCommands.join('|')})
@@ -127,6 +144,8 @@ claude() {
127
144
  command claude "$@"
128
145
  return
129
146
  fi
147
+ # If ccv is gone (uninstalled), drop this stale wrapper and use claude directly
148
+ ${ccvGoneGuard}
130
149
  # Pass through certain commands directly without ccv interception
131
150
  case "$1" in
132
151
  ${passthroughCommands.join('|')})
@@ -148,12 +167,12 @@ claude() {
148
167
  if [ -z "$cli_js" ]; then
149
168
  # cli.js 消失 → Claude Code 已升级到 2.1.114+(native-only 分发)。
150
169
  # 后台重写 hook(下次 shell 就是 native hook),当前调用直接走 native proxy 路径。
151
- ( ccv -logger >/dev/null 2>&1 & )
170
+ ( ccv -logger --self-heal >/dev/null 2>&1 & )
152
171
  ccv run -- claude --ccv-internal "$@"
153
172
  return $?
154
173
  fi
155
174
  if ! grep -q "CC Viewer" "$cli_js" 2>/dev/null; then
156
- ccv -logger 2>/dev/null
175
+ ccv -logger --self-heal 2>/dev/null
157
176
  fi
158
177
  command claude "$@"
159
178
  }
@@ -195,7 +214,8 @@ function removeShellHook() {
195
214
  for (const f of ['.zshrc', '.zprofile', '.bashrc', '.bash_profile', '.profile']) {
196
215
  allPaths.add(resolve(home, f));
197
216
  }
198
- let lastResult = { path: configPath, status: 'clean' };
217
+ // Per-file results; empty array = no file contained the hook marker
218
+ const results = [];
199
219
  for (const p of allPaths) {
200
220
  try {
201
221
  if (!existsSync(p)) continue;
@@ -203,13 +223,19 @@ function removeShellHook() {
203
223
  if (!content.includes(SHELL_HOOK_START)) continue;
204
224
  const regex = new RegExp(`\\n?${SHELL_HOOK_START}[\\s\\S]*?${SHELL_HOOK_END}\\n?`, 'g');
205
225
  const newContent = content.replace(regex, '\n');
226
+ if (newContent === content) {
227
+ // START marker present but the block is malformed (e.g. END marker damaged):
228
+ // leave the file untouched instead of reporting a removal that didn't happen
229
+ results.push({ file: p, status: 'corrupt' });
230
+ continue;
231
+ }
206
232
  writeFileSync(p, newContent);
207
- lastResult = { path: p, status: 'removed' };
233
+ results.push({ file: p, status: 'removed' });
208
234
  } catch (err) {
209
- lastResult = { path: p, status: 'error', error: err.message };
235
+ results.push({ file: p, status: 'error', error: err.message });
210
236
  }
211
237
  }
212
- return lastResult;
238
+ return results;
213
239
  }
214
240
 
215
241
  function injectCliJs() {
@@ -272,17 +298,34 @@ async function runProxyCommand(args) {
272
298
  // 剥离 cc-viewer 的内部短路开关,避免泄漏给 claude 子进程
273
299
  delete env.CCV_SKIP_THINKING_DISPLAY;
274
300
 
275
- const settingsJson = JSON.stringify({
301
+ const settingsObj = {
276
302
  env: {
277
303
  ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL
278
304
  }
279
- });
305
+ };
306
+ let settingsJson = JSON.stringify(settingsObj);
307
+
308
+ const isClaudeCmd = cmd === 'claude' || /[\\/]claude(\.exe)?$/.test(cmd);
309
+
310
+ // Fold a user-supplied --settings into the injected settings and emit a single flag
311
+ // (claude is last-wins for duplicate --settings; two parallel flags let the user's
312
+ // silently clobber the injected ANTHROPIC_BASE_URL and break capture). claude only:
313
+ // another tool's --settings belongs to that tool and must not be touched.
314
+ // Runs on the RAW cmdArgs BEFORE the --thinking-display append below: otherwise a
315
+ // trailing valueless user --settings would consume our appended token as its value.
316
+ if (isClaudeCmd) {
317
+ const settingsMerge = mergeSettingsIntoArgs(cmdArgs, settingsObj, { cwd: process.cwd() });
318
+ if (settingsMerge.warningDetail) {
319
+ console.warn(t('cli.settingsMergeFailed', settingsMerge.warningDetail));
320
+ }
321
+ settingsJson = settingsMerge.settingsJson;
322
+ cmdArgs = settingsMerge.args;
323
+ }
280
324
 
281
325
  // 注入默认 --thinking-display summarized,仅对 claude 二进制(其他命令如 `ccv run -- sometool` 跳过)。
282
326
  // 若 claude 不识别该 flag(老版本/fork)会 unknown option 崩溃——由 pty-manager.js::spawnClaude 的
283
327
  // onExit reactive retry 兜底;cli.js 这条路径是一次性子进程,没有 respawn 机会,用户需手动重试。
284
328
  // 可通过环境变量 CCV_SKIP_THINKING_DISPLAY=1 强制跳过。
285
- const isClaudeCmd = cmd === 'claude' || /[\\/]claude(\.exe)?$/.test(cmd);
286
329
  if (isClaudeCmd && process.env.CCV_SKIP_THINKING_DISPLAY !== '1') {
287
330
  const { withDefaultThinkingDisplay } = await import('./server/pty-manager.js');
288
331
  cmdArgs = withDefaultThinkingDisplay(cmdArgs);
@@ -445,12 +488,18 @@ async function runCliMode(extraClaudeArgs = [], cwd, noOpen = false) {
445
488
  // Win 上 `start` 是 cmd.exe 内置不是 .exe,必须 shell:true;用 spawn + 数组让 Node 自己 escape。
446
489
  // 第二个 arg '""' 是 `start` 的 window-title 占位(否则 start 会把 URL 当 title)。
447
490
  const { spawn } = await import('node:child_process');
491
+ // Headless/container envs may lack xdg-open/open/start → spawn asynchronously emits an 'error' event,
492
+ // which try/catch cannot catch (it only catches synchronous throws); without a handler the process
493
+ // would crash. Swallow it here as a best-effort fallback.
494
+ let child;
448
495
  if (process.platform === 'win32') {
449
- spawn('cmd.exe', ['/c', 'start', '""', url], { stdio: 'ignore', detached: true, windowsHide: true }).unref();
496
+ child = spawn('cmd.exe', ['/c', 'start', '""', url], { stdio: 'ignore', detached: true, windowsHide: true });
450
497
  } else {
451
498
  const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
452
- spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref();
499
+ child = spawn(cmd, [url], { stdio: 'ignore', detached: true });
453
500
  }
501
+ child.on('error', () => {});
502
+ child.unref();
454
503
  } catch {}
455
504
  }
456
505
 
@@ -628,12 +677,18 @@ async function runSdkMode(extraClaudeArgs = [], cwd, noOpen = false) {
628
677
  // Win 上 `start` 是 cmd.exe 内置不是 .exe,必须 shell:true;用 spawn + 数组让 Node 自己 escape。
629
678
  // 第二个 arg '""' 是 `start` 的 window-title 占位(否则 start 会把 URL 当 title)。
630
679
  const { spawn } = await import('node:child_process');
680
+ // Headless/container envs may lack xdg-open/open/start → spawn asynchronously emits an 'error' event,
681
+ // which try/catch cannot catch (it only catches synchronous throws); without a handler the process
682
+ // would crash. Swallow it here as a best-effort fallback.
683
+ let child;
631
684
  if (process.platform === 'win32') {
632
- spawn('cmd.exe', ['/c', 'start', '""', url], { stdio: 'ignore', detached: true, windowsHide: true }).unref();
685
+ child = spawn('cmd.exe', ['/c', 'start', '""', url], { stdio: 'ignore', detached: true, windowsHide: true });
633
686
  } else {
634
687
  const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
635
- spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref();
688
+ child = spawn(cmd, [url], { stdio: 'ignore', detached: true });
636
689
  }
690
+ child.on('error', () => {});
691
+ child.unref();
637
692
  } catch {}
638
693
  }
639
694
 
@@ -787,7 +842,7 @@ if (isVersion) {
787
842
 
788
843
  if (isUninstall) {
789
844
  const cliResult = removeCliJsInjection();
790
- const shellResult = removeShellHook();
845
+ const shellResults = removeShellHook();
791
846
 
792
847
  if (cliResult === 'removed' || cliResult === 'clean') {
793
848
  console.log(t('cli.uninstall.cliCleaned'));
@@ -797,12 +852,18 @@ if (isUninstall) {
797
852
  console.log(t('cli.uninstall.cliFail'));
798
853
  }
799
854
 
800
- if (shellResult.status === 'removed') {
801
- console.log(t('cli.uninstall.hookRemoved', { path: shellResult.path }));
802
- } else if (shellResult.status === 'clean' || shellResult.status === 'not_found') {
803
- console.log(t('cli.uninstall.hookClean', { path: shellResult.path }));
855
+ if (shellResults.length === 0) {
856
+ console.log(t('cli.uninstall.hookClean', { path: getShellConfigPath() }));
804
857
  } else {
805
- console.log(t('cli.uninstall.hookFail', { error: shellResult.error }));
858
+ for (const r of shellResults) {
859
+ if (r.status === 'removed') {
860
+ console.log(t('cli.uninstall.hookRemoved', { path: r.file }));
861
+ } else if (r.status === 'corrupt') {
862
+ console.log(t('cli.uninstall.corruptBlock', { file: r.file }));
863
+ } else {
864
+ console.log(t('cli.uninstall.hookFail', { file: r.file, error: r.error }));
865
+ }
866
+ }
806
867
  }
807
868
 
808
869
  // 清理 settings.json 里的 cc-viewer-managed hooks + 历史 statusLine 残留
@@ -845,12 +906,40 @@ if (isUninstall) {
845
906
  }
846
907
  } catch { }
847
908
 
909
+ // Tombstone: stale hooks in still-open shells background `ccv -logger --self-heal`,
910
+ // which would otherwise re-install everything right after this uninstall.
911
+ try {
912
+ mkdirSync(resolve(getClaudeConfigDir(), 'cc-viewer'), { recursive: true });
913
+ writeFileSync(getUninstallFlagPath(), new Date().toISOString() + '\n');
914
+ } catch { }
915
+
848
916
  console.log(t('cli.uninstall.reloadShell'));
849
917
  console.log(t('cli.uninstall.done'));
850
918
  process.exit(0);
851
919
  }
852
920
 
853
921
  if (isLogger) {
922
+ // Tombstone check must run before mode detection: a backgrounded self-heal with
923
+ // claude fully uninstalled must exit 0 silently, not emit reportClaudeNotFound noise.
924
+ try {
925
+ const flagPath = getUninstallFlagPath();
926
+ if (existsSync(flagPath)) {
927
+ if (args.includes('--self-heal')) {
928
+ // Explicitly uninstalled — never let a stale hook resurrect the integration
929
+ process.exit(0);
930
+ }
931
+ const ageMs = Date.now() - statSync(flagPath).mtimeMs;
932
+ const GRACE_MS = 10 * 60 * 1000;
933
+ if (ageMs < GRACE_MS && !args.includes('--force')) {
934
+ // Old deployed hooks self-heal via plain `ccv -logger`; a grace period keeps
935
+ // them from wiping a fresh tombstone during the uninstall race window.
936
+ console.log(t('cli.logger.justUninstalled'));
937
+ process.exit(0);
938
+ }
939
+ unlinkSync(flagPath);
940
+ }
941
+ } catch { }
942
+
854
943
  // 模式选择:有 cli.js 就走 npm 注入模式(pre-2.1.113),没有就走 native proxy
855
944
  // 模式(2.1.114+)。单一判据,不再靠 realpath 的启发式。
856
945
  const nativePath = resolveNativePath();