@sanqianx/claude-ai-workbench 0.13.0 → 0.13.2-rc.1

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/README.md CHANGED
@@ -12,6 +12,10 @@
12
12
 
13
13
  工作台是产品供应商与凭据的活动配置源。后台维护默认模型持久化,不随会话临时模型切换;解析后的凭据只交给鉴权本地服务,不进入浏览器消息。SDK 会话从当前项目登记的工作区恢复,也兼容旧登记文件。
14
14
 
15
+ MiniMax 向量模型使用独立的 `/v1/embeddings` 接口,不复用聊天的 `/anthropic` 路由。官方域名的已知接口后缀在运行期规范化为服务根地址,现有配置文件不被迁移或改写;自定义网关路径保持原样。
16
+
17
+ Windows 上 Claude Code 探测与实际启动使用同一解析结果:失效的 `CLAUDE_CODE_EXECPATH` 不遮住有效安装,npm/CMD/PowerShell 启动脚本解析成实际文件,JavaScript 入口用当前 Node 探测版本。`/system/doctor` 返回实际启动路径、被跳过的失效路径及版本探测错误。发现和会话失败使用 `log/v2`;受外壳守护时写入外壳统一诊断,独立运行保存在工作台数据目录的 `diagnostics.jsonl`,不记录凭据或原始提示词。
18
+
15
19
  ## 仓库结构
16
20
 
17
21
  - `packages/`:可发布的 SDK 源码。
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: Claude AI Workbench API
4
- version: 0.13.0
4
+ version: 0.13.2-rc.1
5
5
  description: Versioned local API for AI Profile, credential and Claude Code session management.
6
6
  servers:
7
7
  - url: http://127.0.0.1:5760/api/claude-workbench/v1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanqianx/claude-ai-workbench",
3
- "version": "0.13.0",
3
+ "version": "0.13.2-rc.1",
4
4
  "description": "Shared Claude Code terminal and Anthropic-compatible AI profile modules for local developer tools.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/client",
3
- "version": "0.13.0",
3
+ "version": "0.13.2-rc.1",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "browser": "index.js",
@@ -20,7 +20,7 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@claude-ai-workbench/contracts": "0.13.0"
23
+ "@claude-ai-workbench/contracts": "0.13.2-rc.1"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/contracts",
3
- "version": "0.13.0",
3
+ "version": "0.13.2-rc.1",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/core",
3
- "version": "0.13.0",
3
+ "version": "0.13.2-rc.1",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "browser": "index.js",
@@ -20,7 +20,7 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@claude-ai-workbench/contracts": "0.13.0"
23
+ "@claude-ai-workbench/contracts": "0.13.2-rc.1"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18"
@@ -15,7 +15,8 @@ const { AttachmentStore } = require('./lib/attachment-store');
15
15
  const { ProjectStore } = require('./lib/project-store');
16
16
  const { SessionArchiveStore } = require('./lib/session-archive-store');
17
17
  const { AgentRegistry } = require('./lib/agent-registry');
18
- const { createAgentDiscovery, probeExecutableVersion } = require('./lib/agent-discovery');
18
+ const { createAgentDiscovery, probeExecutable } = require('./lib/agent-discovery');
19
+ const { createDiagnostics } = require('./lib/diagnostics');
19
20
  const { createSystemDoctor } = require('./lib/system-doctor');
20
21
  const { createModelDetection } = require('./lib/model-detection');
21
22
  const { createCodexDriver } = require('./lib/drivers/codex-driver');
@@ -68,6 +69,7 @@ function createWorkbenchServer(options = {}) {
68
69
  },
69
70
  });
70
71
  const runner = registry.asRunner();
72
+ const diagnostics = options.logger || createDiagnostics({ dataDir, secrets: () => [...Object.values(profiles._credentials().credentials), process.env.ANTHROPIC_API_KEY, process.env.ANTHROPIC_AUTH_TOKEN] });
71
73
  const agentDiscovery = options.agentDiscovery || createAgentDiscovery({
72
74
  claudeProbe: () => {
73
75
  try {
@@ -77,11 +79,17 @@ function createWorkbenchServer(options = {}) {
77
79
  const cmd = typeof executable === 'string'
78
80
  ? executable
79
81
  : executable && executable.cmd;
80
- if (!cmd) return { available: false, version: null };
82
+ if (!cmd) {
83
+ const reason = executable?.reason || '未发现可用的 Claude Code 启动文件。';
84
+ diagnostics.error('agent.discovery.failed', 'Claude Code 不可用。', { phase: 'discovery', error: { code: 'CLAUDE_EXECUTABLE_NOT_FOUND', message: reason }, context: { agent: 'claude-code', rejectedPaths: executable?.rejected || [] } });
85
+ return { available: false, version: null, reason };
86
+ }
81
87
  // Honest probe: spawn the resolved executable with --version. Same
82
88
  // contract as every other agent: no version, no availability claim.
83
- const version = probeExecutableVersion(cmd);
84
- return { available: version != null, version };
89
+ const probe = probeExecutable(cmd);
90
+ if (!probe.available) diagnostics.error('agent.discovery.failed', 'Claude Code 版本探测失败。', { phase: 'version-probe', error: { code: probe.code, message: probe.reason }, context: { agent: 'claude-code', executable: cmd, exitCode: probe.exitCode } });
91
+ else if (executable.rejected?.length) diagnostics.warn('agent.discovery.recovered', '已跳过失效的 Claude Code 启动路径,使用有效安装。', { phase: 'discovery', context: { executable: cmd, rejectedPaths: executable.rejected } });
92
+ return probe;
85
93
  } catch { return { available: false, version: null }; }
86
94
  },
87
95
  });
@@ -89,16 +97,22 @@ function createWorkbenchServer(options = {}) {
89
97
  const modelDetection = options.modelDetection || createModelDetection({
90
98
  contextLookup: modelContextWindows && modelContextWindows.lookupContextWindow,
91
99
  });
92
- const doctor = options.doctor || createSystemDoctor({
100
+ const collectDoctor = options.doctor || createSystemDoctor({
93
101
  resolveClaudeExecutable: () => {
94
102
  try { return claudeDriver.findClaudeExecutableForSdk(); } catch { return { cmd: null }; }
95
103
  },
96
104
  discoverZCodeCli,
97
105
  probeZCodeVersion,
98
106
  });
107
+ const doctor = () => {
108
+ const report = collectDoctor();
109
+ const claude = report.agents?.['claude-code'];
110
+ if (claude && !claude.versionProbe?.ok) diagnostics.error('agent.doctor.failed', 'Claude Code 环境诊断失败。', { phase: 'doctor', error: { code: 'CLAUDE_PROBE_FAILED', message: claude.versionProbe?.error || claude.resolutionReason }, context: { agent: 'claude-code', executable: claude.resolvedExecutable, rejectedPaths: claude.rejectedPaths, exitCode: claude.versionProbe?.exitCode } });
111
+ return report;
112
+ };
99
113
  const service = options.service || new WorkbenchService({
100
114
  knowledge: options.knowledge || new (require('./lib/knowledge-context').KnowledgeContext)({ engineUrl: options.engineUrl || process.env.KB_KNOWLEDGE_ENGINE_URL, contextFile: path.join(dataDir, 'knowledge-contexts.v1.json') }),
101
- runner, profiles, contexts, attachments, apiPrefix,
115
+ runner, profiles, contexts, attachments, apiPrefix, diagnostics,
102
116
  projects, sessionArchive, agentDiscovery, modelContextWindows, modelDetection,
103
117
  archiveRetentionDays: options.archiveRetentionDays,
104
118
  captureSettings, uiPreferences, onCaptureSettingsSaved: applyBridgeCapture,
@@ -17,19 +17,22 @@ const CACHE_TTL_MS = 30000;
17
17
  // Spawns `<executable> --version` directly (no shell) and returns the first
18
18
  // version-looking token, or null when the probe fails. This is the only
19
19
  // version source: nothing here invents or hardcodes one.
20
- function probeExecutableVersion(cmd) {
20
+ function probeExecutable(cmd) {
21
21
  try {
22
- const result = spawnSync(cmd, ['--version'], {
22
+ const script = /\.(?:[cm]?js)$/i.test(cmd);
23
+ const result = spawnSync(script ? process.execPath : cmd, script ? [cmd, '--version'] : ['--version'], {
23
24
  timeout: PROBE_TIMEOUT_MS,
24
25
  windowsHide: true,
25
26
  encoding: 'utf8',
26
27
  });
27
- if (result.error || result.status !== 0) return null;
28
- return parseVersionToken(String(result.stdout || result.stderr || ''));
28
+ if (result.error || result.status !== 0) return { available: false, version: null, code: result.error?.code || 'CLI_PROBE_FAILED', reason: String(result.error?.message || result.stderr || result.stdout || `exit ${result.status}`).slice(0, 2000), exitCode: result.status };
29
+ const version = parseVersionToken(String(result.stdout || result.stderr || ''));
30
+ return { available: Boolean(version), version, reason: version ? '' : 'Claude Code 未返回版本号。' };
29
31
  } catch {
30
- return null;
32
+ return { available: false, version: null, code: 'CLI_PROBE_FAILED', reason: 'Claude Code 版本探测失败。' };
31
33
  }
32
34
  }
35
+ function probeExecutableVersion(cmd) { return probeExecutable(cmd).version; }
33
36
  function createAgentDiscovery(options = {}) {
34
37
  const claudeProbe = options.claudeProbe || null; // () => {available, version}
35
38
  let cache = null;
@@ -73,7 +76,7 @@ function createAgentDiscovery(options = {}) {
73
76
  const probed = claudeProbe ? claudeProbe() : { available: false, version: null };
74
77
  available = Boolean(probed.available);
75
78
  version = probed.version || null;
76
- if (!available) reason = 'Claude Code executable was not found';
79
+ if (!available) reason = probed.reason || 'Claude Code executable was not found';
77
80
  } else if (id === 'zcode') {
78
81
  const install = probeZCodeInstall();
79
82
  available = install.installed;
@@ -109,4 +112,4 @@ function createAgentDiscovery(options = {}) {
109
112
  return discover;
110
113
  }
111
114
 
112
- module.exports = { createAgentDiscovery, probeExecutableVersion };
115
+ module.exports = { createAgentDiscovery, probeExecutableVersion, probeExecutable };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const crypto = require('node:crypto');
5
+
6
+ function createDiagnostics({ dataDir, secrets = () => [] } = {}) {
7
+ const sanitize = value => {
8
+ let text = String(value || '');
9
+ let values = []; try { values = secrets() || []; } catch {}
10
+ for (const secret of values) if (typeof secret === 'string' && secret.length >= 4) text = text.split(secret).join('[REDACTED]');
11
+ return text.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[REDACTED]@').replace(/\b(Bearer|Basic)\s+[^\s]+/gi, '$1 [REDACTED]').replace(/((?:api[_-]?key|token|password|secret)\s*[=:]\s*)[^\s,;]+/gi, '$1[REDACTED]').slice(0, 8000);
12
+ };
13
+ const logger = { close: async () => {} };
14
+ for (const level of ['debug', 'info', 'warn', 'error']) logger[level] = async (event, message, input = {}) => {
15
+ const record = { schema: 'log/v2', id: `log-${crypto.randomUUID()}`, ts: new Date().toISOString(), component: 'claude-ai-workbench', level, event, message: sanitize(message) };
16
+ for (const key of ['projectId', 'runId', 'operationId', 'commitSha', 'phase']) if (input[key]) record[key] = sanitize(input[key]);
17
+ if (input.error) record.error = { name: input.error.name || 'Error', code: input.error.code || 'CLAUDE_RUNTIME_FAILED', message: sanitize(input.error.message || input.error), stack: sanitize(input.error.stack) };
18
+ if (input.context) record.context = JSON.parse(JSON.stringify(input.context, (key, value) => /key|token|password|secret|prompt|body/i.test(key) ? '[REDACTED]' : typeof value === 'string' ? sanitize(value) : value));
19
+ if (process.connected && process.send) {
20
+ const delivered = await new Promise(resolve => {
21
+ try { process.send({ type: 'kb:diagnostic', record }, error => resolve(!error)); } catch { resolve(false); }
22
+ });
23
+ if (delivered) return;
24
+ }
25
+ try {
26
+ fs.mkdirSync(dataDir, { recursive: true });
27
+ fs.appendFileSync(path.join(dataDir, 'diagnostics.jsonl'), JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 });
28
+ } catch { process.stderr.write(JSON.stringify(record) + '\n'); }
29
+ };
30
+ return logger;
31
+ }
32
+ module.exports = { createDiagnostics };
@@ -61,6 +61,7 @@ class ProfileStore {
61
61
  const saved = this._profiles().embedding;
62
62
  if (!saved) return { configured: false, config: null };
63
63
  const { credentialRef, ...config } = saved;
64
+ config.endpoint = normalizeEmbedding(saved).endpoint;
64
65
  let configured = false;
65
66
  try { configured = Boolean(this.embeddingRuntime()); } catch {}
66
67
  return { configured, config };
@@ -95,7 +96,7 @@ class ProfileStore {
95
96
  if (!saved) throw Object.assign(new Error('向量模型未配置。'), { status: 409 });
96
97
  const secret = saved.profileId ? this.runtimeProfile(saved.profileId).apiKey : this._credentials().credentials[saved.credentialRef];
97
98
  if (!secret) throw Object.assign(new Error('向量模型凭据未配置。'), { status: 409 });
98
- return { provider: saved.provider, model: saved.model, endpoint: saved.endpoint, apiKey: secret };
99
+ return { provider: saved.provider, model: saved.model, endpoint: normalizeEmbedding(saved).endpoint, apiKey: secret };
99
100
  }
100
101
 
101
102
  importEmbedding({ migrationId, settings }) {
@@ -245,10 +246,13 @@ function normalizeEmbedding(input) {
245
246
  if (!['minimax', 'openai'].includes(input.provider)) throw Object.assign(new Error('请选择支持的向量协议。'), { status: 400 });
246
247
  const model = String(input.model || (input.provider === 'minimax' ? 'embo-01' : 'text-embedding-3-small')).trim();
247
248
  if (!model || model.length > 200 || /[\x00-\x1f]/.test(model)) throw Object.assign(new Error('无效的向量模型。'), { status: 400 });
248
- const endpoint = String(input.endpoint || '').trim();
249
+ let endpoint = String(input.endpoint || '').trim();
249
250
  if (endpoint) {
250
251
  let url; try { url = new URL(endpoint); } catch {}
251
252
  if (!url || !['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw Object.assign(new Error('向量接口地址无效。'), { status: 400 });
253
+ // MiniMax appends /v1/embeddings itself. Only official, known route
254
+ // aliases are normalized; private gateways may require their path prefix.
255
+ if (input.provider === 'minimax' && ['api.minimaxi.com', 'api.minimax.io', 'api.minimax.cn', 'api.minimax.chat'].includes(url.hostname) && /^\/(?:anthropic|v1(?:\/embeddings)?)?\/?$/.test(url.pathname)) endpoint = url.origin;
252
256
  }
253
257
  return { provider: input.provider, model, endpoint, profileId: input.profileId ? cleanId(input.profileId, 'profileId') : null };
254
258
  }
@@ -18,11 +18,12 @@ function createSystemDoctor(deps = {}) {
18
18
 
19
19
  function probe(command, args) {
20
20
  try {
21
- const result = doSpawnSync(command, args, {
21
+ const script = /\.[cm]?js$/i.test(command);
22
+ const result = doSpawnSync(script ? process.execPath : command, script ? [command, ...args] : args, {
22
23
  timeout: 10000,
23
24
  windowsHide: true,
24
25
  encoding: 'utf8',
25
- shell: platform === 'win32',
26
+ shell: !script && platform === 'win32' && !require('path').isAbsolute(command),
26
27
  env,
27
28
  });
28
29
  if (result.error) {
@@ -100,6 +101,7 @@ function createSystemDoctor(deps = {}) {
100
101
  'claude-code': {
101
102
  resolvedExecutable: claude.cmd || null,
102
103
  resolutionReason: claude.cmd ? null : (claude.reason || 'not found'),
104
+ rejectedPaths: claude.rejected || [],
103
105
  versionProbe: claudeProbe,
104
106
  loginState: claudeLoginState(),
105
107
  },
@@ -13,6 +13,7 @@ const ACTIVE_STATES = new Set(['created', 'spawning', 'running', 'thinking', 'to
13
13
  class WorkbenchService {
14
14
  constructor(options) {
15
15
  this.runner = options.runner;
16
+ this.diagnostics = options.diagnostics || null;
16
17
  this.knowledge = options.knowledge || null;
17
18
  this.pendingInputs = new Map();
18
19
  this.knowledgeBySession = new Map();
@@ -69,7 +70,7 @@ class WorkbenchService {
69
70
  let executable = null;
70
71
  try { executable = this.runner.findClaudeExecutableForSdk(); } catch {}
71
72
  const command = executable && (executable.cmd || executable);
72
- return { runtime: 'claude-code', available: Boolean(executable), executable: command ? path.basename(String(command)) : null, transport: 'agent-sdk' };
73
+ return { runtime: 'claude-code', available: Boolean(command), executable: command ? path.basename(String(command)) : null, transport: 'agent-sdk' };
73
74
  }
74
75
 
75
76
  // Live availability probing when a discovery provider is wired in,
@@ -79,7 +80,7 @@ class WorkbenchService {
79
80
  return { agents: Object.values(this.agentDiscovery()) };
80
81
  }
81
82
  let claudeAvailable = false;
82
- try { claudeAvailable = Boolean(this.runner.findClaudeExecutableForSdk()); } catch {}
83
+ try { const executable = this.runner.findClaudeExecutableForSdk(); claudeAvailable = Boolean(typeof executable === 'string' ? executable : executable?.cmd); } catch {}
83
84
  return {
84
85
  agents: Object.keys(AGENTS).map(id => {
85
86
  const agent = AGENTS[id];
@@ -351,7 +352,7 @@ class WorkbenchService {
351
352
  }));
352
353
  const runtimeAttachments = attachments.map((item, index) => ({ ...displayAttachments[index], data: item.data }));
353
354
  try { await this.runner.sendInput(id, value, profile, { ...(options || {}), knowledgeContext: knowledge?.prompt || '', attachments: runtimeAttachments, displayAttachments }); }
354
- catch (error) { if (captureTurn) this._capture(captureTurn, 'session_end', undefined, 'failed'); throw error; }
355
+ catch (error) { this._runtimeFailure(id, error); if (captureTurn) this._capture(captureTurn, 'session_end', undefined, 'failed'); throw error; }
355
356
  this._armTimeout(id, profile.timeoutMs);
356
357
  this._watchTurn(id, previousEventCount, captureTurn);
357
358
  this._audit('input', { sessionId: id, contextId: state.projectSlug, characters: value.length, attachments: attachments.length, attachmentBytes: attachments.reduce((sum, item) => sum + item.size, 0) });
@@ -630,6 +631,7 @@ class WorkbenchService {
630
631
  seen += 1;
631
632
  if (seen <= previousEventCount) return;
632
633
  if (['claude/result', 'claude/error', 'claude/aborted'].includes(event && event.type)) {
634
+ if (event.type === 'claude/error' || event.type === 'claude/result' && event.isError) this._runtimeFailure(sessionId, new Error(String(event.message || event.error || event.result || 'Agent 运行失败。')));
633
635
  if (captureTurn && !completed) {
634
636
  completed = true;
635
637
  if (event.type === 'claude/result' && !event.isError && typeof event.result === 'string' && event.result) this._capture(captureTurn, 'assistant_response', event.result);
@@ -647,6 +649,10 @@ class WorkbenchService {
647
649
  if (unsubscribe) try { unsubscribe(); } catch {}
648
650
  this.turnMonitors.delete(sessionId);
649
651
  }
652
+ _runtimeFailure(sessionId, error) {
653
+ const state = this.runner.getState(sessionId);
654
+ this.diagnostics?.error('agent.runtime.failed', 'Agent 会话运行失败。', { projectId: /^project-[A-Za-z0-9._-]+$/.test(state?.projectSlug || '') ? state.projectSlug : undefined, runId: sessionId, phase: 'runtime', error, context: { agent: this.runner.agentOf?.(sessionId) || state?.agentId || 'claude-code', sessionId } });
655
+ }
650
656
  _audit(action, detail) {
651
657
  if (!this.auditFile) return;
652
658
  fs.mkdirSync(path.dirname(this.auditFile), { recursive: true });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/server",
3
- "version": "0.13.0",
3
+ "version": "0.13.2-rc.1",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@anthropic-ai/claude-agent-sdk": "^0.2.141",
28
- "@claude-ai-workbench/contracts": "0.13.0"
28
+ "@claude-ai-workbench/contracts": "0.13.2-rc.1"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=18"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/ui",
3
- "version": "0.13.0",
3
+ "version": "0.13.2-rc.1",
4
4
  "type": "commonjs",
5
5
  "main": "ai-profile-settings.js",
6
6
  "browser": "ai-profile-settings.js",
@@ -33,7 +33,7 @@
33
33
  "README.md"
34
34
  ],
35
35
  "dependencies": {
36
- "@claude-ai-workbench/core": "0.13.0"
36
+ "@claude-ai-workbench/core": "0.13.2-rc.1"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=18"
@@ -1050,6 +1050,7 @@ function getSessionTokenUsage(sessionId) {
1050
1050
 
1051
1051
  const isShellShim = (file) => /\.(cmd|bat|ps1)$/i.test(String(file || ''));
1052
1052
  const sdkSpawnable = (file) => file && !isShellShim(file);
1053
+ const existingFile = file => { try { return fs.statSync(file).isFile(); } catch { return false; } };
1053
1054
 
1054
1055
  // Newer claude-code releases keep moving their entry file (cli.js ->
1055
1056
  // bin/claude.exe), so hardcoded layout guesses rot. The installed package's
@@ -1063,7 +1064,7 @@ function readClaudePackageEntry(packageDir) {
1063
1064
  : (bin && typeof bin === 'object' && typeof bin.claude === 'string' ? bin.claude : null);
1064
1065
  if (!rel) return null;
1065
1066
  const entry = path.join(packageDir, ...String(rel).split(/[\\/]+/).filter(Boolean));
1066
- return fs.existsSync(entry) ? entry : null;
1067
+ return existingFile(entry) ? entry : null;
1067
1068
  } catch {
1068
1069
  return null;
1069
1070
  }
@@ -1077,7 +1078,7 @@ function resolveClaudeShimTarget(shimPath) {
1077
1078
  const dir = path.dirname(shimPath);
1078
1079
  const packageDir = path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code');
1079
1080
  const direct = path.join(packageDir, 'bin', 'claude.exe');
1080
- if (fs.existsSync(direct)) return direct;
1081
+ if (existingFile(direct)) return direct;
1081
1082
  const declared = readClaudePackageEntry(packageDir);
1082
1083
  if (declared) return declared;
1083
1084
  let content;
@@ -1086,14 +1087,14 @@ function resolveClaudeShimTarget(shimPath) {
1086
1087
  } catch {
1087
1088
  return null;
1088
1089
  }
1089
- const embedded = /["']([^"'\r\n]+claude\.exe)["']/gi;
1090
+ const embedded = /["']([^"'\r\n]+(?:claude\.exe|cli\.(?:js|cjs|mjs)))["']/gi;
1090
1091
  for (const match of content.matchAll(embedded)) {
1091
1092
  const raw = match[1]
1092
1093
  .replace(/^\$basedir[\\/]*/i, '')
1093
1094
  .replace(/^%~dp0%?[\\/]*/i, '')
1094
1095
  .replace(/^%dp0%[\\/]*/i, '');
1095
1096
  const candidate = path.isAbsolute(raw) ? raw : path.resolve(dir, raw);
1096
- if (fs.existsSync(candidate)) return candidate;
1097
+ if (existingFile(candidate)) return candidate;
1097
1098
  }
1098
1099
  return null;
1099
1100
  }
@@ -1119,8 +1120,12 @@ function listClaudePathHits() {
1119
1120
  }
1120
1121
 
1121
1122
  function findClaudeExecutableForSdk() {
1122
- if (process.env.CLAUDE_CODE_EXECPATH && sdkSpawnable(process.env.CLAUDE_CODE_EXECPATH)) {
1123
- return { cmd: process.env.CLAUDE_CODE_EXECPATH, shell: false };
1123
+ const rejected = [];
1124
+ if (process.env.CLAUDE_CODE_EXECPATH) {
1125
+ const override = process.env.CLAUDE_CODE_EXECPATH;
1126
+ const target = isShellShim(override) ? resolveClaudeShimTarget(override) : override;
1127
+ if (target && existingFile(target) && sdkSpawnable(target)) return { cmd: target, shell: false, source: 'environment' };
1128
+ rejected.push({ path: override, reason: '配置的启动路径不存在、不是文件,或启动脚本没有有效目标。' });
1124
1129
  }
1125
1130
  if (process.platform === 'win32') {
1126
1131
  const candidates = [];
@@ -1131,44 +1136,39 @@ function findClaudeExecutableForSdk() {
1131
1136
  candidates.push(file);
1132
1137
  }
1133
1138
  };
1134
- // npm global install: known layout first, then the entry the installed
1135
- // package itself declares, then the pre-2.x literal.
1136
1139
  const npmRoot = process.env.APPDATA && path.join(process.env.APPDATA, 'npm');
1137
1140
  const npmPackageDir = npmRoot && path.join(npmRoot, 'node_modules', '@anthropic-ai', 'claude-code');
1138
- if (npmPackageDir) {
1139
- push(path.join(npmPackageDir, 'bin', 'claude.exe'));
1140
- push(readClaudePackageEntry(npmPackageDir));
1141
- push(path.join(npmPackageDir, 'cli.js'));
1142
- }
1143
1141
  // Official native installer layout (PowerShell/CMD installers).
1144
1142
  if (process.env.USERPROFILE) {
1145
1143
  push(path.join(process.env.USERPROFILE, '.local', 'bin', 'claude.exe'));
1146
1144
  }
1147
1145
  if (process.env.LOCALAPPDATA) {
1148
- push(
1149
- path.join(process.env.LOCALAPPDATA, 'Programs', 'claude-code', 'claude.exe'),
1150
- path.join(process.env.LOCALAPPDATA, 'claude-code', 'claude.exe'),
1151
- );
1146
+ push(path.join(process.env.LOCALAPPDATA, 'Programs', 'claude-code', 'claude.exe'));
1147
+ push(path.join(process.env.LOCALAPPDATA, 'claude-code', 'claude.exe'));
1152
1148
  }
1153
1149
  // Any claude.exe reachable from PATH (covers custom install prefixes);
1154
1150
  // .cmd/.bat shims stay excluded — the SDK spawns with shell:false.
1155
1151
  for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
1156
- if (dir) push(path.join(dir, 'claude.exe'));
1152
+ if (dir) {
1153
+ push(path.join(dir, 'claude.exe'));
1154
+ for (const name of ['claude.cmd', 'claude.bat', 'claude.ps1']) push(resolveClaudeShimTarget(path.join(dir, name)));
1155
+ }
1157
1156
  }
1157
+ if (npmPackageDir) { push(readClaudePackageEntry(npmPackageDir)); push(path.join(npmPackageDir, 'bin', 'claude.exe')); push(path.join(npmPackageDir, 'cli.js')); }
1158
1158
  for (const candidate of candidates) {
1159
- if (fs.existsSync(candidate) && sdkSpawnable(candidate)) return { cmd: candidate, shell: false };
1159
+ if (existingFile(candidate) && sdkSpawnable(candidate)) return { cmd: candidate, shell: false, source: 'installed', rejected };
1160
1160
  }
1161
1161
  // Nothing guessed panned out: fall back to the shims a shell would
1162
1162
  // actually run, resolving each to the binary it wraps.
1163
1163
  const pathHits = listClaudePathHits();
1164
1164
  for (const hit of pathHits) {
1165
- if (/\.exe$/i.test(hit) && fs.existsSync(hit) && sdkSpawnable(hit)) return { cmd: hit, shell: false };
1165
+ if (/\.exe$/i.test(hit) && existingFile(hit) && sdkSpawnable(hit)) return { cmd: hit, shell: false, source: 'path', rejected };
1166
1166
  }
1167
1167
  for (const hit of pathHits) {
1168
1168
  const target = resolveClaudeShimTarget(hit);
1169
- if (target && fs.existsSync(target) && sdkSpawnable(target)) return { cmd: target, shell: false };
1169
+ if (target && existingFile(target) && sdkSpawnable(target)) return { cmd: target, shell: false, source: 'shim', rejected };
1170
1170
  }
1171
- return { cmd: null, shell: false, reason: 'no SDK-compatible Claude Code executable found' };
1171
+ return { cmd: null, shell: false, rejected, reason: rejected.length ? 'Claude Code 启动路径已失效,且未发现可用安装。' : '未找到可供 SDK 启动的 Claude Code 文件。' };
1172
1172
  }
1173
1173
  return { cmd: 'claude', shell: false };
1174
1174
  }