@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,259 +1,259 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { getDataDir } = require('./data-dir');
4
- const { completeText } = require('./llm-client');
5
-
6
- const SCHEMA = 'ai-profiles/v1';
7
- const DEFAULT_TIMEOUT_MS = 300000;
8
- const DEFAULT_CONTEXT_WINDOW = 200000;
9
-
10
- function readJsonOrDefault(filePath, fallback) {
11
- try {
12
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
13
- } catch {
14
- return fallback;
15
- }
16
- }
17
-
18
- function writeJson(filePath, value) {
19
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
- fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n', 'utf8');
21
- }
22
-
23
- function maskSecret(value) {
24
- if (!value) return '';
25
- const text = String(value);
26
- if (text.length <= 8) return '********';
27
- return `${text.slice(0, 4)}...${text.slice(-4)}`;
28
- }
29
-
30
- function toPositiveInteger(value, fallback) {
31
- const n = Number(value);
32
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
33
- }
34
-
35
- function normalizeSystemPrompt(value) {
36
- if (value && typeof value === 'object') return value;
37
- if (typeof value === 'string' && value.trim()) {
38
- try {
39
- return JSON.parse(value);
40
- } catch {
41
- return value;
42
- }
43
- }
44
- return { type: 'preset', preset: 'claude_code' };
45
- }
46
-
47
- function isBlankAiProfileDraft(profile) {
48
- return ![
49
- profile.provider,
50
- profile.baseUrl,
51
- profile.apiBaseUrl,
52
- profile.anthropicBaseUrl,
53
- profile.apiKey,
54
- profile.authToken,
55
- profile.anthropicAuthToken,
56
- profile.mainModel,
57
- profile.model,
58
- profile.website,
59
- profile.notes,
60
- ].some(value => String(value || '').trim());
61
- }
62
-
63
- function profileImplementation(profile) {
64
- return profile && (profile.implementation || profile.adapter || 'claude-code-agent') || 'claude-code-agent';
65
- }
66
-
67
- function normalizeProfilesConfig(profiles, existingCfg = { profiles: [] }, activeAiProfileId = null, options = {}) {
68
- const existingById = new Map((existingCfg.profiles || []).filter(p => p && p.id).map(p => [p.id, p]));
69
- const seen = new Set();
70
- const normalized = [];
71
- for (const raw of profiles || []) {
72
- if (!raw || typeof raw !== 'object') continue;
73
- const id = String(raw.id || '').trim();
74
- if (id && id !== activeAiProfileId && options.dropBlankDrafts !== false && isBlankAiProfileDraft(raw)) continue;
75
- if (!id) throw new Error('AI profile is missing id');
76
- if (seen.has(id)) throw new Error(`duplicate AI profile id: ${id}`);
77
- seen.add(id);
78
- const existing = existingById.get(id) || {};
79
- const provider = String(raw.provider || raw.name || existing.provider || existing.name || id).trim() || id;
80
- const profile = {
81
- ...existing,
82
- ...raw,
83
- id,
84
- name: String(raw.name || raw.provider || existing.name || provider || id).trim() || id,
85
- provider,
86
- enabled: raw.enabled !== false,
87
- implementation: 'claude-code-agent',
88
- baseUrl: String(raw.baseUrl || raw.apiBaseUrl || raw.anthropicBaseUrl || '').trim(),
89
- notes: String(raw.notes || '').trim(),
90
- website: String(raw.website || '').trim(),
91
- mainModel: String(raw.mainModel || raw.model || '').trim(),
92
- thinkingModel: String(raw.thinkingModel || raw.mainModel || raw.model || '').trim(),
93
- haikuModel: String(raw.haikuModel || raw.mainModel || raw.model || '').trim(),
94
- sonnetModel: String(raw.sonnetModel || raw.mainModel || raw.model || '').trim(),
95
- opusModel: String(raw.opusModel || raw.mainModel || raw.model || '').trim(),
96
- model: String(raw.model || raw.mainModel || '').trim(),
97
- timeoutMs: toPositiveInteger(raw.timeoutMs, DEFAULT_TIMEOUT_MS),
98
- contextWindow: toPositiveInteger(raw.contextWindow, DEFAULT_CONTEXT_WINDOW),
99
- runner: 'sdk',
100
- systemPrompt: normalizeSystemPrompt(raw.systemPrompt),
101
- };
102
- for (const key of ['apiKey', 'authToken', 'anthropicAuthToken']) {
103
- profile[key] = raw[key] ? String(raw[key]) : existing[key] || '';
104
- }
105
- delete profile.hasApiKey;
106
- delete profile.apiKeyMasked;
107
- normalized.push(profile);
108
- }
109
- return { schema: SCHEMA, profiles: normalized };
110
- }
111
-
112
- function validateProfile(profile, id = profile && profile.id) {
113
- if (!id) return { ok: false, error: 'AI profile id is required' };
114
- if (!profile) return { ok: false, error: `AI profile not found: ${id}` };
115
- if (profile.enabled === false) return { ok: false, error: `AI profile is disabled: ${id}` };
116
- if (profileImplementation(profile) !== 'claude-code-agent') {
117
- return { ok: false, error: `unsupported AI implementation: ${profileImplementation(profile)}` };
118
- }
119
- if (!(profile.apiKey || profile.authToken || profile.anthropicAuthToken)) {
120
- return { ok: false, error: `AI profile is missing API key: ${id}` };
121
- }
122
- if (!(profile.mainModel || profile.model)) {
123
- return { ok: false, error: `AI profile is missing model: ${id}` };
124
- }
125
- if (!(profile.baseUrl || profile.apiBaseUrl || profile.anthropicBaseUrl)) {
126
- return { ok: false, error: `AI profile is missing baseUrl: ${id}` };
127
- }
128
- return { ok: true, profile, implementation: profileImplementation(profile) };
129
- }
130
-
131
- function validateProfileConfig(cfg) {
132
- const errors = [];
133
- if (!cfg || typeof cfg !== 'object') return ['config must be an object'];
134
- if (cfg.schema && cfg.schema !== SCHEMA) errors.push(`schema must be ${SCHEMA}`);
135
- if (!Array.isArray(cfg.profiles)) errors.push('profiles must be an array');
136
- const ids = new Set();
137
- for (const profile of cfg.profiles || []) {
138
- const id = profile && profile.id;
139
- if (!id) {
140
- errors.push('profile id is required');
141
- continue;
142
- }
143
- if (ids.has(id)) errors.push(`duplicate profile id: ${id}`);
144
- ids.add(id);
145
- const check = validateProfile(profile, id);
146
- if (!check.ok) errors.push(check.error);
147
- }
148
- return errors;
149
- }
150
-
151
- function publicProfile(profile) {
152
- const secret = profile.apiKey || profile.authToken || profile.anthropicAuthToken || '';
153
- return {
154
- ...profile,
155
- apiKey: '',
156
- authToken: '',
157
- anthropicAuthToken: '',
158
- hasApiKey: !!secret,
159
- apiKeyMasked: maskSecret(secret),
160
- };
161
- }
162
-
163
- class AiProfileStore {
164
- constructor(options = {}) {
165
- this.profilesPath = options.profilesPath || path.join(getDataDir(), 'ai-profiles.json');
166
- this.activeProfileId = options.activeProfileId || null;
167
- this.dropBlankDrafts = options.dropBlankDrafts !== false;
168
- }
169
-
170
- readConfig() {
171
- const cfg = readJsonOrDefault(this.profilesPath, { schema: SCHEMA, profiles: [] });
172
- if (cfg && cfg.schema === SCHEMA && Array.isArray(cfg.profiles)) return cfg;
173
- return { schema: SCHEMA, profiles: [] };
174
- }
175
-
176
- writeConfig(cfg) {
177
- const normalized = normalizeProfilesConfig(
178
- Array.isArray(cfg && cfg.profiles) ? cfg.profiles : [],
179
- this.readConfig(),
180
- cfg && (cfg.activeAiProfileId || cfg.defaultProviderId || this.activeProfileId),
181
- { dropBlankDrafts: this.dropBlankDrafts }
182
- );
183
- const errors = validateProfileConfig(normalized);
184
- if (errors.length) {
185
- const err = new Error(errors[0]);
186
- err.errors = errors;
187
- throw err;
188
- }
189
- writeJson(this.profilesPath, normalized);
190
- return normalized;
191
- }
192
-
193
- publicConfig() {
194
- const cfg = this.readConfig();
195
- const profiles = (cfg.profiles || []).map(publicProfile);
196
- return {
197
- ok: true,
198
- aiProfilesPath: this.profilesPath,
199
- config: { schema: SCHEMA, profiles },
200
- profiles,
201
- activeAiProfileId: this.activeProfileId || (profiles.find(p => p.enabled !== false) || {}).id || null,
202
- };
203
- }
204
-
205
- profileById(id) {
206
- return (this.readConfig().profiles || []).find(profile => profile && profile.id === id) || null;
207
- }
208
-
209
- firstUsableProfileId() {
210
- return ((this.readConfig().profiles || []).find(profile => validateProfile(profile).ok) || {}).id || null;
211
- }
212
-
213
- activeProfile() {
214
- return this.profileById(this.activeProfileId) || this.profileById(this.firstUsableProfileId());
215
- }
216
-
217
- validateProfile(idOrProfile) {
218
- const profile = typeof idOrProfile === 'string' ? this.profileById(idOrProfile) : idOrProfile;
219
- const id = typeof idOrProfile === 'string' ? idOrProfile : profile && profile.id;
220
- return validateProfile(profile, id);
221
- }
222
-
223
- async testProfile({ profileId, profile, prompt = 'what model are you?', maxTokens = 128 } = {}) {
224
- const resolved = profile || this.profileById(profileId);
225
- const id = profileId || resolved && resolved.id;
226
- const check = validateProfile(resolved, id);
227
- if (!check.ok) {
228
- const err = new Error(check.error);
229
- err.status = 400;
230
- throw err;
231
- }
232
- const result = await completeText({
233
- profileId: id,
234
- profile: resolved,
235
- user: prompt,
236
- maxTokens,
237
- });
238
- return {
239
- ok: true,
240
- profileId: id,
241
- model: result.model,
242
- text: result.text,
243
- usage: result.usage,
244
- };
245
- }
246
- }
247
-
248
- module.exports = {
249
- AiProfileStore,
250
- SCHEMA,
251
- DEFAULT_TIMEOUT_MS,
252
- DEFAULT_CONTEXT_WINDOW,
253
- maskSecret,
254
- publicProfile,
255
- profileImplementation,
256
- normalizeProfilesConfig,
257
- validateProfile,
258
- validateProfileConfig,
259
- };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getDataDir } = require('./data-dir');
4
+ const { completeText } = require('./llm-client');
5
+
6
+ const SCHEMA = 'ai-profiles/v1';
7
+ const DEFAULT_TIMEOUT_MS = 300000;
8
+ const DEFAULT_CONTEXT_WINDOW = 200000;
9
+
10
+ function readJsonOrDefault(filePath, fallback) {
11
+ try {
12
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
13
+ } catch {
14
+ return fallback;
15
+ }
16
+ }
17
+
18
+ function writeJson(filePath, value) {
19
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n', 'utf8');
21
+ }
22
+
23
+ function maskSecret(value) {
24
+ if (!value) return '';
25
+ const text = String(value);
26
+ if (text.length <= 8) return '********';
27
+ return `${text.slice(0, 4)}...${text.slice(-4)}`;
28
+ }
29
+
30
+ function toPositiveInteger(value, fallback) {
31
+ const n = Number(value);
32
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
33
+ }
34
+
35
+ function normalizeSystemPrompt(value) {
36
+ if (value && typeof value === 'object') return value;
37
+ if (typeof value === 'string' && value.trim()) {
38
+ try {
39
+ return JSON.parse(value);
40
+ } catch {
41
+ return value;
42
+ }
43
+ }
44
+ return { type: 'preset', preset: 'claude_code' };
45
+ }
46
+
47
+ function isBlankAiProfileDraft(profile) {
48
+ return ![
49
+ profile.provider,
50
+ profile.baseUrl,
51
+ profile.apiBaseUrl,
52
+ profile.anthropicBaseUrl,
53
+ profile.apiKey,
54
+ profile.authToken,
55
+ profile.anthropicAuthToken,
56
+ profile.mainModel,
57
+ profile.model,
58
+ profile.website,
59
+ profile.notes,
60
+ ].some(value => String(value || '').trim());
61
+ }
62
+
63
+ function profileImplementation(profile) {
64
+ return profile && (profile.implementation || profile.adapter || 'claude-code-agent') || 'claude-code-agent';
65
+ }
66
+
67
+ function normalizeProfilesConfig(profiles, existingCfg = { profiles: [] }, activeAiProfileId = null, options = {}) {
68
+ const existingById = new Map((existingCfg.profiles || []).filter(p => p && p.id).map(p => [p.id, p]));
69
+ const seen = new Set();
70
+ const normalized = [];
71
+ for (const raw of profiles || []) {
72
+ if (!raw || typeof raw !== 'object') continue;
73
+ const id = String(raw.id || '').trim();
74
+ if (id && id !== activeAiProfileId && options.dropBlankDrafts !== false && isBlankAiProfileDraft(raw)) continue;
75
+ if (!id) throw new Error('AI profile is missing id');
76
+ if (seen.has(id)) throw new Error(`duplicate AI profile id: ${id}`);
77
+ seen.add(id);
78
+ const existing = existingById.get(id) || {};
79
+ const provider = String(raw.provider || raw.name || existing.provider || existing.name || id).trim() || id;
80
+ const profile = {
81
+ ...existing,
82
+ ...raw,
83
+ id,
84
+ name: String(raw.name || raw.provider || existing.name || provider || id).trim() || id,
85
+ provider,
86
+ enabled: raw.enabled !== false,
87
+ implementation: 'claude-code-agent',
88
+ baseUrl: String(raw.baseUrl || raw.apiBaseUrl || raw.anthropicBaseUrl || '').trim(),
89
+ notes: String(raw.notes || '').trim(),
90
+ website: String(raw.website || '').trim(),
91
+ mainModel: String(raw.mainModel || raw.model || '').trim(),
92
+ thinkingModel: String(raw.thinkingModel || raw.mainModel || raw.model || '').trim(),
93
+ haikuModel: String(raw.haikuModel || raw.mainModel || raw.model || '').trim(),
94
+ sonnetModel: String(raw.sonnetModel || raw.mainModel || raw.model || '').trim(),
95
+ opusModel: String(raw.opusModel || raw.mainModel || raw.model || '').trim(),
96
+ model: String(raw.model || raw.mainModel || '').trim(),
97
+ timeoutMs: toPositiveInteger(raw.timeoutMs, DEFAULT_TIMEOUT_MS),
98
+ contextWindow: toPositiveInteger(raw.contextWindow, DEFAULT_CONTEXT_WINDOW),
99
+ runner: 'sdk',
100
+ systemPrompt: normalizeSystemPrompt(raw.systemPrompt),
101
+ };
102
+ for (const key of ['apiKey', 'authToken', 'anthropicAuthToken']) {
103
+ profile[key] = raw[key] ? String(raw[key]) : existing[key] || '';
104
+ }
105
+ delete profile.hasApiKey;
106
+ delete profile.apiKeyMasked;
107
+ normalized.push(profile);
108
+ }
109
+ return { schema: SCHEMA, profiles: normalized };
110
+ }
111
+
112
+ function validateProfile(profile, id = profile && profile.id) {
113
+ if (!id) return { ok: false, error: 'AI profile id is required' };
114
+ if (!profile) return { ok: false, error: `AI profile not found: ${id}` };
115
+ if (profile.enabled === false) return { ok: false, error: `AI profile is disabled: ${id}` };
116
+ if (profileImplementation(profile) !== 'claude-code-agent') {
117
+ return { ok: false, error: `unsupported AI implementation: ${profileImplementation(profile)}` };
118
+ }
119
+ if (!(profile.apiKey || profile.authToken || profile.anthropicAuthToken)) {
120
+ return { ok: false, error: `AI profile is missing API key: ${id}` };
121
+ }
122
+ if (!(profile.mainModel || profile.model)) {
123
+ return { ok: false, error: `AI profile is missing model: ${id}` };
124
+ }
125
+ if (!(profile.baseUrl || profile.apiBaseUrl || profile.anthropicBaseUrl)) {
126
+ return { ok: false, error: `AI profile is missing baseUrl: ${id}` };
127
+ }
128
+ return { ok: true, profile, implementation: profileImplementation(profile) };
129
+ }
130
+
131
+ function validateProfileConfig(cfg) {
132
+ const errors = [];
133
+ if (!cfg || typeof cfg !== 'object') return ['config must be an object'];
134
+ if (cfg.schema && cfg.schema !== SCHEMA) errors.push(`schema must be ${SCHEMA}`);
135
+ if (!Array.isArray(cfg.profiles)) errors.push('profiles must be an array');
136
+ const ids = new Set();
137
+ for (const profile of cfg.profiles || []) {
138
+ const id = profile && profile.id;
139
+ if (!id) {
140
+ errors.push('profile id is required');
141
+ continue;
142
+ }
143
+ if (ids.has(id)) errors.push(`duplicate profile id: ${id}`);
144
+ ids.add(id);
145
+ const check = validateProfile(profile, id);
146
+ if (!check.ok) errors.push(check.error);
147
+ }
148
+ return errors;
149
+ }
150
+
151
+ function publicProfile(profile) {
152
+ const secret = profile.apiKey || profile.authToken || profile.anthropicAuthToken || '';
153
+ return {
154
+ ...profile,
155
+ apiKey: '',
156
+ authToken: '',
157
+ anthropicAuthToken: '',
158
+ hasApiKey: !!secret,
159
+ apiKeyMasked: maskSecret(secret),
160
+ };
161
+ }
162
+
163
+ class AiProfileStore {
164
+ constructor(options = {}) {
165
+ this.profilesPath = options.profilesPath || path.join(getDataDir(), 'ai-profiles.json');
166
+ this.activeProfileId = options.activeProfileId || null;
167
+ this.dropBlankDrafts = options.dropBlankDrafts !== false;
168
+ }
169
+
170
+ readConfig() {
171
+ const cfg = readJsonOrDefault(this.profilesPath, { schema: SCHEMA, profiles: [] });
172
+ if (cfg && cfg.schema === SCHEMA && Array.isArray(cfg.profiles)) return cfg;
173
+ return { schema: SCHEMA, profiles: [] };
174
+ }
175
+
176
+ writeConfig(cfg) {
177
+ const normalized = normalizeProfilesConfig(
178
+ Array.isArray(cfg && cfg.profiles) ? cfg.profiles : [],
179
+ this.readConfig(),
180
+ cfg && (cfg.activeAiProfileId || cfg.defaultProviderId || this.activeProfileId),
181
+ { dropBlankDrafts: this.dropBlankDrafts }
182
+ );
183
+ const errors = validateProfileConfig(normalized);
184
+ if (errors.length) {
185
+ const err = new Error(errors[0]);
186
+ err.errors = errors;
187
+ throw err;
188
+ }
189
+ writeJson(this.profilesPath, normalized);
190
+ return normalized;
191
+ }
192
+
193
+ publicConfig() {
194
+ const cfg = this.readConfig();
195
+ const profiles = (cfg.profiles || []).map(publicProfile);
196
+ return {
197
+ ok: true,
198
+ aiProfilesPath: this.profilesPath,
199
+ config: { schema: SCHEMA, profiles },
200
+ profiles,
201
+ activeAiProfileId: this.activeProfileId || (profiles.find(p => p.enabled !== false) || {}).id || null,
202
+ };
203
+ }
204
+
205
+ profileById(id) {
206
+ return (this.readConfig().profiles || []).find(profile => profile && profile.id === id) || null;
207
+ }
208
+
209
+ firstUsableProfileId() {
210
+ return ((this.readConfig().profiles || []).find(profile => validateProfile(profile).ok) || {}).id || null;
211
+ }
212
+
213
+ activeProfile() {
214
+ return this.profileById(this.activeProfileId) || this.profileById(this.firstUsableProfileId());
215
+ }
216
+
217
+ validateProfile(idOrProfile) {
218
+ const profile = typeof idOrProfile === 'string' ? this.profileById(idOrProfile) : idOrProfile;
219
+ const id = typeof idOrProfile === 'string' ? idOrProfile : profile && profile.id;
220
+ return validateProfile(profile, id);
221
+ }
222
+
223
+ async testProfile({ profileId, profile, prompt = 'what model are you?', maxTokens = 128 } = {}) {
224
+ const resolved = profile || this.profileById(profileId);
225
+ const id = profileId || resolved && resolved.id;
226
+ const check = validateProfile(resolved, id);
227
+ if (!check.ok) {
228
+ const err = new Error(check.error);
229
+ err.status = 400;
230
+ throw err;
231
+ }
232
+ const result = await completeText({
233
+ profileId: id,
234
+ profile: resolved,
235
+ user: prompt,
236
+ maxTokens,
237
+ });
238
+ return {
239
+ ok: true,
240
+ profileId: id,
241
+ model: result.model,
242
+ text: result.text,
243
+ usage: result.usage,
244
+ };
245
+ }
246
+ }
247
+
248
+ module.exports = {
249
+ AiProfileStore,
250
+ SCHEMA,
251
+ DEFAULT_TIMEOUT_MS,
252
+ DEFAULT_CONTEXT_WINDOW,
253
+ maskSecret,
254
+ publicProfile,
255
+ profileImplementation,
256
+ normalizeProfilesConfig,
257
+ validateProfile,
258
+ validateProfileConfig,
259
+ };