cc-viewer 1.9.9 → 1.9.11

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 (140) hide show
  1. package/README.md +1 -1
  2. package/cli.js +4 -3
  3. package/dist/assets/App-D3swMyLc.js +2 -0
  4. package/dist/assets/{App-D06hBGII.css → App-DU5jmGrx.css} +1 -1
  5. package/dist/assets/{MdxEditorPanel-BJVD_bcU.js → MdxEditorPanel-DeyoQsxw.js} +1 -1
  6. package/dist/assets/Mobile-DvI_Kby2.js +1 -0
  7. package/dist/assets/ProxyStatsModal-CoI275by.js +1 -0
  8. package/dist/assets/{_baseUniq-CwQykUuc.js → _baseUniq-BvPjRimO.js} +1 -1
  9. package/dist/assets/{arc-EdUdBxtG.js → arc-BzZyqc6p.js} +1 -1
  10. package/dist/assets/{architectureDiagram-Q4EWVU46-DtBIdElY.js → architectureDiagram-Q4EWVU46-B1oszHzA.js} +1 -1
  11. package/dist/assets/{blockDiagram-DXYQGD6D-guNZIKkZ.js → blockDiagram-DXYQGD6D-Ndp4o_QB.js} +1 -1
  12. package/dist/assets/{c4Diagram-AHTNJAMY-xV5vB-Z9.js → c4Diagram-AHTNJAMY-DlDE-ALt.js} +1 -1
  13. package/dist/assets/{channel-DYEktNap.js → channel-BhXx9y6q.js} +1 -1
  14. package/dist/assets/{chunk-4BX2VUAB-ZTkItpvj.js → chunk-4BX2VUAB-CM-2e6q_.js} +1 -1
  15. package/dist/assets/{chunk-4TB4RGXK-dYIewyyR.js → chunk-4TB4RGXK-GWLkQKRO.js} +1 -1
  16. package/dist/assets/{chunk-55IACEB6-CSsPNgPX.js → chunk-55IACEB6-B6LOFqRZ.js} +1 -1
  17. package/dist/assets/{chunk-EDXVE4YY-CnMiuqzQ.js → chunk-EDXVE4YY-Ct3e09Ku.js} +1 -1
  18. package/dist/assets/{chunk-FMBD7UC4-CVpATPZ3.js → chunk-FMBD7UC4-DK-vXpZP.js} +1 -1
  19. package/dist/assets/{chunk-OYMX7WX6-Dpq4s8dW.js → chunk-OYMX7WX6-CWxZc_DU.js} +1 -1
  20. package/dist/assets/{chunk-QZHKN3VN-Bnojh6X6.js → chunk-QZHKN3VN-jp8ISHue.js} +1 -1
  21. package/dist/assets/{chunk-YZCP3GAM-BYqK4Dsa.js → chunk-YZCP3GAM-DtYet6WV.js} +1 -1
  22. package/dist/assets/classDiagram-6PBFFD2Q-C0dwAyuU.js +1 -0
  23. package/dist/assets/classDiagram-v2-HSJHXN6E-C0dwAyuU.js +1 -0
  24. package/dist/assets/clone-fiofFlrV.js +1 -0
  25. package/dist/assets/{cose-bilkent-S5V4N54A-OJ7mmWyV.js → cose-bilkent-S5V4N54A-lNC-slVX.js} +1 -1
  26. package/dist/assets/{dagre-KV5264BT-B1Kwd_Rk.js → dagre-KV5264BT-vm6U82qq.js} +1 -1
  27. package/dist/assets/{diagram-5BDNPKRD-D8qrw8sp.js → diagram-5BDNPKRD-Dq3FXh-U.js} +1 -1
  28. package/dist/assets/{diagram-G4DWMVQ6-ip-N4ho8.js → diagram-G4DWMVQ6-BxF85wcF.js} +1 -1
  29. package/dist/assets/{diagram-MMDJMWI5-2PZaskUF.js → diagram-MMDJMWI5-CFCTeMz0.js} +1 -1
  30. package/dist/assets/{diagram-TYMM5635-CeAOQUZP.js → diagram-TYMM5635-Bui5Azdg.js} +1 -1
  31. package/dist/assets/{erDiagram-SMLLAGMA-BCBaEYEb.js → erDiagram-SMLLAGMA-DrpUO1Kb.js} +1 -1
  32. package/dist/assets/{flowDiagram-DWJPFMVM-CsGyIqcX.js → flowDiagram-DWJPFMVM-Bb3amxiK.js} +1 -1
  33. package/dist/assets/{ganttDiagram-T4ZO3ILL-COsj3FZ-.js → ganttDiagram-T4ZO3ILL-BRwCFM3C.js} +1 -1
  34. package/dist/assets/{gitGraphDiagram-UUTBAWPF-DvQVQGJG.js → gitGraphDiagram-UUTBAWPF-DVXbBwhs.js} +1 -1
  35. package/dist/assets/{graph-BSB9fDSP.js → graph-Dh22wYWs.js} +1 -1
  36. package/dist/assets/index-BRW8dfBU.js +2 -0
  37. package/dist/assets/{index-BRw4Iz2O.js → index-BwZB7UwN.js} +1 -1
  38. package/dist/assets/{index-eM2ih5n7.js → index-CQ1Nswa3.js} +1 -1
  39. package/dist/assets/{index-BuVComcY.js → index-CfFFgxxf.js} +1 -1
  40. package/dist/assets/{index-DKufsAzx.js → index-Cfm4N4ao.js} +1 -1
  41. package/dist/assets/{index-CNNAYjkF.js → index-UlO7Q5dX.js} +1 -1
  42. package/dist/assets/{index-DXBfwPYD.js → index-e8wejtj_.js} +1 -1
  43. package/dist/assets/{index-Bo-12bug.js → index-xZXOwglE.js} +1 -1
  44. package/dist/assets/{infoDiagram-42DDH7IO-eNucS4iW.js → infoDiagram-42DDH7IO-cQPbF7Q6.js} +1 -1
  45. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Dxxj-gUX.js → ishikawaDiagram-UXIWVN3A-cp-ir90Y.js} +1 -1
  46. package/dist/assets/{journeyDiagram-VCZTEJTY-yJJzVEBb.js → journeyDiagram-VCZTEJTY-aoMcyNLC.js} +1 -1
  47. package/dist/assets/{jszip.min-ByI0ds5u.js → jszip.min-C7BBsGL5.js} +1 -1
  48. package/dist/assets/{kanban-definition-6JOO6SKY-2rfTi2zw.js → kanban-definition-6JOO6SKY-Cugr0o0f.js} +1 -1
  49. package/dist/assets/{layout-DTmHj0Wm.js → layout-CL0eiocE.js} +1 -1
  50. package/dist/assets/{linear-BOA3a9FE.js → linear-D6EJvwNs.js} +1 -1
  51. package/dist/assets/{mermaid.core-5H-NR79H.js → mermaid.core-CNYcCP19.js} +2 -2
  52. package/dist/assets/{min-CpjFTEec.js → min-dFBWKmZ_.js} +1 -1
  53. package/dist/assets/{mindmap-definition-QFDTVHPH-B8KCjFgB.js → mindmap-definition-QFDTVHPH-4fbr7E2n.js} +1 -1
  54. package/dist/assets/{pieDiagram-DEJITSTG-BARy96g5.js → pieDiagram-DEJITSTG-UHWzU_JW.js} +1 -1
  55. package/dist/assets/{quadrantDiagram-34T5L4WZ-DYoTapuI.js → quadrantDiagram-34T5L4WZ-e87bZkAb.js} +1 -1
  56. package/dist/assets/{requirementDiagram-MS252O5E-EeANLLK2.js → requirementDiagram-MS252O5E-l_97YNSi.js} +1 -1
  57. package/dist/assets/{sankeyDiagram-XADWPNL6-DmQ-YVPj.js → sankeyDiagram-XADWPNL6-D3JFnltO.js} +1 -1
  58. package/dist/assets/seqResourceLoaders-8kfGbCYo.js +2 -0
  59. package/dist/assets/seqResourceLoaders-C2YGihCH.css +41 -0
  60. package/dist/assets/{sequenceDiagram-FGHM5R23-SZtW0rYC.js → sequenceDiagram-FGHM5R23-5jDKcsRZ.js} +1 -1
  61. package/dist/assets/{stateDiagram-FHFEXIEX-CEygsIld.js → stateDiagram-FHFEXIEX-D5_gBKn8.js} +1 -1
  62. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BU1XPhBe.js → stateDiagram-v2-QKLJ7IA2-BP5uQ5EH.js} +1 -1
  63. package/dist/assets/{timeline-definition-GMOUNBTQ-DhPBaQBt.js → timeline-definition-GMOUNBTQ-7IVQRwK1.js} +1 -1
  64. package/dist/assets/vendor-antd-CfuxfYfy.js +17 -0
  65. package/dist/assets/vendor-codemirror-C4Jn4TTc.js +1 -0
  66. package/dist/assets/{vendor-mdxeditor-Cz09a1_I.js → vendor-mdxeditor-BreKt5Hd.js} +2 -2
  67. package/dist/assets/{vendor-qrcode-qbbntD6-.js → vendor-qrcode-CutTqXgh.js} +1 -1
  68. package/dist/assets/{vendor-virtuoso-Sk8fQ4Hx.js → vendor-virtuoso-Cc9ovCDf.js} +1 -1
  69. package/dist/assets/{vennDiagram-DHZGUBPP-NniTHxDw.js → vennDiagram-DHZGUBPP-D6qzGquN.js} +1 -1
  70. package/dist/assets/{wardley-RL74JXVD-D9SNOrDw.js → wardley-RL74JXVD-DzAFqZmY.js} +1 -1
  71. package/dist/assets/{wardleyDiagram-NUSXRM2D-DvHXKNza.js → wardleyDiagram-NUSXRM2D-CImP9kv8.js} +1 -1
  72. package/dist/assets/{xychartDiagram-5P7HB3ND-CaiwPTQB.js → xychartDiagram-5P7HB3ND-Co45rRaM.js} +1 -1
  73. package/dist/index.html +4 -4
  74. package/package.json +1 -1
  75. package/plugins/README.md +9 -1
  76. package/server/interceptor.js +1 -1
  77. package/server/lib/builtin-model-prompts.js +1 -1
  78. package/server/lib/chat-queue.js +1 -1
  79. package/server/lib/credential-access.js +1 -1
  80. package/server/lib/credential-migrate.js +1 -1
  81. package/server/lib/error-report.js +26 -0
  82. package/server/lib/json-store.js +1 -1
  83. package/server/lib/launch-config.js +1 -1
  84. package/server/lib/log-watcher.js +1 -1
  85. package/server/lib/plugin-host.mjs +198 -0
  86. package/server/lib/plugin-loader.js +346 -90
  87. package/server/lib/plugin-manager.js +46 -2
  88. package/server/lib/plugin-manifest.js +329 -0
  89. package/server/lib/plugin-panels.js +183 -0
  90. package/server/lib/plugin-proxy.js +408 -0
  91. package/server/lib/plugin-runtime.js +775 -0
  92. package/server/lib/plugin-service-manager.js +463 -0
  93. package/server/lib/proxy/proxy-retry.js +1 -1
  94. package/server/lib/proxy/proxy-stats.js +1 -1
  95. package/server/lib/resume-watchdog.js +1 -1
  96. package/server/lib/same-origin.js +13 -3
  97. package/server/lib/sdk-manager.js +16 -9
  98. package/server/lib/sdk-session-index.js +1 -1
  99. package/server/lib/stats-worker.js +1 -1
  100. package/server/lib/system-prompt-files.js +1 -1
  101. package/server/lib/system-prompt-live.js +1 -1
  102. package/server/lib/system-prompt-snapshots.js +1 -1
  103. package/server/lib/v2/adapter.js +1 -1
  104. package/server/lib/v2/convert.js +1 -1
  105. package/server/lib/v2/jsonl-read.js +1 -1
  106. package/server/lib/v2/live-feed.js +1 -1
  107. package/server/lib/v2/meta-rows.js +1 -1
  108. package/server/lib/v2/resume-list.js +1 -1
  109. package/server/lib/v2/session-list.js +1 -1
  110. package/server/lib/v2/session-owner.js +1 -1
  111. package/server/lib/v2/v2-writer.js +1 -1
  112. package/server/lib/wire-compress.js +1 -1
  113. package/server/lib/ws-mux.js +1 -1
  114. package/server/proxy.js +1 -1
  115. package/server/pty-manager.js +1 -1
  116. package/server/routes/ask-perm.js +1 -1
  117. package/server/routes/events.js +1 -1
  118. package/server/routes/expert.js +1 -1
  119. package/server/routes/files-content.js +1 -1
  120. package/server/routes/files-fs.js +4 -2
  121. package/server/routes/misc.js +11 -0
  122. package/server/routes/plugin-services.js +104 -0
  123. package/server/routes/plugins.js +145 -17
  124. package/server/routes/preferences.js +1 -1
  125. package/server/routes/resume.js +1 -1
  126. package/server/routes/sdk-context.js +1 -1
  127. package/server/routes/sdk-mcp-status.js +1 -1
  128. package/server/routes/workspaces.js +1 -1
  129. package/server/server.js +328 -171
  130. package/dist/assets/App-CLteNB83.js +0 -2
  131. package/dist/assets/Mobile-CJCQf3MQ.js +0 -1
  132. package/dist/assets/ProxyStatsModal-B5ccoOPy.js +0 -1
  133. package/dist/assets/classDiagram-6PBFFD2Q-ClGF33bF.js +0 -1
  134. package/dist/assets/classDiagram-v2-HSJHXN6E-ClGF33bF.js +0 -1
  135. package/dist/assets/clone-CFj55h49.js +0 -1
  136. package/dist/assets/index-CqX5TkEL.js +0 -2
  137. package/dist/assets/seqResourceLoaders-7lsgbvGa.js +0 -2
  138. package/dist/assets/seqResourceLoaders-C0cU2e1o.css +0 -41
  139. package/dist/assets/vendor-antd-CS_S2mBN.js +0 -17
  140. package/dist/assets/vendor-codemirror-B5l-VbBO.js +0 -1
@@ -0,0 +1,463 @@
1
+ // Service-plugin child-process management (parent side).
2
+ // Spawns one detached child per (server process × service plugin): the child runs the
3
+ // plugin's service.mjs as its own HTTP server on 127.0.0.1:0, writes a lock file with
4
+ // its port, and the parent reverse-proxies /plugins/<name>/* to it (see plugin-proxy.js).
5
+ //
6
+ // Modeled on lib/im/im-process-manager.js: detached process group (group kill on stop),
7
+ // env prefix-stripping, lock-file handshake, liveness probing, test guard. Key
8
+ // differences: locks are per-server-PID (Electron runs several server processes over the
9
+ // same LOG_DIR, each gets its own plugin instance), identity includes a per-spawn nonce
10
+ // (prevents adopting a previous generation's child), and stale-lock cleanup verifies
11
+ // health identity before killing (a reused PID may belong to an unrelated process).
12
+ import { spawn as nodeSpawn, execSync, spawnSync } from 'node:child_process';
13
+ import { openSync, closeSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, readdirSync } from 'node:fs';
14
+ import { join, dirname } from 'node:path';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import { randomBytes } from 'node:crypto';
17
+ import { reportSwallowed } from './error-report.js';
18
+
19
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
+
21
+ const PLUGIN_HOST_PATH = join(dirname(fileURLToPath(import.meta.url)), 'plugin-host.mjs');
22
+
23
+ /** Resolve the real node binary (under Electron, process.execPath is the Electron binary). */
24
+ export function resolveNodeBinary() {
25
+ if (!process.versions.electron) return process.execPath;
26
+ try {
27
+ const out = execSync(process.platform === 'win32' ? 'where node' : 'which node', { encoding: 'utf-8', windowsHide: true });
28
+ const p = process.platform === 'win32' ? out.split('\n')[0].trim() : out.trim();
29
+ if (p) return p;
30
+ } catch { /* fall through */ }
31
+ // #129: packaged GUI environments may lack a shell PATH; fall back to Electron itself + ELECTRON_RUN_AS_NODE
32
+ return null; // null -> spawn with process.execPath + ELECTRON_RUN_AS_NODE=1
33
+ }
34
+
35
+ export function serviceRuntimeDir(logDir, name) {
36
+ return join(logDir, 'plugins-runtime', name);
37
+ }
38
+
39
+ export function serviceDataDir(logDir, name) {
40
+ return join(logDir, 'plugin-data', name);
41
+ }
42
+
43
+ export function serviceLockPath(runtimeDir, serverPid) {
44
+ return join(runtimeDir, `service-${serverPid}.lock`);
45
+ }
46
+
47
+ export function serviceConfigPath(runtimeDir, serverPid, generation) {
48
+ return join(runtimeDir, `config-${serverPid}-${generation}.json`);
49
+ }
50
+
51
+ export function serviceLogPath(runtimeDir, serverPid) {
52
+ return join(runtimeDir, `process-${serverPid}.log`);
53
+ }
54
+
55
+ // Inherited environment is an allowlist, not a denylist: ANTHROPIC_* / CCV_* secrets
56
+ // (auth tokens, passwords) must never reach a plugin child. /proc/<pid>/environ is
57
+ // readable by the same user, and argv/env leaks are not worth saving a config file read.
58
+ const CHILD_ENV_ALLOWLIST = new Set([
59
+ 'PATH', 'HOME', 'TMPDIR', 'TMP', 'TEMP', 'LANG', 'LC_ALL', 'LC_CTYPE',
60
+ 'SYSTEMROOT', 'COMSPEC', 'PATHEXT', 'TERM', 'SHELL', 'USER', 'LOGNAME',
61
+ 'HOSTNAME', 'NODE_ENV',
62
+ // Windows user-profile dirs: Node's os.homedir() throws without USERPROFILE, and many
63
+ // runtimes write caches under APPDATA/LOCALAPPDATA. None of these carry secrets.
64
+ 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
65
+ 'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)',
66
+ ]);
67
+
68
+ /**
69
+ * Build the service child environment from an explicit allowlist.
70
+ * Inherits only OS basics plus LC_* and CCV_PLUGIN_*; manifest.service.env is injected
71
+ * explicitly below. The plugin secret/nonce live only in the 0600 config file.
72
+ */
73
+ export function buildChildEnv({ name, version, pluginDir, configPath, lockPath, runtimeDir, dataDir, parentPort, parentBasePath, manifestEnv = {}, base = process.env }) {
74
+ const env = {};
75
+ for (const [k, v] of Object.entries(base)) {
76
+ const upper = k.toUpperCase();
77
+ if (CHILD_ENV_ALLOWLIST.has(upper) || upper.startsWith('LC_') || k.startsWith('CCV_PLUGIN_')) {
78
+ env[k] = v;
79
+ }
80
+ }
81
+ env.CCV_PLUGIN_NAME = name;
82
+ if (version) env.CCV_PLUGIN_VERSION = version;
83
+ env.CCV_PLUGIN_DIR = pluginDir;
84
+ env.CCV_PLUGIN_CONFIG = configPath;
85
+ env.CCV_PLUGIN_HOST = pathToFileURL(PLUGIN_HOST_PATH).href; // file:// URL so Windows can import it too
86
+ env.CCV_PLUGIN_LOCK = lockPath;
87
+ env.CCV_PLUGIN_RUNTIME_DIR = runtimeDir;
88
+ env.CCV_PLUGIN_DATA_DIR = dataDir;
89
+ env.CCV_PLUGIN_PARENT_PORT = String(parentPort || '');
90
+ env.CCV_PLUGIN_PARENT_BASE_PATH = parentBasePath || '';
91
+ for (const [k, v] of Object.entries(manifestEnv)) {
92
+ if (!k.startsWith('CCV_') && !k.startsWith('CCVIEWER_')) env[k] = v;
93
+ }
94
+ return env;
95
+ }
96
+
97
+ /**
98
+ * Spawn one service child. Returns { pid, child, lockPath, configPath, outLog, secret, nonce }
99
+ * or { blockedByTestGuard: true } (test guard).
100
+ *
101
+ * @param {object} desc { name, version, entryPath, pluginDir, prefix, env }
102
+ * @param {object} ctx { logDir, parentPort, parentBasePath, generation, serverPid, bootNonce }
103
+ * @param {object} opts { spawnImpl?, nodeBinary? } test injection points
104
+ */
105
+ export function spawnServiceProcess(desc, ctx, opts = {}) {
106
+ const runtimeDir = serviceRuntimeDir(ctx.logDir, desc.name);
107
+ const serverPid = ctx.serverPid ?? process.pid;
108
+ // ████████ test isolation guard (modeled on im-process-manager L4) ████████
109
+ if (process.env.NODE_TEST_CONTEXT && !opts.spawnImpl && process.env.CCV_TEST_ALLOW_PLUGIN_SPAWN !== '1') {
110
+ return { blockedByTestGuard: true, runtimeDir, pid: undefined };
111
+ }
112
+ // ████████████████████████████████████████████████████████████
113
+ const spawnImpl = opts.spawnImpl || nodeSpawn;
114
+ const dataDir = serviceDataDir(ctx.logDir, desc.name);
115
+ mkdirSync(runtimeDir, { recursive: true });
116
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 });
117
+
118
+ const generation = ctx.generation ?? 1;
119
+ const lockPath = serviceLockPath(runtimeDir, serverPid);
120
+ const configPath = serviceConfigPath(runtimeDir, serverPid, generation);
121
+ const outLog = serviceLogPath(runtimeDir, serverPid);
122
+ const secret = randomBytes(24).toString('hex');
123
+ const nonce = randomBytes(16).toString('hex');
124
+
125
+ // 0600 config: the plugin secret/nonce live only here (atomic write). The parent
126
+ // ACCESS_TOKEN is deliberately absent — loopback parent APIs need no token, and a
127
+ // secret sitting in config (or a URL query) is needless exposure.
128
+ const configBody = {
129
+ name: desc.name,
130
+ version: desc.version ?? null,
131
+ prefix: desc.prefix,
132
+ pluginDir: desc.pluginDir,
133
+ dataDir,
134
+ runtimeDir,
135
+ lockPath,
136
+ secret,
137
+ nonce,
138
+ generation,
139
+ parentPid: serverPid,
140
+ bootNonce: ctx.bootNonce ?? null, // lets cleanup detect a stale same-PID/different-boot lock
141
+ parentPort: ctx.parentPort ?? null,
142
+ parentBasePath: ctx.parentBasePath ?? '',
143
+ };
144
+ try {
145
+ const tmpPath = `${configPath}.tmp-${process.pid}`;
146
+ writeFileSync(tmpPath, JSON.stringify(configBody), { mode: 0o600 });
147
+ renameSync(tmpPath, configPath);
148
+ } catch (err) {
149
+ reportSwallowed('plugin-service.config-write', err);
150
+ }
151
+
152
+ let stdio = 'ignore';
153
+ let fd = null;
154
+ try {
155
+ fd = openSync(outLog, 'a');
156
+ stdio = ['ignore', fd, fd];
157
+ } catch { /* fall back to ignore when the log file cannot be opened */ }
158
+
159
+ const nodeBinary = opts.nodeBinary !== undefined ? opts.nodeBinary : resolveNodeBinary();
160
+ const spawnEnv = buildChildEnv({
161
+ name: desc.name,
162
+ version: desc.version,
163
+ pluginDir: desc.pluginDir,
164
+ configPath,
165
+ lockPath,
166
+ runtimeDir,
167
+ dataDir,
168
+ parentPort: ctx.parentPort,
169
+ parentBasePath: ctx.parentBasePath,
170
+ manifestEnv: desc.env,
171
+ });
172
+ let executable = nodeBinary || process.execPath;
173
+ if (!nodeBinary && process.versions.electron) {
174
+ // fall back to Electron itself as the node runtime
175
+ spawnEnv.ELECTRON_RUN_AS_NODE = '1';
176
+ } else {
177
+ delete spawnEnv.ELECTRON_RUN_AS_NODE;
178
+ }
179
+
180
+ const child = spawnImpl(executable, [desc.entryPath], {
181
+ cwd: desc.pluginDir,
182
+ env: spawnEnv,
183
+ stdio,
184
+ detached: true,
185
+ windowsHide: true,
186
+ });
187
+ // No unref: the parent must keep the event loop referenced while the child lives (detached
188
+ // does not mean unsupervised; stopServiceProcess/process-group kill owns cleanup). unref
189
+ // would let the parent exit while the child lives, and some runtimes harvest detached+unref children instantly (fake code=0 exit).
190
+ if (fd !== null) { try { closeSync(fd); } catch { /* child already duped it */ } }
191
+ return { pid: child?.pid, child, lockPath, configPath, outLog, secret, nonce, runtimeDir, dataDir };
192
+ }
193
+
194
+ export function readServiceLock(lockPath) {
195
+ try {
196
+ return JSON.parse(readFileSync(lockPath, 'utf-8'));
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ export function isPidAlive(pid) {
203
+ if (!pid || typeof pid !== 'number') return false;
204
+ try {
205
+ process.kill(pid, 0);
206
+ return true;
207
+ } catch (err) {
208
+ return err && err.code === 'EPERM';
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Wait for the child to become ready: poll lock (pid+name) -> GET /__ccv/health (secret + nonce).
214
+ * @returns {{ ok:true, port, pid, lock } | { ok:false, error, state }}
215
+ */
216
+ export async function waitForServiceReady(desc, opts = {}) {
217
+ const lockPath = opts.lockPath;
218
+ const secret = opts.secret;
219
+ const nonce = opts.nonce;
220
+ const expectedPid = opts.expectedPid;
221
+ const timeoutMs = opts.timeoutMs ?? 10000;
222
+ const pollMs = opts.pollMs ?? 50;
223
+ const probe = opts.probeImpl || probeServiceHealth;
224
+ const deadline = Date.now() + timeoutMs;
225
+
226
+ while (Date.now() < deadline) {
227
+ const lock = readServiceLock(lockPath);
228
+ if (lock && lock.pid && lock.port && lock.name === desc.name) {
229
+ if (expectedPid && lock.pid !== expectedPid) {
230
+ // lock overwritten by another process/previous generation - keep waiting for ours
231
+ } else {
232
+ const health = await probe(lock.port, secret, { timeoutMs: 400 });
233
+ if (health.ok && health.body && health.body.ok === true && health.body.name === desc.name) {
234
+ if (nonce && health.body.nonce !== nonce) {
235
+ return { ok: false, error: 'health nonce mismatch (stale or foreign service)', state: 'foreign' };
236
+ }
237
+ if (health.body.pid && health.body.pid !== lock.pid) {
238
+ return { ok: false, error: 'health pid mismatch', state: 'foreign' };
239
+ }
240
+ return { ok: true, port: lock.port, pid: lock.pid, lock };
241
+ }
242
+ if (!health.ok && health.state === 'foreign') {
243
+ return { ok: false, error: 'health identity mismatch', state: 'foreign' };
244
+ }
245
+ }
246
+ }
247
+ if (opts.isChildDead && opts.isChildDead()) {
248
+ return { ok: false, error: 'child exited before becoming ready', state: 'dead' };
249
+ }
250
+ await sleep(pollMs);
251
+ }
252
+ return { ok: false, error: `service did not become ready within ${timeoutMs}ms`, state: 'timeout' };
253
+ }
254
+
255
+ /**
256
+ * GET http://127.0.0.1:<port>/__ccv/health with secret header.
257
+ * x-cc-viewer-internal:1 lets the interceptor's fetch patch pass through (otherwise loopback
258
+ * requests are rewritten as API traffic to ANTHROPIC_BASE_URL - 2026-10-22 field incident:
259
+ * in CLI mode every health probe hit the upstream and the service never became ready).
260
+ * @returns {Promise<{ok:boolean, state?:string, body?:object}>}
261
+ */
262
+ export async function probeServiceHealth(port, secret, opts = {}) {
263
+ const timeoutMs = opts.timeoutMs ?? 400;
264
+ try {
265
+ const controller = new AbortController();
266
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
267
+ timer.unref?.();
268
+ const res = await fetch(`http://127.0.0.1:${port}/__ccv/health`, {
269
+ headers: { 'x-ccv-plugin-secret': secret, 'x-cc-viewer-internal': '1' },
270
+ signal: controller.signal,
271
+ });
272
+ clearTimeout(timer);
273
+ const body = await res.json().catch(() => null);
274
+ if (res.status === 401 || res.status === 403) return { ok: false, state: 'foreign', body };
275
+ return { ok: res.ok, body };
276
+ } catch (err) {
277
+ return { ok: false, error: err.message };
278
+ }
279
+ }
280
+
281
+ /** Default process-group kill (detached group leader); Windows uses taskkill /T /F; falls back to the single process. */
282
+ export function defaultKill(pid, signal) {
283
+ if (process.platform === 'win32' && signal === 'SIGKILL') {
284
+ try {
285
+ spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, timeout: 1000 });
286
+ return;
287
+ } catch { /* fall through */ }
288
+ }
289
+ try { process.kill(-pid, signal); return; } catch { /* not a group leader / windows */ }
290
+ try { process.kill(pid, signal); } catch { /* already gone */ }
291
+ }
292
+
293
+ /**
294
+ * Stop a service child: SIGTERM → poll for exit → SIGKILL on timeout → remove lock/config.
295
+ *
296
+ * A PID alone proves nothing — a crashed child's PID can be reused by an unrelated
297
+ * process, and killing it would be catastrophic. Signal only when the lock file confirms
298
+ * this exact PID (and, when both sides carry one, the same per-spawn nonce). Otherwise
299
+ * remove the stale files and leave the process alone.
300
+ */
301
+ export async function stopServiceProcess(name, opts = {}) {
302
+ const lockPath = opts.lockPath;
303
+ const configPath = opts.configPath;
304
+ const kill = opts.killImpl || defaultKill;
305
+ const isAlive = opts.isAlive || isPidAlive;
306
+ const timeoutMs = opts.timeoutMs ?? 2000;
307
+ const pollMs = opts.pollMs ?? 100;
308
+
309
+ const lock = lockPath ? readServiceLock(lockPath) : null;
310
+ const pid = opts.pid ?? lock?.pid;
311
+ const removeFiles = () => {
312
+ for (const p of [lockPath, configPath]) {
313
+ if (p) { try { unlinkSync(p); } catch { /* already gone */ } }
314
+ }
315
+ };
316
+ const lockMatchesPid = !!lock && !!pid && lock.pid === pid;
317
+ const nonceMatches = !opts.nonce || !lock?.nonce || lock.nonce === opts.nonce;
318
+ if (!pid || !lockMatchesPid || !nonceMatches || !isAlive(pid)) {
319
+ removeFiles();
320
+ return { stopped: true, alreadyDead: !isAlive(pid), skippedSignal: !lockMatchesPid || !nonceMatches };
321
+ }
322
+
323
+ kill(pid, 'SIGTERM');
324
+ const deadline = Date.now() + timeoutMs;
325
+ while (Date.now() < deadline) {
326
+ await sleep(pollMs);
327
+ if (!isAlive(pid)) {
328
+ removeFiles();
329
+ return { stopped: true };
330
+ }
331
+ }
332
+ kill(pid, 'SIGKILL');
333
+ await sleep(pollMs);
334
+ removeFiles();
335
+ return { stopped: true, forced: true };
336
+ }
337
+
338
+ /**
339
+ * Clean stale locks at startup:
340
+ * - parentPid dead: verify health identity first (kill only on nonce/pid match - the PID may be reused);
341
+ * - parentPid === this process but bootNonce differs: same-PID reboot; the old child is orphaned, treat as above;
342
+ * - parentPid alive and not this process: another cc-viewer instance's plugin, keep it.
343
+ *
344
+ * @param {string} logDir
345
+ * @param {{ bootNonce:string, isAlive?:Function, killImpl?:Function }} opts
346
+ */
347
+ export async function cleanupStaleServiceLocks(logDir, opts = {}) {
348
+ const bootNonce = opts.bootNonce;
349
+ const isAlive = opts.isAlive || isPidAlive;
350
+ const kill = opts.killImpl || defaultKill;
351
+ const runtimeRoot = join(logDir, 'plugins-runtime');
352
+ let pluginDirs;
353
+ try {
354
+ pluginDirs = readdirSync(runtimeRoot, { withFileTypes: true });
355
+ } catch {
356
+ return { cleaned: 0, kept: 0 };
357
+ }
358
+ let cleaned = 0;
359
+ let kept = 0;
360
+ for (const dirent of pluginDirs) {
361
+ if (!dirent.isDirectory()) continue;
362
+ const dir = join(runtimeRoot, dirent.name);
363
+ let lockFiles;
364
+ try {
365
+ lockFiles = readdirSync(dir).filter(f => f.startsWith('service-') && f.endsWith('.lock'));
366
+ } catch {
367
+ continue;
368
+ }
369
+ for (const lockFile of lockFiles) {
370
+ const lockPath = join(dir, lockFile);
371
+ const lock = readServiceLock(lockPath);
372
+ if (!lock || !lock.pid) {
373
+ try { unlinkSync(lockPath); } catch { /* noop */ }
374
+ continue;
375
+ }
376
+ const parentAlive = lock.parentPid && isAlive(lock.parentPid);
377
+ const ownParent = lock.parentPid === process.pid;
378
+ const bootMismatch = ownParent && bootNonce && lock.bootNonce && lock.bootNonce !== bootNonce;
379
+ if (parentAlive && !bootMismatch) {
380
+ kept++;
381
+ continue;
382
+ }
383
+ // parent dead or boot mismatch: verify the child's identity before killing
384
+ if (isAlive(lock.pid)) {
385
+ let identityConfirmed = false;
386
+ try {
387
+ // pick the config with the same serverPid and highest numeric generation (lexical order puts -10 before -9)
388
+ const lockServerPid = lockFile.slice('service-'.length, -'.lock'.length);
389
+ const configFiles = readdirSync(dir)
390
+ .filter(f => f.startsWith(`config-${lockServerPid}-`) && f.endsWith('.json'))
391
+ .sort((a, b) => {
392
+ const ga = Number(a.slice(`config-${lockServerPid}-`.length, -'.json'.length)) || 0;
393
+ const gb = Number(b.slice(`config-${lockServerPid}-`.length, -'.json'.length)) || 0;
394
+ return ga - gb;
395
+ });
396
+ const configPath = configFiles.length ? join(dir, configFiles[configFiles.length - 1]) : null;
397
+ const config = configPath ? JSON.parse(readFileSync(configPath, 'utf-8')) : null;
398
+ if (config && config.secret && lock.port) {
399
+ const health = await probeServiceHealth(lock.port, config.secret, { timeoutMs: 400 });
400
+ if (health.ok && health.body && health.body.pid === lock.pid) {
401
+ identityConfirmed = true;
402
+ }
403
+ }
404
+ } catch (err) {
405
+ reportSwallowed('plugin-service.stale-probe', err);
406
+ }
407
+ if (!identityConfirmed) {
408
+ // identity unconfirmed - the PID may be unrelated; clear the lock without killing
409
+ kept++;
410
+ try { unlinkSync(lockPath); } catch { /* noop */ }
411
+ continue;
412
+ }
413
+ kill(lock.pid, 'SIGTERM');
414
+ await sleep(300);
415
+ if (isAlive(lock.pid)) kill(lock.pid, 'SIGKILL');
416
+ }
417
+ try { unlinkSync(lockPath); } catch { /* noop */ }
418
+ // also clear configs for older generations of this serverPid (aborted startups and
419
+ // previous generations can leave them behind; crash cleanup removes the current one)
420
+ try {
421
+ const lockServerPid = lockFile.slice('service-'.length, -'.lock'.length);
422
+ for (const f of readdirSync(dir)) {
423
+ if (f.startsWith(`config-${lockServerPid}-`) && f.endsWith('.json')) {
424
+ try { unlinkSync(join(dir, f)); } catch { /* noop */ }
425
+ }
426
+ }
427
+ } catch { /* noop */ }
428
+ cleaned++;
429
+ }
430
+ }
431
+ return { cleaned, kept };
432
+ }
433
+
434
+ /**
435
+ * Restart backoff: at most 5 restarts per 60s window; the counter resets after 60s of stable uptime.
436
+ */
437
+ export function createRestartBackoff(opts = {}) {
438
+ const delays = opts.delays || [0, 500, 1000, 2000, 5000];
439
+ const windowMs = opts.windowMs ?? 60000;
440
+ const stableMs = opts.stableMs ?? 60000;
441
+ let crashes = [];
442
+ let lastStartAt = 0;
443
+ return {
444
+ markStarted() {
445
+ const now = Date.now();
446
+ if (lastStartAt && now - lastStartAt >= stableMs) crashes = [];
447
+ lastStartAt = now;
448
+ },
449
+ /** @returns {{ allowed:boolean, delayMs:number }} */
450
+ next() {
451
+ const now = Date.now();
452
+ crashes = crashes.filter(t => now - t < windowMs);
453
+ if (crashes.length >= delays.length) return { allowed: false, delayMs: 0 };
454
+ const delayMs = delays[crashes.length];
455
+ crashes.push(now);
456
+ return { allowed: true, delayMs };
457
+ },
458
+ reset() { crashes = []; },
459
+ get crashCount() { return crashes.length; },
460
+ };
461
+ }
462
+
463
+ export { PLUGIN_HOST_PATH };
@@ -17,7 +17,7 @@
17
17
  // - race/stagger use AbortController; cancelled requests must be released correctly.
18
18
  import { resolveProfileModel, replaceTopLevelSystem } from '../interceptor-core.js';
19
19
  import { readFileSync, existsSync } from 'node:fs';
20
- import { reportSwallowed } from '@ccv/core/error-report';
20
+ import { reportSwallowed } from '../../lib/error-report.js';
21
21
  import { liveSystemPromptEnabled, getLiveEntry, applyLiveSystem, knownInjectedTexts } from '../system-prompt-live.js';
22
22
  import { parseUserId } from '../session-id.js';
23
23
 
@@ -9,7 +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 '@ccv/core/error-report';
12
+ import { reportSwallowed } from '../../lib/error-report.js';
13
13
 
14
14
  // ── Detail record schema ──────────────────────────────────────────────
15
15
  // Appends one JSON line after each proxied LLM API request completes. Fields align with llm-retry-proxy's retry records.
@@ -12,7 +12,7 @@
12
12
  // affects the request or spawn paths.
13
13
  import { existsSync, readdirSync, statSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
- import { reportSwallowed } from '@ccv/core/error-report';
15
+ import { reportSwallowed } from '../lib/error-report.js';
16
16
  import { LOG_DIR } from '../../findcc.js';
17
17
  import { injectionConfigured } from './launch-config.js';
18
18
  import { readSnapshotByKey, transcriptDirForCwd, projectKeyForCwd } from './system-prompt-snapshots.js';
@@ -9,13 +9,23 @@
9
9
  //
10
10
  // Canonical implementation — every call site imports from here.
11
11
  export function isSameOriginBrowserRequest(req, parsedUrl) {
12
+ // Sec-Fetch-Site is set by browsers even on Origin-less subresource/no-cors GETs;
13
+ // check it first so a cross-site page cannot slip through the no-Origin CLI path.
14
+ if (req.headers?.['sec-fetch-site'] === 'cross-site') return false;
12
15
  const origin = req.headers?.origin;
13
16
  if (!origin) return true; // CLI/native clients do not send Origin.
14
- if (req.headers?.['sec-fetch-site'] === 'cross-site') return false;
15
17
  try {
16
18
  const originUrl = new URL(origin);
17
- if (parsedUrl?.origin) return originUrl.origin.toLowerCase() === parsedUrl.origin.toLowerCase();
18
- return originUrl.host.toLowerCase() === String(req.headers?.host || '').toLowerCase();
19
+ // Host is the invariant (already allowlist-guarded). Scheme can differ legitimately
20
+ // when a TLS-terminating reverse proxy fronts the server (browser sees https, the
21
+ // backend still parses http) — so compare host, and only when the proxy explicitly
22
+ // forwards the scheme (X-Forwarded-Proto) do we also require it to match.
23
+ const originHost = originUrl.host.toLowerCase();
24
+ const reqHost = String(parsedUrl?.host || req.headers?.host || '').toLowerCase();
25
+ if (originHost !== reqHost) return false;
26
+ const xfp = req.headers?.['x-forwarded-proto'];
27
+ if (xfp && originUrl.protocol.replace(':', '').toLowerCase() !== String(xfp).toLowerCase()) return false;
28
+ return true;
19
29
  } catch {
20
30
  return false;
21
31
  }
@@ -30,7 +30,7 @@ import { ASK_TIMEOUT_MS } from './ask/ask-constants.js';
30
30
  import { withDefaultThinkingDisplay, resolveLaunchSystemPrompt, launchArgsToExtraArgs } from './launch-config.js';
31
31
  import { evaluateImDeny } from './im-deny.js';
32
32
  import { APPROVAL_TOOLS, isOverTrustDir, isPublishCommand } from './approval-policy.js';
33
- import { reportSwallowed } from '@ccv/core/error-report';
33
+ import { reportSwallowed } from '../lib/error-report.js';
34
34
  import { resolve as _pathResolve, dirname as _pathDirname, sep as _pathSep } from 'node:path';
35
35
  import { createHash } from 'node:crypto';
36
36
  import { registerSdkSessionLister } from './sdk-session-index.js';
@@ -147,7 +147,7 @@ const MAX_QUEUE_ITEMS = 50;
147
147
  // Callbacks registered by cli.js (one wiring for the whole process; every session
148
148
  // reuses the same callbacks — they broadcast to the shared terminal WS).
149
149
  let _onTurnEnd = null; // SDK mode has no Stop hook (ensureHooks() skipped) — fire turnEnd directly when the SDK 'result' message arrives
150
- let _onQueryError = null; // query-level failure → (message: string) => void; cli.js forwards to a WS toast
150
+ let _onQueryError = null; // query-level failure → (message: string, meta: {cwd, sessionId, instanceKey}) => void; cli.js forwards it as a scoped sdk-error WS frame (dialogue row + toast)
151
151
  let _broadcastWs = null;
152
152
  let _runWaterfallHook = null;
153
153
 
@@ -710,10 +710,12 @@ function _createSession(key, cwd) {
710
710
  }
711
711
  }
712
712
 
713
- /** Turn-level error toast (callback throw must never break the caller). */
713
+ /** Query-level error signal; the routing envelope always comes from this
714
+ * session's closure (callback throw must never break the caller). */
714
715
  function _notifyQueryError(err) {
715
716
  if (!_onQueryError) return;
716
- try { _onQueryError(String(err?.message || err)); }
717
+ const context = { cwd: _cwd, sessionId: _sessionId || null, instanceKey };
718
+ try { _onQueryError(String(err?.message || err), context); }
717
719
  catch (cbErr) { console.warn('[sdk-manager] onQueryError threw:', cbErr?.message); }
718
720
  }
719
721
 
@@ -883,7 +885,8 @@ function _createSession(key, cwd) {
883
885
 
884
886
  /**
885
887
  * Surface a failed turn to the Web UI — the wire path persists the failed entry but
886
- * never pushes a toast, so a dead/errored turn would otherwise look like a hang.
888
+ * never signals it, so a dead/errored turn would otherwise look like a hang. The
889
+ * sdk-error frame renders as a persistent dialogue row plus a toast client-side.
887
890
  */
888
891
  function _notifyTurnError(resultMsg) {
889
892
  const isError = resultMsg.is_error === true || (resultMsg.subtype && resultMsg.subtype !== 'success');
@@ -1674,9 +1677,9 @@ export function isSdkAvailable() {
1674
1677
  * @param {string} projectName
1675
1678
  * @param {object} deps
1676
1679
  * @param {function} deps.onTurnEnd — ({sessionId, ts}) => void, fired on SDK 'result'
1677
- * @param {function} [deps.onQueryError] — (message: string) => void, fired when the
1678
- * query itself fails (spawn/protocol/iterator errors) — until this existed the only
1679
- * surfacing was console.error, invisible in the Web UI
1680
+ * @param {function} [deps.onQueryError] — (message: string, meta: {cwd, sessionId, instanceKey}) => void,
1681
+ * fired when the query itself fails (spawn/protocol/iterator errors); meta carries the session's
1682
+ * routing envelope so the sdk-error frame scopes to the owning workspace's viewers
1680
1683
  * @param {function} deps.broadcastWs — (msg) => void, terminal-WS broadcast for approvals
1681
1684
  * @param {string} [deps.permissionMode]
1682
1685
  * @param {function} [deps.runWaterfallHook] — plugin waterfall (onPlanRequest/onAskRequest/onPermRequest)
@@ -1727,7 +1730,11 @@ export async function sendUserMessage(text, anchor) {
1727
1730
  console.warn(`[sdk-manager] no SDK session registered for "${targetName}" — message dropped`);
1728
1731
  reportSwallowed('sdk-manager.route', new Error(`no SDK session for ${targetName}`));
1729
1732
  if (_onQueryError) {
1730
- try { _onQueryError(`Workspace "${targetName}" is no longer available — message not sent.`); }
1733
+ // Module-level fallback has no session closure. Anchors never carry cwd
1734
+ // (server.js builds {project, instanceKey} only) — pass the project key so
1735
+ // broadcastWsMessage can scope the frame; instanceKey narrows further when present.
1736
+ const meta = { cwd: anchor.cwd || null, sessionId: null, instanceKey: anchor.instanceKey || null, project: anchor.project || null };
1737
+ try { _onQueryError(`Workspace "${targetName}" is no longer available — message not sent.`, meta); }
1731
1738
  catch (err) { console.warn('[sdk-manager] onQueryError threw:', err?.message); }
1732
1739
  }
1733
1740
  return;
@@ -1,6 +1,6 @@
1
1
  // Synchronous read-only projection of the SDK session registry. Generic view
2
2
  // routing must not import sdk-manager (and eagerly load the Agent SDK) in PTY mode.
3
- import { reportSwallowed } from '@ccv/core/error-report';
3
+ import { reportSwallowed } from '../lib/error-report.js';
4
4
 
5
5
  let _listSessions = null;
6
6
 
@@ -10,7 +10,7 @@ import { join, basename } from 'node:path';
10
10
  import { readJsonlTolerant, listSessionIds } from './v2/replay.js';
11
11
  import { dirSizeSync } from './v2/layout.js';
12
12
  import { isDiscardableSession } from './v2/session-select.js';
13
- import { reportSwallowed } from '@ccv/core/error-report';
13
+ import { reportSwallowed } from '../lib/error-report.js';
14
14
  import {
15
15
  INTER_SESSION_TYPES, isSystemText, extractUserTexts, isSuggestionMode,
16
16
  collectPromptsFromEvents, sortEpochFiles,
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { MODEL_PROMPT_DIR, matchModelPrompt } from './model-system-prompts.js';
4
4
  import { isBuiltinDisabled, matchBuiltinModelPrompt, materializeBuiltinPrompt } from './builtin-model-prompts.js';
5
5
  import { isNonEmptyFile } from './file-api.js';
6
- import { reportSwallowed } from '@ccv/core/error-report';
6
+ import { reportSwallowed } from '../lib/error-report.js';
7
7
 
8
8
  // isNonEmptyFile lives in file-api.js (shared leaf, cycle break:
9
9
  // model-system-prompts.js must not import this module). Re-exported here so
@@ -19,7 +19,7 @@
19
19
  import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, statSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { randomBytes } from 'node:crypto';
22
- import { reportSwallowed } from '@ccv/core/error-report';
22
+ import { reportSwallowed } from '../lib/error-report.js';
23
23
  import { LOG_DIR } from '../../findcc.js';
24
24
  import { renameSyncWithRetry } from './file-api.js';
25
25
  import { matchModelPrompt, MODEL_PROMPT_DIR, readModelPrompt } from './model-system-prompts.js';
@@ -33,7 +33,7 @@
33
33
  import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, statSync, realpathSync, openSync, readSync, closeSync } from 'node:fs';
34
34
  import { join, basename, dirname } from 'node:path';
35
35
  import { randomBytes } from 'node:crypto';
36
- import { reportSwallowed } from '@ccv/core/error-report';
36
+ import { reportSwallowed } from '../lib/error-report.js';
37
37
  import { LOG_DIR, getClaudeConfigDir } from '../../findcc.js';
38
38
  import { renameSyncWithRetry } from './file-api.js';
39
39
 
@@ -29,7 +29,7 @@
29
29
 
30
30
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
31
31
  import { join, dirname, basename } from 'node:path';
32
- import { reportSwallowed } from '@ccv/core/error-report';
32
+ import { reportSwallowed } from '../../lib/error-report.js';
33
33
  import { isMainAgentRequest } from '../interceptor-core.js';
34
34
  import { readSession } from './replay.js';
35
35
  import { iterateJsonlLines } from './jsonl-read.js';
@@ -28,7 +28,7 @@ import { join, basename } from 'node:path';
28
28
  import { iterateRawEntriesAsync } from '../log-stream.js';
29
29
  import { createIncrementalReconstructor } from '@ccv/core/delta-reconstructor';
30
30
  import { isLogFileName, parseLogTs } from '../log-file-utils.js';
31
- import { reportSwallowed } from '@ccv/core/error-report';
31
+ import { reportSwallowed } from '../../lib/error-report.js';
32
32
  import { V2Writer } from './v2-writer.js';
33
33
  import { verifyV1File } from './verify.js';
34
34
  import { parseUserId } from './identity.js';