@sanqianx/claude-ai-workbench 0.13.2 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +652 -613
  4. package/apps/agent-terminal/app.js +143 -67
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +172 -170
  7. package/apps/agent-terminal/panel-host-proof.html +152 -152
  8. package/apps/agent-terminal/panel-proof-content.html +41 -41
  9. package/apps/agent-terminal/right-workspace-dock.css +189 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +652 -521
  11. package/apps/agent-terminal/user-question-card.js +68 -0
  12. package/apps/ai-profile-settings-host/index.html +25 -25
  13. package/apps/ai-profile-settings-host/page.css +1 -1
  14. package/apps/ai-profile-settings-host/page.js +113 -113
  15. package/apps/minimal-test-host/README.md +15 -15
  16. package/apps/minimal-test-host/host.css +2 -2
  17. package/apps/minimal-test-host/host.js +35 -35
  18. package/apps/minimal-test-host/index.html +58 -58
  19. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  20. package/docs/sdk/FILE_LAYOUT.md +31 -31
  21. package/docs/sdk/INTEGRATION.md +126 -126
  22. package/docs/sdk/README.md +21 -21
  23. package/docs/sdk/SESSION_EVENTS.md +15 -13
  24. package/docs/sdk/openapi.yaml +16 -1
  25. package/package.json +1 -1
  26. package/packages/client/README.md +29 -29
  27. package/packages/client/index.d.ts +122 -122
  28. package/packages/client/index.js +260 -260
  29. package/packages/client/package.json +2 -2
  30. package/packages/contracts/README.md +9 -9
  31. package/packages/contracts/index.d.ts +519 -517
  32. package/packages/contracts/index.js +312 -312
  33. package/packages/contracts/package.json +1 -1
  34. package/packages/core/README.md +8 -8
  35. package/packages/core/index.d.ts +76 -76
  36. package/packages/core/index.js +222 -222
  37. package/packages/core/package.json +2 -2
  38. package/packages/server/README.md +17 -17
  39. package/packages/server/bin/agent-terminal-server.js +42 -42
  40. package/packages/server/bin/claude-workbench.js +225 -225
  41. package/packages/server/index.d.ts +171 -171
  42. package/packages/server/index.js +230 -230
  43. package/packages/server/lib/agent-discovery.js +133 -133
  44. package/packages/server/lib/agent-registry.js +143 -143
  45. package/packages/server/lib/agent-spawn-env.js +17 -17
  46. package/packages/server/lib/attachment-store.js +78 -78
  47. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  48. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  49. package/packages/server/lib/capture-settings-store.js +35 -35
  50. package/packages/server/lib/claude-history-recovery.js +87 -87
  51. package/packages/server/lib/cli-session-recovery.js +28 -28
  52. package/packages/server/lib/context-resolver.js +55 -55
  53. package/packages/server/lib/conversation-capture.js +51 -51
  54. package/packages/server/lib/delegation-phase-store.js +357 -357
  55. package/packages/server/lib/delegation-store.js +474 -468
  56. package/packages/server/lib/diagnostics.js +32 -32
  57. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  58. package/packages/server/lib/drivers/codex-driver.js +1461 -1442
  59. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  60. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  61. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  62. package/packages/server/lib/event-stream.js +138 -138
  63. package/packages/server/lib/folder-picker.js +623 -623
  64. package/packages/server/lib/http-handler.js +339 -339
  65. package/packages/server/lib/json-file.js +26 -26
  66. package/packages/server/lib/knowledge-context.js +33 -33
  67. package/packages/server/lib/model-detection.js +295 -295
  68. package/packages/server/lib/parent-context.js +44 -0
  69. package/packages/server/lib/profile-store.js +283 -283
  70. package/packages/server/lib/project-store.js +102 -102
  71. package/packages/server/lib/runtime-loader.js +120 -120
  72. package/packages/server/lib/session-archive-store.js +53 -53
  73. package/packages/server/lib/session-history.js +612 -612
  74. package/packages/server/lib/system-doctor.js +125 -125
  75. package/packages/server/lib/ui-preferences-store.js +48 -48
  76. package/packages/server/lib/user-questions.js +67 -0
  77. package/packages/server/lib/workbench-service.js +1696 -1675
  78. package/packages/server/package.json +2 -2
  79. package/packages/ui/README.md +30 -30
  80. package/packages/ui/ai-profile-settings.d.ts +45 -45
  81. package/packages/ui/ai-profile-settings.js +283 -283
  82. package/packages/ui/claude-terminal.d.ts +31 -31
  83. package/packages/ui/claude-terminal.js +343 -343
  84. package/packages/ui/index.d.ts +18 -18
  85. package/packages/ui/index.js +51 -51
  86. package/packages/ui/package.json +2 -2
  87. package/packages/ui/theme.css +13 -13
  88. package/src/assets/claude-prompts.json +21 -21
  89. package/src/backend/http/ai-profile-routes.js +61 -61
  90. package/src/backend/http/claude-routes.js +170 -170
  91. package/src/backend/http/node-helpers.js +45 -45
  92. package/src/backend/index.js +21 -21
  93. package/src/backend/lib/ai-adapter.js +144 -144
  94. package/src/backend/lib/ai-profile-store.js +259 -259
  95. package/src/backend/lib/ai-vendor-presets.js +83 -83
  96. package/src/backend/lib/ai-workspace.js +140 -140
  97. package/src/backend/lib/automation-config.js +253 -253
  98. package/src/backend/lib/claude-cli-runner.js +2408 -2392
  99. package/src/backend/lib/data-dir.js +169 -169
  100. package/src/backend/lib/llm-client.js +176 -176
  101. package/src/backend/lib/model-context-windows.js +82 -82
  102. package/src/backend/lib/native-task-control.js +217 -217
  103. package/src/backend/lib/native-task-history-index.js +475 -475
  104. package/src/backend/lib/native-task-history-reader.js +526 -526
  105. package/src/backend/lib/native-task-messages.js +457 -457
  106. package/src/backend/lib/native-task-registry.js +325 -325
  107. package/src/backend/lib/native-task-runtime.js +242 -242
  108. package/src/backend/lib/native-task-trace.js +440 -440
  109. package/src/backend/lib/native-task-transcript.js +393 -393
  110. package/src/backend/lib/prompt-registry.js +166 -166
  111. package/src/backend/lib/runtime-config.js +121 -121
  112. package/src/backend/lib/sdk-session-stream.js +143 -143
  113. package/src/backend/lib/sdk-turn-stream.js +16 -16
  114. package/src/backend/lib/store-scope.js +131 -131
  115. package/src/backend/lib/task-identity.js +104 -104
  116. package/src/backend/lib/task-state-projection.js +58 -58
  117. package/src/index.js +7 -7
  118. package/src/sdk-wave-a.ts +1 -1
  119. package/src/sdk-wave-b.ts +1 -1
  120. package/src/sdk-wave-c.ts +1 -1
  121. package/src/sdk-wave-d.ts +1 -1
  122. package/src/sdk-wave-e.ts +1 -1
@@ -1,312 +1,312 @@
1
- 'use strict';
2
-
3
- const API_VERSION = 'v1';
4
- const DEFAULT_API_PREFIX = `/api/claude-workbench/${API_VERSION}`;
5
- const RUNTIME = 'claude-code';
6
- const DEFAULT_AGENT = 'claude-code';
7
- const EFFORT_LEVELS = Object.freeze(['low', 'medium', 'high', 'max']);
8
- const PERMISSION_MODES = Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']);
9
- // Delegated-session identity split: a delegation command is a Workbench
10
- // server command (basis), never an SDK run or a claimed SDK tool causality.
11
- const DELEGATED_SESSION_KIND = 'delegated-session';
12
- const DELEGATION_COMMAND_SOURCE = 'workbench-delegation-command';
13
- const DELEGATION_COMMAND_BASIS = 'server-command';
14
- const DELEGATION_PHASES = Object.freeze([
15
- 'reserved', 'child-created', 'child-create-failed', 'dispatching', 'dispatched', 'dispatch-unknown',
16
- ]);
17
- const DELEGATION_OUTCOMES = Object.freeze(['not-started', 'not-dispatched', 'pending', 'dispatched', 'unknown']);
18
- const IMAGE_MEDIA_TYPES = Object.freeze(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
19
- const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
20
- const MAX_IMAGES_PER_MESSAGE = 20;
21
- const MAX_IMAGE_MESSAGE_BYTES = 20 * 1024 * 1024;
22
- const TEXT_MEDIA_TYPES = Object.freeze(['text/plain', 'text/markdown', 'application/json', 'text/csv']);
23
- const TEXT_FILE_TYPES = Object.freeze({ '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.csv': 'text/csv' });
24
- const MAX_TEXT_BYTES = 512 * 1024;
25
- const MAX_ATTACHMENTS_PER_MESSAGE = MAX_IMAGES_PER_MESSAGE;
26
- const MAX_ATTACHMENT_MESSAGE_BYTES = MAX_IMAGE_MESSAGE_BYTES;
27
-
28
- // Capability matrix for every agent the terminal knows about. Drivers must
29
- // declare the same shape; the UI hides controls the active agent does not
30
- // support instead of failing.
31
- const AGENTS = Object.freeze({
32
- 'claude-code': Object.freeze({
33
- id: 'claude-code', label: 'Claude Code', transport: 'agent-sdk',
34
- authModes: Object.freeze(['profile-injected']),
35
- capabilities: Object.freeze({
36
- thinking: true, permissions: true, toolStream: true, images: true, backgroundTasks: true,
37
- resume: true, modelOverride: true, generationParams: Object.freeze(['effort']),
38
- }),
39
- }),
40
- codex: Object.freeze({
41
- id: 'codex', label: 'Codex', transport: 'cli-json',
42
- authModes: Object.freeze(['agent-owned', 'profile-injected']),
43
- capabilities: Object.freeze({
44
- thinking: false,
45
- backgroundTasks: false,
46
- // Permission modes map to exec sandboxes (read-only/workspace-write/danger).
47
- permissions: true,
48
- toolStream: true, images: true,
49
- resume: true, modelOverride: true,
50
- // The driver maps effort to `model_reasoning_effort` (low/medium/high/xhigh).
51
- generationParams: Object.freeze(['effort']),
52
- }),
53
- }),
54
- opencode: Object.freeze({
55
- id: 'opencode', label: 'OpenCode', transport: 'cli',
56
- authModes: Object.freeze(['agent-owned', 'profile-injected']),
57
- capabilities: Object.freeze({
58
- // The CLI streams reasoning parts; the driver maps them to thinking events.
59
- thinking: true,
60
- backgroundTasks: false,
61
- permissions: true, toolStream: true, images: true,
62
- resume: true, modelOverride: true, generationParams: Object.freeze([]),
63
- }),
64
- }),
65
- zcode: Object.freeze({
66
- id: 'zcode', label: 'ZCode', transport: 'cli',
67
- authModes: Object.freeze(['agent-owned', 'profile-injected']),
68
- capabilities: Object.freeze({
69
- // The bundled zcode CLI (app-server --stdio) exists, but the terminal has
70
- // no bridge driver yet; every capability stays off until one ships.
71
- thinking: false, permissions: false, toolStream: false, images: false,
72
- backgroundTasks: false,
73
- resume: false, modelOverride: false, generationParams: Object.freeze([]),
74
- }),
75
- }),
76
- });
77
- const AGENT_IDS = Object.freeze(Object.keys(AGENTS));
78
-
79
- const EVENT_TYPES = Object.freeze([
80
- 'workbench/stream-open', 'workbench/stream-reconnecting', 'workbench/resync',
81
- 'claude/init', 'claude/state', 'claude/session-ready', 'claude/user-prompt',
82
- 'claude/system-prompt', 'claude/text-start', 'claude/text-delta', 'claude/result',
83
- 'claude/thinking-start', 'claude/thinking-delta', 'claude/tool-use', 'claude/tool-result',
84
- 'claude/permission-request', 'claude/permission-resolved', 'claude/usage',
85
- 'claude/commands', 'claude/image', 'claude/retry', 'claude/error', 'claude/stderr', 'claude/aborted',
86
- 'claude/selection-changed',
87
- 'claude/turn-end', 'claude/task-started', 'claude/task-progress', 'claude/task-updated', 'claude/task-notification',
88
- 'claude/native-trace', 'claude/native-task-state', 'claude/native-task-transcript', 'claude/task-interrupted',
89
- 'workbench/sessions-snapshot', 'workbench/session-changed',
90
- ]);
91
-
92
- function cleanId(value, field = 'id') {
93
- const id = String(value || '').trim();
94
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(id)) {
95
- throw Object.assign(new Error(`${field} is invalid`), { status: 400 });
96
- }
97
- return id;
98
- }
99
-
100
- function normalizeContext(input = {}) {
101
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
102
- throw Object.assign(new Error('context must be an object'), { status: 400 });
103
- }
104
- return {
105
- contextId: cleanId(input.contextId, 'contextId'),
106
- contextType: input.contextType ? String(input.contextType).slice(0, 64) : undefined,
107
- displayName: input.displayName ? String(input.displayName).slice(0, 256) : undefined,
108
- workspaceRef: input.workspaceRef ? cleanId(input.workspaceRef, 'workspaceRef') : undefined,
109
- aiProfileId: input.aiProfileId === null ? null : input.aiProfileId ? cleanId(input.aiProfileId, 'aiProfileId') : undefined,
110
- model: input.model === null ? null : input.model ? String(input.model).trim().slice(0, 128) : undefined,
111
- metadata: input.metadata && typeof input.metadata === 'object' && !Array.isArray(input.metadata)
112
- ? input.metadata : undefined,
113
- };
114
- }
115
-
116
- function normalizeAgentId(value) {
117
- const id = String(value || DEFAULT_AGENT).trim();
118
- if (!AGENTS[id]) throw Object.assign(new Error(`unknown agent: ${id}`), { status: 400 });
119
- return id;
120
- }
121
-
122
- function normalizeEffort(value) {
123
- if (value == null || value === '') return undefined;
124
- const effort = String(value).trim();
125
- if (!EFFORT_LEVELS.includes(effort)) {
126
- throw Object.assign(new Error(`effort must be one of ${EFFORT_LEVELS.join(', ')}`), { status: 400 });
127
- }
128
- return effort;
129
- }
130
-
131
- function isAbsoluteLikePath(value) {
132
- return /^([a-zA-Z]:[\\/]|\\\\|\/)/.test(String(value || ''));
133
- }
134
-
135
- function normalizeProject(input = {}, idOverride) {
136
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
137
- throw Object.assign(new Error('project must be an object'), { status: 400 });
138
- }
139
- const id = cleanId(idOverride || input.id, 'projectId');
140
- const name = String(input.name || '').trim().slice(0, 128);
141
- if (!name) throw Object.assign(new Error('project name is required'), { status: 400 });
142
- const rootPath = String(input.rootPath || '').trim();
143
- if (!isAbsoluteLikePath(rootPath)) {
144
- throw Object.assign(new Error('project rootPath must be an absolute local directory'), { status: 400 });
145
- }
146
- return {
147
- id, name, rootPath,
148
- ...(input.defaultAiProfileId !== undefined ? { defaultAiProfileId: input.defaultAiProfileId === null ? null : cleanId(input.defaultAiProfileId, 'defaultAiProfileId') } : {}),
149
- ...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel === null ? null : String(input.defaultModel).trim().slice(0, 128) || null } : {}),
150
- ...(input.defaultAgentId !== undefined ? { defaultAgentId: normalizeAgentId(input.defaultAgentId) } : {}),
151
- createdAt: input.createdAt || new Date().toISOString(),
152
- updatedAt: input.updatedAt || input.createdAt || new Date().toISOString(),
153
- };
154
- }
155
-
156
- function normalizeModelEntries(input) {
157
- if (input == null) return [];
158
- if (!Array.isArray(input)) throw Object.assign(new Error('models.list must be an array'), { status: 400 });
159
- if (input.length > 100) throw Object.assign(new Error('models.list exceeds 100 entries'), { status: 400 });
160
- const seen = new Set();
161
- const entries = [];
162
- for (const item of input) {
163
- const raw = item && typeof item === 'object' && !Array.isArray(item) ? item : { id: item };
164
- const id = String(raw.id || raw.model || '').trim().slice(0, 128);
165
- if (!id) throw Object.assign(new Error('model entry id is required'), { status: 400 });
166
- if (seen.has(id)) continue;
167
- seen.add(id);
168
- const tags = Array.isArray(raw.tags)
169
- ? raw.tags.map(tag => String(tag).trim().slice(0, 32)).filter(Boolean).slice(0, 8)
170
- : [];
171
- // Detected context windows ride along so drivers (opencode limit.context)
172
- // and the capacity bubble can use them; invalid values are dropped.
173
- const contextWindow = Number(raw.contextWindow);
174
- const limits = Number.isFinite(contextWindow) && contextWindow > 0 && contextWindow <= 10_000_000
175
- ? { contextWindow: Math.floor(contextWindow) }
176
- : {};
177
- entries.push({ id, openaiId: String(raw.openaiId || id.replace(/\[1m\]$/i, '')).trim().slice(0, 128), label: String(raw.label || id).slice(0, 128), tags, ...limits });
178
- }
179
- return entries;
180
- }
181
-
182
- function normalizeProfile(input = {}, idOverride) {
183
- const id = cleanId(idOverride || input.id, 'profileId');
184
- const models = input.models && typeof input.models === 'object' ? input.models : {};
185
- const list = normalizeModelEntries(models.list);
186
- const fallbackModel = list.length ? list[0].id : '';
187
- const defaultModel = String(models.default || input.mainModel || input.model || fallbackModel).trim();
188
- return {
189
- id,
190
- name: String(input.name || id).trim().slice(0, 256),
191
- provider: String(input.provider || '').trim().slice(0, 128),
192
- protocol: String(input.protocol || 'anthropic-compatible').trim(),
193
- runtime: RUNTIME,
194
- baseUrl: String(input.baseUrl || '').trim().replace(/\/+$/, ''),
195
- openaiBaseUrl: resolveOpenAIBaseUrl(input),
196
- credentialRef: String(input.credentialRef || input.secretId || `credential:${id}`).trim(),
197
- models: {
198
- default: defaultModel,
199
- fast: String(models.fast || defaultModel).trim(),
200
- reasoning: String(models.reasoning || defaultModel).trim(),
201
- coding: String(models.coding || defaultModel).trim(),
202
- largeContext: String(models.largeContext || defaultModel).trim(),
203
- list,
204
- },
205
- contextWindow: positiveInt(input.contextWindow, 200000),
206
- timeoutMs: positiveInt(input.timeoutMs, 300000),
207
- permissionMode: PERMISSION_MODES.includes(input.permissionMode) ? input.permissionMode : 'default',
208
- systemPrompt: normalizeSystemPrompt(input.systemPrompt),
209
- enabled: input.enabled !== false,
210
- };
211
- }
212
-
213
- function positiveInt(value, fallback) {
214
- const parsed = Number(value);
215
- return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
216
- }
217
-
218
- function resolveOpenAIBaseUrl(profile = {}) {
219
- const explicit = String(profile.openaiBaseUrl || '').trim().replace(/\/+$/, '');
220
- if (explicit) return explicit;
221
- const baseUrl = String(profile.baseUrl || '').trim().replace(/\/+$/, '');
222
- if (profile.protocol === 'openai-compatible') return baseUrl;
223
- let hostname = '';
224
- try { hostname = new URL(baseUrl).hostname; } catch {}
225
- if (hostname === 'api.minimax.io') return 'https://api.minimax.io/v1';
226
- if (hostname === 'api.minimaxi.com' || /minimax/i.test(String(profile.provider || ''))) return 'https://api.minimaxi.com/v1';
227
- return '';
228
- }
229
-
230
- function resolveOpenAIModel(profile = {}, model) {
231
- const id = String(model || profile.mainModel || profile.models && profile.models.default || '').trim();
232
- const entries = profile.models && profile.models.list;
233
- const entry = Array.isArray(entries) && entries.find(item => item.id === id);
234
- return String(entry && entry.openaiId || id.replace(/\[1m\]$/i, '')).trim();
235
- }
236
-
237
- function normalizeAttachments(input) {
238
- if (input == null) return [];
239
- if (!Array.isArray(input)) throw Object.assign(new Error('attachments must be an array'), { status: 400 });
240
- if (input.length > MAX_ATTACHMENTS_PER_MESSAGE) throw Object.assign(new Error(`at most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments are allowed`), { status: 413 });
241
- let totalBytes = 0;
242
- return input.map((item, index) => {
243
- if (!item || typeof item !== 'object' || Array.isArray(item)) throw Object.assign(new Error(`attachment ${index + 1} is invalid`), { status: 400 });
244
- const name = String(item.name || `image-${index + 1}`).replace(/[\u0000-\u001f]/g, '').slice(0, 255);
245
- let mediaType = String(item.mediaType || item.mimeType || '').toLowerCase().split(';')[0].trim();
246
- const isImage = IMAGE_MEDIA_TYPES.includes(mediaType);
247
- if (!isImage) {
248
- const extension = (name.match(/\.[^.\\/]+$/) || [''])[0].toLowerCase();
249
- const textType = TEXT_FILE_TYPES[extension];
250
- if (!textType || !['', 'application/octet-stream', 'text/plain', textType].includes(mediaType)) {
251
- throw Object.assign(new Error(`attachment ${index + 1} has an unsupported type; use images or .txt, .md, .json, .csv files`), { status: 415 });
252
- }
253
- mediaType = textType;
254
- }
255
- const data = String(item.data || '').replace(/^data:[^;]+;base64,/, '').replace(/\s+/g, '');
256
- if (!data || !/^[a-zA-Z0-9+/]+={0,2}$/.test(data) || data.length % 4 === 1 || (data.includes('=') && data.length % 4 !== 0)) throw Object.assign(new Error(`attachment ${index + 1} has invalid base64 data`), { status: 400 });
257
- const size = Math.floor((data.length * 3) / 4) - (data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0);
258
- const maxBytes = isImage ? MAX_IMAGE_BYTES : MAX_TEXT_BYTES;
259
- if (size <= 0 || size > maxBytes) throw Object.assign(new Error(`attachment ${index + 1} exceeds ${maxBytes} bytes`), { status: 413 });
260
- const bytes = Buffer.from(data, 'base64');
261
- if (bytes.toString('base64').replace(/=+$/, '') !== data.replace(/=+$/, '')) throw Object.assign(new Error(`attachment ${index + 1} has invalid base64 data`), { status: 400 });
262
- if (!isImage) {
263
- try {
264
- const content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
265
- if (content.includes('\u0000')) throw new Error('binary content');
266
- } catch { throw Object.assign(new Error(`attachment ${index + 1} must contain UTF-8 text`), { status: 415 }); }
267
- }
268
- totalBytes += size;
269
- if (totalBytes > MAX_ATTACHMENT_MESSAGE_BYTES) throw Object.assign(new Error(`attachment payload exceeds ${MAX_ATTACHMENT_MESSAGE_BYTES} bytes`), { status: 413 });
270
- return {
271
- id: item.id ? cleanId(item.id, 'attachmentId') : undefined,
272
- name,
273
- mediaType,
274
- size,
275
- width: positiveInt(item.width, 0) || undefined,
276
- height: positiveInt(item.height, 0) || undefined,
277
- data,
278
- };
279
- });
280
- }
281
-
282
- function normalizeImageAttachments(input) {
283
- const attachments = normalizeAttachments(input);
284
- if (attachments.some(item => !IMAGE_MEDIA_TYPES.includes(item.mediaType))) throw Object.assign(new Error('unsupported image type'), { status: 415 });
285
- return attachments;
286
- }
287
-
288
- function appendTextAttachments(text, attachments = []) {
289
- const blocks = attachments.filter(item => TEXT_MEDIA_TYPES.includes(item.mediaType)).map(item => {
290
- const name = String(item.name).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
291
- return `<attachment name="${name}">\n${Buffer.from(item.data, 'base64').toString('utf8')}\n</attachment>`;
292
- });
293
- return [String(text || ''), ...blocks].filter(Boolean).join('\n\n');
294
- }
295
-
296
- function normalizeSystemPrompt(value) {
297
- if (value && typeof value === 'object' && !Array.isArray(value)) return value;
298
- if (typeof value === 'string' && value.trim()) return { type: 'custom', content: value };
299
- return { type: 'preset', preset: 'claude_code', content: '' };
300
- }
301
-
302
- module.exports = {
303
- API_VERSION, DEFAULT_API_PREFIX, RUNTIME, DEFAULT_AGENT, AGENTS, AGENT_IDS, EFFORT_LEVELS,
304
- PERMISSION_MODES, IMAGE_MEDIA_TYPES, TEXT_MEDIA_TYPES, TEXT_FILE_TYPES,
305
- MAX_IMAGE_BYTES, MAX_IMAGES_PER_MESSAGE, MAX_IMAGE_MESSAGE_BYTES, EVENT_TYPES,
306
- MAX_TEXT_BYTES, MAX_ATTACHMENTS_PER_MESSAGE, MAX_ATTACHMENT_MESSAGE_BYTES,
307
- DELEGATED_SESSION_KIND, DELEGATION_COMMAND_SOURCE, DELEGATION_COMMAND_BASIS,
308
- DELEGATION_PHASES, DELEGATION_OUTCOMES,
309
- cleanId, normalizeContext, normalizeProfile, normalizeProject, normalizeAgentId, normalizeEffort,
310
- normalizeModelEntries, normalizeImageAttachments, normalizeAttachments, appendTextAttachments,
311
- resolveOpenAIBaseUrl, resolveOpenAIModel, positiveInt,
312
- };
1
+ 'use strict';
2
+
3
+ const API_VERSION = 'v1';
4
+ const DEFAULT_API_PREFIX = `/api/claude-workbench/${API_VERSION}`;
5
+ const RUNTIME = 'claude-code';
6
+ const DEFAULT_AGENT = 'claude-code';
7
+ const EFFORT_LEVELS = Object.freeze(['low', 'medium', 'high', 'max']);
8
+ const PERMISSION_MODES = Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']);
9
+ // Delegated-session identity split: a delegation command is a Workbench
10
+ // server command (basis), never an SDK run or a claimed SDK tool causality.
11
+ const DELEGATED_SESSION_KIND = 'delegated-session';
12
+ const DELEGATION_COMMAND_SOURCE = 'workbench-delegation-command';
13
+ const DELEGATION_COMMAND_BASIS = 'server-command';
14
+ const DELEGATION_PHASES = Object.freeze([
15
+ 'reserved', 'child-created', 'child-create-failed', 'dispatching', 'dispatched', 'dispatch-unknown',
16
+ ]);
17
+ const DELEGATION_OUTCOMES = Object.freeze(['not-started', 'not-dispatched', 'pending', 'dispatched', 'unknown']);
18
+ const IMAGE_MEDIA_TYPES = Object.freeze(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
19
+ const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
20
+ const MAX_IMAGES_PER_MESSAGE = 20;
21
+ const MAX_IMAGE_MESSAGE_BYTES = 20 * 1024 * 1024;
22
+ const TEXT_MEDIA_TYPES = Object.freeze(['text/plain', 'text/markdown', 'application/json', 'text/csv']);
23
+ const TEXT_FILE_TYPES = Object.freeze({ '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.csv': 'text/csv' });
24
+ const MAX_TEXT_BYTES = 512 * 1024;
25
+ const MAX_ATTACHMENTS_PER_MESSAGE = MAX_IMAGES_PER_MESSAGE;
26
+ const MAX_ATTACHMENT_MESSAGE_BYTES = MAX_IMAGE_MESSAGE_BYTES;
27
+
28
+ // Capability matrix for every agent the terminal knows about. Drivers must
29
+ // declare the same shape; the UI hides controls the active agent does not
30
+ // support instead of failing.
31
+ const AGENTS = Object.freeze({
32
+ 'claude-code': Object.freeze({
33
+ id: 'claude-code', label: 'Claude Code', transport: 'agent-sdk',
34
+ authModes: Object.freeze(['profile-injected']),
35
+ capabilities: Object.freeze({
36
+ thinking: true, permissions: true, toolStream: true, images: true, backgroundTasks: true,
37
+ resume: true, modelOverride: true, generationParams: Object.freeze(['effort']),
38
+ }),
39
+ }),
40
+ codex: Object.freeze({
41
+ id: 'codex', label: 'Codex', transport: 'cli-json',
42
+ authModes: Object.freeze(['agent-owned', 'profile-injected']),
43
+ capabilities: Object.freeze({
44
+ thinking: false,
45
+ backgroundTasks: false,
46
+ // Permission modes map to exec sandboxes (read-only/workspace-write/danger).
47
+ permissions: true,
48
+ toolStream: true, images: true,
49
+ resume: true, modelOverride: true,
50
+ // The driver maps effort to `model_reasoning_effort` (low/medium/high/xhigh).
51
+ generationParams: Object.freeze(['effort']),
52
+ }),
53
+ }),
54
+ opencode: Object.freeze({
55
+ id: 'opencode', label: 'OpenCode', transport: 'cli',
56
+ authModes: Object.freeze(['agent-owned', 'profile-injected']),
57
+ capabilities: Object.freeze({
58
+ // The CLI streams reasoning parts; the driver maps them to thinking events.
59
+ thinking: true,
60
+ backgroundTasks: false,
61
+ permissions: true, toolStream: true, images: true,
62
+ resume: true, modelOverride: true, generationParams: Object.freeze([]),
63
+ }),
64
+ }),
65
+ zcode: Object.freeze({
66
+ id: 'zcode', label: 'ZCode', transport: 'cli',
67
+ authModes: Object.freeze(['agent-owned', 'profile-injected']),
68
+ capabilities: Object.freeze({
69
+ // The bundled zcode CLI (app-server --stdio) exists, but the terminal has
70
+ // no bridge driver yet; every capability stays off until one ships.
71
+ thinking: false, permissions: false, toolStream: false, images: false,
72
+ backgroundTasks: false,
73
+ resume: false, modelOverride: false, generationParams: Object.freeze([]),
74
+ }),
75
+ }),
76
+ });
77
+ const AGENT_IDS = Object.freeze(Object.keys(AGENTS));
78
+
79
+ const EVENT_TYPES = Object.freeze([
80
+ 'workbench/stream-open', 'workbench/stream-reconnecting', 'workbench/resync',
81
+ 'claude/init', 'claude/state', 'claude/session-ready', 'claude/user-prompt',
82
+ 'claude/system-prompt', 'claude/text-start', 'claude/text-delta', 'claude/result',
83
+ 'claude/thinking-start', 'claude/thinking-delta', 'claude/tool-use', 'claude/tool-result',
84
+ 'claude/permission-request', 'claude/permission-resolved', 'claude/usage',
85
+ 'claude/commands', 'claude/image', 'claude/retry', 'claude/error', 'claude/stderr', 'claude/aborted',
86
+ 'claude/selection-changed',
87
+ 'claude/turn-end', 'claude/task-started', 'claude/task-progress', 'claude/task-updated', 'claude/task-notification',
88
+ 'claude/native-trace', 'claude/native-task-state', 'claude/native-task-transcript', 'claude/task-interrupted',
89
+ 'workbench/sessions-snapshot', 'workbench/session-changed',
90
+ ]);
91
+
92
+ function cleanId(value, field = 'id') {
93
+ const id = String(value || '').trim();
94
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(id)) {
95
+ throw Object.assign(new Error(`${field} is invalid`), { status: 400 });
96
+ }
97
+ return id;
98
+ }
99
+
100
+ function normalizeContext(input = {}) {
101
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
102
+ throw Object.assign(new Error('context must be an object'), { status: 400 });
103
+ }
104
+ return {
105
+ contextId: cleanId(input.contextId, 'contextId'),
106
+ contextType: input.contextType ? String(input.contextType).slice(0, 64) : undefined,
107
+ displayName: input.displayName ? String(input.displayName).slice(0, 256) : undefined,
108
+ workspaceRef: input.workspaceRef ? cleanId(input.workspaceRef, 'workspaceRef') : undefined,
109
+ aiProfileId: input.aiProfileId === null ? null : input.aiProfileId ? cleanId(input.aiProfileId, 'aiProfileId') : undefined,
110
+ model: input.model === null ? null : input.model ? String(input.model).trim().slice(0, 128) : undefined,
111
+ metadata: input.metadata && typeof input.metadata === 'object' && !Array.isArray(input.metadata)
112
+ ? input.metadata : undefined,
113
+ };
114
+ }
115
+
116
+ function normalizeAgentId(value) {
117
+ const id = String(value || DEFAULT_AGENT).trim();
118
+ if (!AGENTS[id]) throw Object.assign(new Error(`unknown agent: ${id}`), { status: 400 });
119
+ return id;
120
+ }
121
+
122
+ function normalizeEffort(value) {
123
+ if (value == null || value === '') return undefined;
124
+ const effort = String(value).trim();
125
+ if (!EFFORT_LEVELS.includes(effort)) {
126
+ throw Object.assign(new Error(`effort must be one of ${EFFORT_LEVELS.join(', ')}`), { status: 400 });
127
+ }
128
+ return effort;
129
+ }
130
+
131
+ function isAbsoluteLikePath(value) {
132
+ return /^([a-zA-Z]:[\\/]|\\\\|\/)/.test(String(value || ''));
133
+ }
134
+
135
+ function normalizeProject(input = {}, idOverride) {
136
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
137
+ throw Object.assign(new Error('project must be an object'), { status: 400 });
138
+ }
139
+ const id = cleanId(idOverride || input.id, 'projectId');
140
+ const name = String(input.name || '').trim().slice(0, 128);
141
+ if (!name) throw Object.assign(new Error('project name is required'), { status: 400 });
142
+ const rootPath = String(input.rootPath || '').trim();
143
+ if (!isAbsoluteLikePath(rootPath)) {
144
+ throw Object.assign(new Error('project rootPath must be an absolute local directory'), { status: 400 });
145
+ }
146
+ return {
147
+ id, name, rootPath,
148
+ ...(input.defaultAiProfileId !== undefined ? { defaultAiProfileId: input.defaultAiProfileId === null ? null : cleanId(input.defaultAiProfileId, 'defaultAiProfileId') } : {}),
149
+ ...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel === null ? null : String(input.defaultModel).trim().slice(0, 128) || null } : {}),
150
+ ...(input.defaultAgentId !== undefined ? { defaultAgentId: normalizeAgentId(input.defaultAgentId) } : {}),
151
+ createdAt: input.createdAt || new Date().toISOString(),
152
+ updatedAt: input.updatedAt || input.createdAt || new Date().toISOString(),
153
+ };
154
+ }
155
+
156
+ function normalizeModelEntries(input) {
157
+ if (input == null) return [];
158
+ if (!Array.isArray(input)) throw Object.assign(new Error('models.list must be an array'), { status: 400 });
159
+ if (input.length > 100) throw Object.assign(new Error('models.list exceeds 100 entries'), { status: 400 });
160
+ const seen = new Set();
161
+ const entries = [];
162
+ for (const item of input) {
163
+ const raw = item && typeof item === 'object' && !Array.isArray(item) ? item : { id: item };
164
+ const id = String(raw.id || raw.model || '').trim().slice(0, 128);
165
+ if (!id) throw Object.assign(new Error('model entry id is required'), { status: 400 });
166
+ if (seen.has(id)) continue;
167
+ seen.add(id);
168
+ const tags = Array.isArray(raw.tags)
169
+ ? raw.tags.map(tag => String(tag).trim().slice(0, 32)).filter(Boolean).slice(0, 8)
170
+ : [];
171
+ // Detected context windows ride along so drivers (opencode limit.context)
172
+ // and the capacity bubble can use them; invalid values are dropped.
173
+ const contextWindow = Number(raw.contextWindow);
174
+ const limits = Number.isFinite(contextWindow) && contextWindow > 0 && contextWindow <= 10_000_000
175
+ ? { contextWindow: Math.floor(contextWindow) }
176
+ : {};
177
+ entries.push({ id, openaiId: String(raw.openaiId || id.replace(/\[1m\]$/i, '')).trim().slice(0, 128), label: String(raw.label || id).slice(0, 128), tags, ...limits });
178
+ }
179
+ return entries;
180
+ }
181
+
182
+ function normalizeProfile(input = {}, idOverride) {
183
+ const id = cleanId(idOverride || input.id, 'profileId');
184
+ const models = input.models && typeof input.models === 'object' ? input.models : {};
185
+ const list = normalizeModelEntries(models.list);
186
+ const fallbackModel = list.length ? list[0].id : '';
187
+ const defaultModel = String(models.default || input.mainModel || input.model || fallbackModel).trim();
188
+ return {
189
+ id,
190
+ name: String(input.name || id).trim().slice(0, 256),
191
+ provider: String(input.provider || '').trim().slice(0, 128),
192
+ protocol: String(input.protocol || 'anthropic-compatible').trim(),
193
+ runtime: RUNTIME,
194
+ baseUrl: String(input.baseUrl || '').trim().replace(/\/+$/, ''),
195
+ openaiBaseUrl: resolveOpenAIBaseUrl(input),
196
+ credentialRef: String(input.credentialRef || input.secretId || `credential:${id}`).trim(),
197
+ models: {
198
+ default: defaultModel,
199
+ fast: String(models.fast || defaultModel).trim(),
200
+ reasoning: String(models.reasoning || defaultModel).trim(),
201
+ coding: String(models.coding || defaultModel).trim(),
202
+ largeContext: String(models.largeContext || defaultModel).trim(),
203
+ list,
204
+ },
205
+ contextWindow: positiveInt(input.contextWindow, 200000),
206
+ timeoutMs: positiveInt(input.timeoutMs, 300000),
207
+ permissionMode: PERMISSION_MODES.includes(input.permissionMode) ? input.permissionMode : 'default',
208
+ systemPrompt: normalizeSystemPrompt(input.systemPrompt),
209
+ enabled: input.enabled !== false,
210
+ };
211
+ }
212
+
213
+ function positiveInt(value, fallback) {
214
+ const parsed = Number(value);
215
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
216
+ }
217
+
218
+ function resolveOpenAIBaseUrl(profile = {}) {
219
+ const explicit = String(profile.openaiBaseUrl || '').trim().replace(/\/+$/, '');
220
+ if (explicit) return explicit;
221
+ const baseUrl = String(profile.baseUrl || '').trim().replace(/\/+$/, '');
222
+ if (profile.protocol === 'openai-compatible') return baseUrl;
223
+ let hostname = '';
224
+ try { hostname = new URL(baseUrl).hostname; } catch {}
225
+ if (hostname === 'api.minimax.io') return 'https://api.minimax.io/v1';
226
+ if (hostname === 'api.minimaxi.com' || /minimax/i.test(String(profile.provider || ''))) return 'https://api.minimaxi.com/v1';
227
+ return '';
228
+ }
229
+
230
+ function resolveOpenAIModel(profile = {}, model) {
231
+ const id = String(model || profile.mainModel || profile.models && profile.models.default || '').trim();
232
+ const entries = profile.models && profile.models.list;
233
+ const entry = Array.isArray(entries) && entries.find(item => item.id === id);
234
+ return String(entry && entry.openaiId || id.replace(/\[1m\]$/i, '')).trim();
235
+ }
236
+
237
+ function normalizeAttachments(input) {
238
+ if (input == null) return [];
239
+ if (!Array.isArray(input)) throw Object.assign(new Error('attachments must be an array'), { status: 400 });
240
+ if (input.length > MAX_ATTACHMENTS_PER_MESSAGE) throw Object.assign(new Error(`at most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments are allowed`), { status: 413 });
241
+ let totalBytes = 0;
242
+ return input.map((item, index) => {
243
+ if (!item || typeof item !== 'object' || Array.isArray(item)) throw Object.assign(new Error(`attachment ${index + 1} is invalid`), { status: 400 });
244
+ const name = String(item.name || `image-${index + 1}`).replace(/[\u0000-\u001f]/g, '').slice(0, 255);
245
+ let mediaType = String(item.mediaType || item.mimeType || '').toLowerCase().split(';')[0].trim();
246
+ const isImage = IMAGE_MEDIA_TYPES.includes(mediaType);
247
+ if (!isImage) {
248
+ const extension = (name.match(/\.[^.\\/]+$/) || [''])[0].toLowerCase();
249
+ const textType = TEXT_FILE_TYPES[extension];
250
+ if (!textType || !['', 'application/octet-stream', 'text/plain', textType].includes(mediaType)) {
251
+ throw Object.assign(new Error(`attachment ${index + 1} has an unsupported type; use images or .txt, .md, .json, .csv files`), { status: 415 });
252
+ }
253
+ mediaType = textType;
254
+ }
255
+ const data = String(item.data || '').replace(/^data:[^;]+;base64,/, '').replace(/\s+/g, '');
256
+ if (!data || !/^[a-zA-Z0-9+/]+={0,2}$/.test(data) || data.length % 4 === 1 || (data.includes('=') && data.length % 4 !== 0)) throw Object.assign(new Error(`attachment ${index + 1} has invalid base64 data`), { status: 400 });
257
+ const size = Math.floor((data.length * 3) / 4) - (data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0);
258
+ const maxBytes = isImage ? MAX_IMAGE_BYTES : MAX_TEXT_BYTES;
259
+ if (size <= 0 || size > maxBytes) throw Object.assign(new Error(`attachment ${index + 1} exceeds ${maxBytes} bytes`), { status: 413 });
260
+ const bytes = Buffer.from(data, 'base64');
261
+ if (bytes.toString('base64').replace(/=+$/, '') !== data.replace(/=+$/, '')) throw Object.assign(new Error(`attachment ${index + 1} has invalid base64 data`), { status: 400 });
262
+ if (!isImage) {
263
+ try {
264
+ const content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
265
+ if (content.includes('\u0000')) throw new Error('binary content');
266
+ } catch { throw Object.assign(new Error(`attachment ${index + 1} must contain UTF-8 text`), { status: 415 }); }
267
+ }
268
+ totalBytes += size;
269
+ if (totalBytes > MAX_ATTACHMENT_MESSAGE_BYTES) throw Object.assign(new Error(`attachment payload exceeds ${MAX_ATTACHMENT_MESSAGE_BYTES} bytes`), { status: 413 });
270
+ return {
271
+ id: item.id ? cleanId(item.id, 'attachmentId') : undefined,
272
+ name,
273
+ mediaType,
274
+ size,
275
+ width: positiveInt(item.width, 0) || undefined,
276
+ height: positiveInt(item.height, 0) || undefined,
277
+ data,
278
+ };
279
+ });
280
+ }
281
+
282
+ function normalizeImageAttachments(input) {
283
+ const attachments = normalizeAttachments(input);
284
+ if (attachments.some(item => !IMAGE_MEDIA_TYPES.includes(item.mediaType))) throw Object.assign(new Error('unsupported image type'), { status: 415 });
285
+ return attachments;
286
+ }
287
+
288
+ function appendTextAttachments(text, attachments = []) {
289
+ const blocks = attachments.filter(item => TEXT_MEDIA_TYPES.includes(item.mediaType)).map(item => {
290
+ const name = String(item.name).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
291
+ return `<attachment name="${name}">\n${Buffer.from(item.data, 'base64').toString('utf8')}\n</attachment>`;
292
+ });
293
+ return [String(text || ''), ...blocks].filter(Boolean).join('\n\n');
294
+ }
295
+
296
+ function normalizeSystemPrompt(value) {
297
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value;
298
+ if (typeof value === 'string' && value.trim()) return { type: 'custom', content: value };
299
+ return { type: 'preset', preset: 'claude_code', content: '' };
300
+ }
301
+
302
+ module.exports = {
303
+ API_VERSION, DEFAULT_API_PREFIX, RUNTIME, DEFAULT_AGENT, AGENTS, AGENT_IDS, EFFORT_LEVELS,
304
+ PERMISSION_MODES, IMAGE_MEDIA_TYPES, TEXT_MEDIA_TYPES, TEXT_FILE_TYPES,
305
+ MAX_IMAGE_BYTES, MAX_IMAGES_PER_MESSAGE, MAX_IMAGE_MESSAGE_BYTES, EVENT_TYPES,
306
+ MAX_TEXT_BYTES, MAX_ATTACHMENTS_PER_MESSAGE, MAX_ATTACHMENT_MESSAGE_BYTES,
307
+ DELEGATED_SESSION_KIND, DELEGATION_COMMAND_SOURCE, DELEGATION_COMMAND_BASIS,
308
+ DELEGATION_PHASES, DELEGATION_OUTCOMES,
309
+ cleanId, normalizeContext, normalizeProfile, normalizeProject, normalizeAgentId, normalizeEffort,
310
+ normalizeModelEntries, normalizeImageAttachments, normalizeAttachments, appendTextAttachments,
311
+ resolveOpenAIBaseUrl, resolveOpenAIModel, positiveInt,
312
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/contracts",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",