yingclaw 2.5.31 → 2.5.38

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.
package/lib/desktop.js CHANGED
@@ -3,7 +3,7 @@ const fs = require('fs');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
  const { spawnSync } = require('child_process');
6
- const { normalizeAnthropicBaseUrl } = require('./config');
6
+ const { CLEAR_CLAUDE_ENV_KEYS, normalizeAnthropicBaseUrl } = require('./config');
7
7
  const {
8
8
  YINGCLAW_GATEWAY_PREFIX,
9
9
  buildDesktopGatewayRoutes,
@@ -13,6 +13,7 @@ const {
13
13
  const CLAUDE_DESKTOP_LABEL = 'Claude 桌面应用配置';
14
14
  const YINGCLAW_ENTRY_NAME = 'yingclaw';
15
15
  const MAC_POLICY_BUNDLE = 'com.anthropic.claudefordesktop';
16
+ const CLAUDE_CODE_SETTINGS_SCHEMA = 'https://json.schemastore.org/claude-code-settings.json';
16
17
  const DESKTOP_GATEWAY_KEYS = [
17
18
  'inferenceProvider',
18
19
  'inferenceGatewayBaseUrl',
@@ -96,6 +97,11 @@ function readJsonFile(file) {
96
97
  }
97
98
  }
98
99
 
100
+ function writeJsonFile(file, value) {
101
+ fs.mkdirSync(path.dirname(file), { recursive: true });
102
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
103
+ }
104
+
99
105
  function toDesktopModelId(model) {
100
106
  return model.startsWith('claude-') ? model : `claude-${model}`;
101
107
  }
@@ -112,6 +118,215 @@ function buildGatewayBaseUrl(config) {
112
118
  return `http://127.0.0.1:${port}${YINGCLAW_GATEWAY_PREFIX}`;
113
119
  }
114
120
 
121
+ function getClaudeCodeSettingsPath(options = {}) {
122
+ const homeDir = options.homeDir || os.homedir();
123
+ return options.settingsFile || path.join(homeDir, '.claude', 'settings.json');
124
+ }
125
+
126
+ function getClaudePluginCacheDir(options = {}) {
127
+ const homeDir = options.homeDir || os.homedir();
128
+ return options.pluginCacheDir || path.join(homeDir, '.claude', 'plugins', 'cache');
129
+ }
130
+
131
+ function getProcessStart(pid, options = {}) {
132
+ const runner = options.processRunner || options.runner || spawnSync;
133
+ const platform = options.platform || process.platform;
134
+ const pidText = String(pid);
135
+ let result;
136
+ if (platform === 'win32') {
137
+ result = runner('powershell.exe', [
138
+ '-NoProfile',
139
+ '-Command',
140
+ `$p = Get-Process -Id ${pidText} -ErrorAction SilentlyContinue; if ($p) { $p.StartTime.ToUniversalTime().ToString('o') }`,
141
+ ], { stdio: 'pipe', encoding: 'utf8', windowsHide: true });
142
+ } else {
143
+ result = runner('ps', ['-p', pidText, '-o', 'lstart='], { stdio: 'pipe', encoding: 'utf8' });
144
+ }
145
+ if (result.status !== 0) return null;
146
+ return (result.stdout || '').toString().trim() || null;
147
+ }
148
+
149
+ function readLockProcStart(file) {
150
+ try {
151
+ const raw = fs.readFileSync(file, 'utf8');
152
+ const parsed = JSON.parse(raw);
153
+ return typeof parsed.procStart === 'string' ? parsed.procStart.trim() : null;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function walkFiles(dir, visitor) {
160
+ let entries;
161
+ try {
162
+ entries = fs.readdirSync(dir, { withFileTypes: true });
163
+ } catch {
164
+ return;
165
+ }
166
+ for (const entry of entries) {
167
+ const file = path.join(dir, entry.name);
168
+ if (entry.isDirectory()) {
169
+ walkFiles(file, visitor);
170
+ } else if (entry.isFile()) {
171
+ visitor(file);
172
+ }
173
+ }
174
+ }
175
+
176
+ function cleanupStaleClaudePluginLocks(options = {}) {
177
+ const pluginCacheDir = getClaudePluginCacheDir(options);
178
+ if (!pluginCacheDir || !fs.existsSync(pluginCacheDir)) {
179
+ return { result: 'missing', checked: 0, removed: 0, kept: 0 };
180
+ }
181
+
182
+ let checked = 0;
183
+ let removed = 0;
184
+ let kept = 0;
185
+ const processStartCache = new Map();
186
+
187
+ walkFiles(pluginCacheDir, (file) => {
188
+ if (path.basename(path.dirname(file)) !== '.in_use') return;
189
+ const pid = path.basename(file);
190
+ if (!/^\d+$/.test(pid)) return;
191
+
192
+ checked += 1;
193
+ if (!processStartCache.has(pid)) {
194
+ processStartCache.set(pid, getProcessStart(pid, options));
195
+ }
196
+ const currentProcStart = processStartCache.get(pid);
197
+ const lockProcStart = readLockProcStart(file);
198
+ const isStale = !currentProcStart || (lockProcStart && lockProcStart !== currentProcStart);
199
+
200
+ if (!isStale) {
201
+ kept += 1;
202
+ return;
203
+ }
204
+
205
+ try {
206
+ fs.unlinkSync(file);
207
+ removed += 1;
208
+ } catch {
209
+ kept += 1;
210
+ }
211
+ });
212
+
213
+ return { result: removed > 0 ? 'updated' : 'unchanged', checked, removed, kept };
214
+ }
215
+
216
+ function findDesktopRoute(routes, family) {
217
+ return routes.find((route) => route && route.id.includes(`claude-${family}-`))?.id;
218
+ }
219
+
220
+ function buildClaudeDesktopCodeEnv(config, options = {}) {
221
+ const gatewayConfig = ensureDesktopGatewayConfig(config, options);
222
+ const routes = buildDesktopGatewayRoutes(gatewayConfig);
223
+ const fallback = routes[0]?.id || 'claude-sonnet-4-6';
224
+ const sonnet = findDesktopRoute(routes, 'sonnet') || fallback;
225
+ const haiku = findDesktopRoute(routes, 'haiku') || routes[1]?.id || sonnet;
226
+ const opus = findDesktopRoute(routes, 'opus') || sonnet;
227
+
228
+ return {
229
+ ANTHROPIC_BASE_URL: buildGatewayBaseUrl(gatewayConfig),
230
+ ANTHROPIC_AUTH_TOKEN: gatewayConfig.desktopGatewayKey,
231
+ ANTHROPIC_MODEL: sonnet,
232
+ ANTHROPIC_DEFAULT_OPUS_MODEL: opus,
233
+ ANTHROPIC_DEFAULT_SONNET_MODEL: sonnet,
234
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: haiku,
235
+ CLAUDE_CODE_SUBAGENT_MODEL: haiku,
236
+ CLAUDE_CODE_EFFORT_LEVEL: 'low',
237
+ CLAUDE_CODE_SIMPLE: '1',
238
+ };
239
+ }
240
+
241
+ function buildClaudeDesktopLaunchEnv(env = process.env, overrides = {}) {
242
+ const next = { ...env };
243
+ for (const key of CLEAR_CLAUDE_ENV_KEYS) {
244
+ delete next[key];
245
+ }
246
+ return { ...next, ...overrides };
247
+ }
248
+
249
+ function shouldWriteClaudeDesktopCodeSettings(options = {}) {
250
+ return options.withCode === true && options.direct !== true;
251
+ }
252
+
253
+ function shouldPromptClaudeDesktopCodeSettings(options = {}) {
254
+ return options.direct !== true && options.withCode !== true;
255
+ }
256
+
257
+ function writeClaudeDesktopCodeSettings(config, options = {}) {
258
+ const file = getClaudeCodeSettingsPath(options);
259
+ const current = readJsonFile(file);
260
+ const env = {
261
+ ...(current.env && typeof current.env === 'object' ? current.env : {}),
262
+ ...buildClaudeDesktopCodeEnv(config, options),
263
+ };
264
+ const next = {
265
+ ...current,
266
+ $schema: current.$schema || CLAUDE_CODE_SETTINGS_SCHEMA,
267
+ env,
268
+ };
269
+ writeJsonFile(file, next);
270
+ try {
271
+ fs.chmodSync(file, 0o600);
272
+ } catch {}
273
+ return { result: 'updated', file };
274
+ }
275
+
276
+ function checkClaudeDesktopCodeSettingsEnv(config, options = {}) {
277
+ const file = getClaudeCodeSettingsPath(options);
278
+ const settings = readJsonFile(file);
279
+ const env = settings.env && typeof settings.env === 'object' ? settings.env : {};
280
+ const expected = buildClaudeDesktopCodeEnv(config, options);
281
+ const missing = [];
282
+ const mismatched = [];
283
+ const hasYingclawEnv = Object.keys(expected).some((key) => Object.prototype.hasOwnProperty.call(env, key));
284
+
285
+ for (const [key, value] of Object.entries(expected)) {
286
+ if (!Object.prototype.hasOwnProperty.call(env, key)) {
287
+ missing.push(key);
288
+ } else if (String(env[key]) !== String(value)) {
289
+ mismatched.push(key);
290
+ }
291
+ }
292
+
293
+ return {
294
+ file,
295
+ configured: missing.length === 0 && mismatched.length === 0,
296
+ hasYingclawEnv,
297
+ simpleEnabled: env.CLAUDE_CODE_SIMPLE === '1',
298
+ missing,
299
+ mismatched,
300
+ baseUrl: env.ANTHROPIC_BASE_URL || null,
301
+ model: env.ANTHROPIC_MODEL || null,
302
+ };
303
+ }
304
+
305
+ function clearClaudeDesktopCodeSettings(options = {}) {
306
+ const file = getClaudeCodeSettingsPath(options);
307
+ if (!fs.existsSync(file)) return { result: 'missing', file };
308
+
309
+ const current = readJsonFile(file);
310
+ const nextEnv = { ...(current.env && typeof current.env === 'object' ? current.env : {}) };
311
+ let changed = false;
312
+ for (const key of CLEAR_CLAUDE_ENV_KEYS) {
313
+ if (Object.prototype.hasOwnProperty.call(nextEnv, key)) {
314
+ delete nextEnv[key];
315
+ changed = true;
316
+ }
317
+ }
318
+ if (!changed) return { result: 'missing', file };
319
+
320
+ const next = { ...current };
321
+ if (Object.keys(nextEnv).length > 0) {
322
+ next.env = nextEnv;
323
+ } else {
324
+ delete next.env;
325
+ }
326
+ writeJsonFile(file, next);
327
+ return { result: 'updated', file };
328
+ }
329
+
115
330
  function serializeGatewayModels(config) {
116
331
  return buildDesktopGatewayRoutes(config).map((route) => {
117
332
  const item = { name: route.id, displayName: route.displayName };
@@ -463,10 +678,15 @@ async function openClaudeDesktop(options = {}) {
463
678
  const runner = options.runner || spawnSync;
464
679
  const isMocked = options.runner !== undefined;
465
680
  const timeoutMs = options.timeoutMs || 5000;
681
+ const launchEnv = buildClaudeDesktopLaunchEnv(options.env || process.env, options.injectEnv || {});
682
+ const shouldCleanupPluginLocks = options.cleanupPluginLocks !== false;
466
683
 
467
684
  const trace = [];
685
+ let pluginLocks = shouldCleanupPluginLocks
686
+ ? [cleanupStaleClaudePluginLocks(options)]
687
+ : [];
468
688
  for (const { command, args, optional, waitAfter, shell } of commands) {
469
- const result = runner(command, args, { stdio: 'pipe', encoding: 'utf8', windowsHide: true, timeout: timeoutMs, shell });
689
+ const result = runner(command, args, { stdio: 'pipe', encoding: 'utf8', windowsHide: true, timeout: timeoutMs, shell, env: launchEnv });
470
690
  const stderr = (result.stderr || '').toString().trim();
471
691
  trace.push({ command, args, status: result.status, stderr });
472
692
 
@@ -478,24 +698,36 @@ async function openClaudeDesktop(options = {}) {
478
698
  if (waitAfter && !isMocked) {
479
699
  await sleep(waitAfter);
480
700
  }
701
+ if (shouldCleanupPluginLocks && (command === 'pkill' || command === 'taskkill')) {
702
+ pluginLocks.push(cleanupStaleClaudePluginLocks(options));
703
+ }
481
704
  }
482
705
 
483
- return { result: 'reopened', trace };
706
+ return { result: 'reopened', trace, pluginLocks };
484
707
  }
485
708
 
486
709
  module.exports = {
710
+ buildClaudeDesktopCodeEnv,
487
711
  buildClaudeDesktopEnterpriseConfig,
488
712
  buildClaudeDesktopDirectEnterpriseConfig,
713
+ buildClaudeDesktopLaunchEnv,
489
714
  buildClaudeDesktopMacDefaultsCommands,
490
715
  buildClaudeDesktopMacDefaultsDeleteCommands,
491
716
  buildClaudeDesktopOpenCommands,
717
+ cleanupStaleClaudePluginLocks,
718
+ checkClaudeDesktopCodeSettingsEnv,
719
+ clearClaudeDesktopCodeSettings,
492
720
  clearClaudeDesktopConfig,
721
+ getClaudeCodeSettingsPath,
493
722
  getClaudeDesktopConfigLibraryDir,
494
723
  getClaudeDesktopConfigPath,
495
724
  getClaudeDesktopDataDir,
496
725
  getClaudeDesktopDataDirs,
497
726
  isDesktopConfigured,
498
727
  openClaudeDesktop,
728
+ shouldPromptClaudeDesktopCodeSettings,
729
+ shouldWriteClaudeDesktopCodeSettings,
730
+ writeClaudeDesktopCodeSettings,
499
731
  writeClaudeDesktopConfig,
500
732
  CLAUDE_DESKTOP_LABEL,
501
733
  };
package/lib/doctor.js CHANGED
@@ -2,6 +2,9 @@ const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
4
  const { execSync, spawnSync } = require('child_process');
5
+ const {
6
+ getGatewayAutostartStatus,
7
+ } = require('./autostart');
5
8
  const {
6
9
  loadConfig,
7
10
  validateConfig,
@@ -10,8 +13,9 @@ const {
10
13
  normalizeAnthropicBaseUrl,
11
14
  PROVIDERS,
12
15
  CLAUDE_ENV_KEYS,
16
+ CLEAR_CLAUDE_ENV_KEYS,
13
17
  } = require('./config');
14
- const { isDesktopConfigured } = require('./desktop');
18
+ const { checkClaudeDesktopCodeSettingsEnv, getClaudeDesktopDataDirs, isDesktopConfigured } = require('./desktop');
15
19
  const { checkClaudeCodeSettingsEnv } = require('./vscode');
16
20
 
17
21
  const STATUS_OK = 'ok';
@@ -47,7 +51,7 @@ async function runDoctorChecks(options = {}) {
47
51
  });
48
52
 
49
53
  // 3. 配置文件
50
- const config = loadConfig();
54
+ const config = options.config || loadConfig();
51
55
  if (!config) {
52
56
  checks.push({
53
57
  name: '配置文件',
@@ -142,6 +146,23 @@ async function runDoctorChecks(options = {}) {
142
146
  message: desktopConfigured ? '已通过 yingclaw 接入' : '未接入(如需运行 claw desktop)',
143
147
  });
144
148
 
149
+ const desktopCode = checkClaudeDesktopCodeSettingsEnv(config, options);
150
+ const desktopCodeProblems = desktopCode.missing.length + desktopCode.mismatched.length;
151
+ checks.push({
152
+ name: 'Claude Desktop Code',
153
+ status: desktopCode.configured ? STATUS_OK : desktopCode.hasYingclawEnv ? STATUS_WARN : STATUS_INFO,
154
+ message: desktopCode.configured
155
+ ? 'Code 模式环境已配置 · simple 启用'
156
+ : desktopCode.hasYingclawEnv
157
+ ? `${desktopCodeProblems} 项配置未写入或不匹配`
158
+ : '未接入(可选;如需 Desktop Code 运行 claw desktop --with-code)',
159
+ fix: desktopCode.configured || !desktopCode.hasYingclawEnv ? null : '运行 claw desktop --with-code,并完全重启 Claude 桌面应用',
160
+ });
161
+
162
+ if (platform === 'win32' || options.windows) {
163
+ checks.push(...buildWindowsDoctorChecks(config, options));
164
+ }
165
+
145
166
  // 9. DeepSeek 旧模型名提醒
146
167
  if (config.provider === 'deepseek' && (
147
168
  config.model === 'deepseek-v4-pro' ||
@@ -184,17 +205,194 @@ function checkWindowsEnvVars(options = {}) {
184
205
  return { allWritten: missing.length === 0, missing };
185
206
  }
186
207
 
208
+ function readConfigMirror(file) {
209
+ try {
210
+ return JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')));
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ function checkWindowsDesktopConfigMirrors(options = {}) {
217
+ const dataDirs = options.dataDirs || getClaudeDesktopDataDirs({ ...options, platform: 'win32' });
218
+ const files = dataDirs.map((dir) => path.join(dir, 'claude_desktop_config.json'));
219
+ const existing = files.filter((file) => fs.existsSync(file));
220
+ const missing = files.filter((file) => !fs.existsSync(file));
221
+ if (existing.length === 0) return { status: 'missing', files: existing, missing };
222
+ if (existing.length !== files.length) return { status: 'partial', files: existing, missing };
223
+ const contents = existing.map(readConfigMirror);
224
+ if (contents.some((content) => content == null)) return { status: 'invalid', files: existing, missing };
225
+ return {
226
+ status: new Set(contents).size === 1 ? 'synced' : 'mismatch',
227
+ files: existing,
228
+ missing,
229
+ };
230
+ }
231
+
232
+ function checkWindowsClaudeProcesses(options = {}) {
233
+ const runner = options.runner || spawnSync;
234
+ const result = runner('powershell.exe', [
235
+ '-NoProfile',
236
+ '-Command',
237
+ `$p = @(Get-Process Claude -ErrorAction SilentlyContinue); [pscustomobject]@{count=$p.Count;pids=@($p.Id)} | ConvertTo-Json -Compress`,
238
+ ], { stdio: 'pipe', encoding: 'utf8', windowsHide: true });
239
+ if (result.status !== 0) return { running: false, count: 0, pids: [] };
240
+ try {
241
+ const parsed = JSON.parse(String(result.stdout || '').trim() || '{}');
242
+ const pids = Array.isArray(parsed.pids)
243
+ ? parsed.pids.map(Number).filter(Number.isFinite)
244
+ : Number.isFinite(Number(parsed.pids)) ? [Number(parsed.pids)] : [];
245
+ return { running: Number(parsed.count) > 0, count: Number(parsed.count) || 0, pids };
246
+ } catch {
247
+ return { running: false, count: 0, pids: [] };
248
+ }
249
+ }
250
+
251
+ function buildWindowsDoctorChecks(config = {}, options = {}) {
252
+ const checks = [];
253
+ const autostart = getGatewayAutostartStatus({
254
+ ...options,
255
+ platform: 'win32',
256
+ file: options.startupFile || options.file,
257
+ port: config.desktopGatewayPort || options.port || 18080,
258
+ });
259
+ checks.push({
260
+ name: 'Windows Gateway 端口',
261
+ status: autostart.running ? STATUS_OK : STATUS_WARN,
262
+ message: autostart.running ? `127.0.0.1:${config.desktopGatewayPort || options.port || 18080} 正在监听` : '未监听',
263
+ fix: autostart.running ? null : '运行 claw gateway,或重新执行 claw desktop 设置自动启动',
264
+ });
265
+ checks.push({
266
+ name: 'Windows Gateway 自动启动',
267
+ status: autostart.installed ? STATUS_OK : STATUS_WARN,
268
+ message: autostart.installed ? `已写入 ${autostart.file}` : '未写入启动脚本',
269
+ fix: autostart.installed ? null : '运行 claw desktop 重新写入启动脚本',
270
+ });
271
+
272
+ const mirror = checkWindowsDesktopConfigMirrors(options);
273
+ const mirrorOk = mirror.status === 'synced' || mirror.status === 'missing';
274
+ checks.push({
275
+ name: 'Windows Claude 配置镜像',
276
+ status: mirrorOk ? STATUS_OK : STATUS_WARN,
277
+ message: mirror.status === 'synced'
278
+ ? 'Roaming / Local 配置一致'
279
+ : mirror.status === 'missing'
280
+ ? '未找到桌面 3P 配置'
281
+ : `${mirror.status}:${mirror.files.length} 个存在,${mirror.missing.length} 个缺失`,
282
+ fix: mirrorOk ? null : '运行 claw desktop 同步 Roaming 和 Local 配置',
283
+ });
284
+
285
+ const claude = checkWindowsClaudeProcesses(options);
286
+ checks.push({
287
+ name: 'Windows Claude 进程',
288
+ status: STATUS_INFO,
289
+ message: claude.running ? `正在运行 ${claude.count} 个 Claude 进程` : '未运行',
290
+ fix: claude.running ? '如配置未生效,请从系统托盘退出 Claude 后重开' : null,
291
+ });
292
+ return checks;
293
+ }
294
+
187
295
  function summarize(checks) {
188
296
  const counts = { ok: 0, fail: 0, warn: 0, info: 0 };
189
297
  for (const c of checks) counts[c.status] = (counts[c.status] || 0) + 1;
190
298
  return counts;
191
299
  }
192
300
 
301
+ function redactEnvValue(key, env, expectedEnv) {
302
+ const present = Object.prototype.hasOwnProperty.call(env, key);
303
+ const expected = Object.prototype.hasOwnProperty.call(expectedEnv, key);
304
+ const sensitive = key.includes('TOKEN') || key.includes('KEY');
305
+ return {
306
+ present,
307
+ expected,
308
+ matchesExpected: present && expected ? String(env[key]) === String(expectedEnv[key]) : false,
309
+ value: present ? (sensitive ? '[redacted]' : String(env[key])) : null,
310
+ };
311
+ }
312
+
313
+ function buildDiagnosticReport(options = {}) {
314
+ const config = options.config || null;
315
+ const checks = options.checks || [];
316
+ const env = options.env || process.env;
317
+ const expectedEnv = options.expectedEnv || (config ? buildClaudeEnv(config) : {});
318
+ const provider = config ? PROVIDERS[config.provider] : null;
319
+ const envKeys = [...new Set([...CLEAR_CLAUDE_ENV_KEYS, ...Object.keys(expectedEnv)])];
320
+
321
+ return {
322
+ generatedAt: options.now || new Date().toISOString(),
323
+ platform: options.platform || process.platform,
324
+ nodeVersion: options.nodeVersion || process.version,
325
+ packageVersion: options.packageVersion || null,
326
+ config: config ? {
327
+ provider: config.provider,
328
+ providerName: config.providerName || provider?.name || config.provider,
329
+ baseUrl: normalizeAnthropicBaseUrl(config.baseUrl),
330
+ model: config.model,
331
+ fastModel: config.fastModel || null,
332
+ availableModelCount: Array.isArray(config.availableModels) ? config.availableModels.length : 0,
333
+ hasApiKey: Boolean(config.apiKey),
334
+ desktopGatewayPort: config.desktopGatewayPort || null,
335
+ hasDesktopGatewayKey: Boolean(config.desktopGatewayKey),
336
+ } : null,
337
+ checks: checks.map((check) => ({
338
+ name: check.name,
339
+ status: check.status,
340
+ message: check.message,
341
+ fix: check.fix || null,
342
+ })),
343
+ env: Object.fromEntries(envKeys.map((key) => [key, redactEnvValue(key, env, expectedEnv)])),
344
+ desktopCode: options.desktopCodeStatus ? {
345
+ configured: options.desktopCodeStatus.configured,
346
+ simpleEnabled: options.desktopCodeStatus.simpleEnabled,
347
+ file: options.desktopCodeStatus.file,
348
+ missing: options.desktopCodeStatus.missing || [],
349
+ mismatched: options.desktopCodeStatus.mismatched || [],
350
+ baseUrl: options.desktopCodeStatus.baseUrl || null,
351
+ model: options.desktopCodeStatus.model || null,
352
+ } : null,
353
+ };
354
+ }
355
+
356
+ function buildSupportBundle(options = {}) {
357
+ const diagnostic = buildDiagnosticReport(options);
358
+ return {
359
+ type: 'yingclaw-support-bundle',
360
+ version: 1,
361
+ generatedAt: diagnostic.generatedAt,
362
+ packageVersion: options.packageVersion || null,
363
+ platform: diagnostic.platform,
364
+ diagnostic,
365
+ gateway: options.gatewayStatus ? {
366
+ configured: options.gatewayStatus.configured,
367
+ running: options.gatewayStatus.running,
368
+ port: options.gatewayStatus.port,
369
+ url: options.gatewayStatus.url,
370
+ error: options.gatewayStatus.error || null,
371
+ } : null,
372
+ notes: [
373
+ 'This file is generated by yingclaw and should not contain API keys.',
374
+ 'If you share it, review it once for any provider-specific private URL before posting publicly.',
375
+ ],
376
+ };
377
+ }
378
+
379
+ function writeDiagnosticReport(file, report) {
380
+ fs.mkdirSync(path.dirname(file), { recursive: true });
381
+ fs.writeFileSync(file, JSON.stringify(report, null, 2) + '\n');
382
+ return file;
383
+ }
384
+
193
385
  module.exports = {
386
+ buildDiagnosticReport,
387
+ buildSupportBundle,
388
+ buildWindowsDoctorChecks,
389
+ checkWindowsClaudeProcesses,
390
+ checkWindowsDesktopConfigMirrors,
194
391
  runDoctorChecks,
195
392
  checkShellRcBlock,
196
393
  checkWindowsEnvVars,
197
394
  summarize,
395
+ writeDiagnosticReport,
198
396
  STATUS_OK,
199
397
  STATUS_FAIL,
200
398
  STATUS_WARN,