@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 +4 -0
- package/docs/sdk/openapi.yaml +1 -1
- package/package.json +1 -1
- package/packages/client/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/core/package.json +2 -2
- package/packages/server/index.js +20 -6
- package/packages/server/lib/agent-discovery.js +10 -7
- package/packages/server/lib/diagnostics.js +32 -0
- package/packages/server/lib/profile-store.js +6 -2
- package/packages/server/lib/system-doctor.js +4 -2
- package/packages/server/lib/workbench-service.js +9 -3
- package/packages/server/package.json +2 -2
- package/packages/ui/package.json +2 -2
- package/src/backend/lib/claude-cli-runner.js +22 -22
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 源码。
|
package/docs/sdk/openapi.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/client",
|
|
3
|
-
"version": "0.13.
|
|
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.
|
|
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/core",
|
|
3
|
-
"version": "0.13.
|
|
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.
|
|
23
|
+
"@claude-ai-workbench/contracts": "0.13.2-rc.1"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
package/packages/server/index.js
CHANGED
|
@@ -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,
|
|
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)
|
|
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
|
|
84
|
-
|
|
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
|
|
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
|
|
20
|
+
function probeExecutable(cmd) {
|
|
21
21
|
try {
|
|
22
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
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.
|
|
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.
|
|
28
|
+
"@claude-ai-workbench/contracts": "0.13.2-rc.1"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=18"
|
package/packages/ui/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/ui",
|
|
3
|
-
"version": "0.13.
|
|
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.
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
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)
|
|
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 (
|
|
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) &&
|
|
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 &&
|
|
1169
|
+
if (target && existingFile(target) && sdkSpawnable(target)) return { cmd: target, shell: false, source: 'shim', rejected };
|
|
1170
1170
|
}
|
|
1171
|
-
return { cmd: null, shell: false, reason: '
|
|
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
|
}
|