@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3

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 (120) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +636 -613
  4. package/apps/agent-terminal/app.js +125 -65
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +171 -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 +187 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +623 -519
  11. package/apps/ai-profile-settings-host/index.html +25 -25
  12. package/apps/ai-profile-settings-host/page.css +1 -1
  13. package/apps/ai-profile-settings-host/page.js +113 -113
  14. package/apps/minimal-test-host/README.md +15 -15
  15. package/apps/minimal-test-host/host.css +2 -2
  16. package/apps/minimal-test-host/host.js +35 -35
  17. package/apps/minimal-test-host/index.html +58 -58
  18. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  19. package/docs/sdk/FILE_LAYOUT.md +31 -31
  20. package/docs/sdk/INTEGRATION.md +126 -126
  21. package/docs/sdk/README.md +21 -21
  22. package/docs/sdk/SESSION_EVENTS.md +13 -13
  23. package/docs/sdk/openapi.yaml +8 -1
  24. package/package.json +1 -1
  25. package/packages/client/README.md +29 -29
  26. package/packages/client/index.d.ts +122 -122
  27. package/packages/client/index.js +260 -260
  28. package/packages/client/package.json +2 -2
  29. package/packages/contracts/README.md +9 -9
  30. package/packages/contracts/index.d.ts +519 -517
  31. package/packages/contracts/index.js +312 -312
  32. package/packages/contracts/package.json +1 -1
  33. package/packages/core/README.md +8 -8
  34. package/packages/core/index.d.ts +76 -76
  35. package/packages/core/index.js +222 -222
  36. package/packages/core/package.json +2 -2
  37. package/packages/server/README.md +17 -17
  38. package/packages/server/bin/agent-terminal-server.js +42 -42
  39. package/packages/server/bin/claude-workbench.js +225 -225
  40. package/packages/server/index.d.ts +171 -171
  41. package/packages/server/index.js +230 -230
  42. package/packages/server/lib/agent-discovery.js +133 -133
  43. package/packages/server/lib/agent-registry.js +143 -143
  44. package/packages/server/lib/agent-spawn-env.js +17 -17
  45. package/packages/server/lib/attachment-store.js +78 -78
  46. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  47. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  48. package/packages/server/lib/capture-settings-store.js +35 -35
  49. package/packages/server/lib/claude-history-recovery.js +87 -87
  50. package/packages/server/lib/cli-session-recovery.js +28 -28
  51. package/packages/server/lib/context-resolver.js +55 -55
  52. package/packages/server/lib/conversation-capture.js +51 -51
  53. package/packages/server/lib/delegation-phase-store.js +357 -357
  54. package/packages/server/lib/delegation-store.js +474 -468
  55. package/packages/server/lib/diagnostics.js +32 -32
  56. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  57. package/packages/server/lib/drivers/codex-driver.js +1442 -1442
  58. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  59. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  60. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  61. package/packages/server/lib/event-stream.js +138 -138
  62. package/packages/server/lib/folder-picker.js +623 -623
  63. package/packages/server/lib/http-handler.js +339 -339
  64. package/packages/server/lib/json-file.js +26 -26
  65. package/packages/server/lib/knowledge-context.js +33 -33
  66. package/packages/server/lib/model-detection.js +295 -295
  67. package/packages/server/lib/parent-context.js +44 -0
  68. package/packages/server/lib/profile-store.js +283 -283
  69. package/packages/server/lib/project-store.js +102 -102
  70. package/packages/server/lib/runtime-loader.js +120 -120
  71. package/packages/server/lib/session-archive-store.js +53 -53
  72. package/packages/server/lib/session-history.js +612 -612
  73. package/packages/server/lib/system-doctor.js +125 -125
  74. package/packages/server/lib/ui-preferences-store.js +48 -48
  75. package/packages/server/lib/workbench-service.js +1696 -1675
  76. package/packages/server/package.json +2 -2
  77. package/packages/ui/README.md +30 -30
  78. package/packages/ui/ai-profile-settings.d.ts +45 -45
  79. package/packages/ui/ai-profile-settings.js +283 -283
  80. package/packages/ui/claude-terminal.d.ts +31 -31
  81. package/packages/ui/claude-terminal.js +343 -343
  82. package/packages/ui/index.d.ts +18 -18
  83. package/packages/ui/index.js +51 -51
  84. package/packages/ui/package.json +2 -2
  85. package/packages/ui/theme.css +13 -13
  86. package/src/assets/claude-prompts.json +21 -21
  87. package/src/backend/http/ai-profile-routes.js +61 -61
  88. package/src/backend/http/claude-routes.js +170 -170
  89. package/src/backend/http/node-helpers.js +45 -45
  90. package/src/backend/index.js +21 -21
  91. package/src/backend/lib/ai-adapter.js +144 -144
  92. package/src/backend/lib/ai-profile-store.js +259 -259
  93. package/src/backend/lib/ai-vendor-presets.js +83 -83
  94. package/src/backend/lib/ai-workspace.js +140 -140
  95. package/src/backend/lib/automation-config.js +253 -253
  96. package/src/backend/lib/claude-cli-runner.js +2392 -2392
  97. package/src/backend/lib/data-dir.js +169 -169
  98. package/src/backend/lib/llm-client.js +176 -176
  99. package/src/backend/lib/model-context-windows.js +82 -82
  100. package/src/backend/lib/native-task-control.js +217 -217
  101. package/src/backend/lib/native-task-history-index.js +475 -475
  102. package/src/backend/lib/native-task-history-reader.js +526 -526
  103. package/src/backend/lib/native-task-messages.js +457 -457
  104. package/src/backend/lib/native-task-registry.js +325 -325
  105. package/src/backend/lib/native-task-runtime.js +242 -242
  106. package/src/backend/lib/native-task-trace.js +440 -440
  107. package/src/backend/lib/native-task-transcript.js +393 -393
  108. package/src/backend/lib/prompt-registry.js +166 -166
  109. package/src/backend/lib/runtime-config.js +121 -121
  110. package/src/backend/lib/sdk-session-stream.js +143 -143
  111. package/src/backend/lib/sdk-turn-stream.js +16 -16
  112. package/src/backend/lib/store-scope.js +131 -131
  113. package/src/backend/lib/task-identity.js +104 -104
  114. package/src/backend/lib/task-state-projection.js +58 -58
  115. package/src/index.js +7 -7
  116. package/src/sdk-wave-a.ts +1 -1
  117. package/src/sdk-wave-b.ts +1 -1
  118. package/src/sdk-wave-c.ts +1 -1
  119. package/src/sdk-wave-d.ts +1 -1
  120. 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.3",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",