@mcoda/core 0.1.37 → 0.1.40

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 (29) hide show
  1. package/dist/api/MswarmApi.d.ts +155 -0
  2. package/dist/api/MswarmApi.d.ts.map +1 -0
  3. package/dist/api/MswarmApi.js +593 -0
  4. package/dist/api/MswarmConfigStore.d.ts +53 -0
  5. package/dist/api/MswarmConfigStore.d.ts.map +1 -0
  6. package/dist/api/MswarmConfigStore.js +111 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -0
  10. package/dist/services/docs/DocsService.d.ts.map +1 -1
  11. package/dist/services/docs/DocsService.js +1 -11
  12. package/dist/services/estimate/VelocityService.d.ts.map +1 -1
  13. package/dist/services/estimate/VelocityService.js +1 -2
  14. package/dist/services/execution/AddTestsService.d.ts.map +1 -1
  15. package/dist/services/execution/AddTestsService.js +2 -2
  16. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  17. package/dist/services/execution/QaTasksService.js +3 -2
  18. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  19. package/dist/services/execution/WorkOnTasksService.js +2 -6
  20. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  21. package/dist/services/openapi/OpenApiService.js +1 -11
  22. package/dist/services/planning/CreateTasksService.d.ts +9 -0
  23. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  24. package/dist/services/planning/CreateTasksService.js +490 -209
  25. package/dist/services/review/CodeReviewService.js +2 -2
  26. package/dist/services/shared/GitBranch.d.ts +6 -0
  27. package/dist/services/shared/GitBranch.d.ts.map +1 -0
  28. package/dist/services/shared/GitBranch.js +62 -0
  29. package/package.json +6 -6
@@ -0,0 +1,593 @@
1
+ import { GlobalRepository } from '@mcoda/db';
2
+ import { CryptoHelper, } from '@mcoda/shared';
3
+ import { MswarmConfigStore } from './MswarmConfigStore.js';
4
+ const DEFAULT_BASE_URL = 'https://api.mswarm.org/';
5
+ const DEFAULT_TIMEOUT_MS = 15000;
6
+ const DEFAULT_AGENT_SLUG_PREFIX = 'mswarm-cloud';
7
+ export const MSWARM_CONSENT_POLICY_VERSION = '2026-03-18';
8
+ export const MCODA_FREE_CLIENT_TYPE = 'free_mcoda_client';
9
+ const MCODA_PRODUCT_SLUG = 'mcoda';
10
+ const MCODA_CONSENT_TYPES = ['anonymous', 'non_anonymous'];
11
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ const resolveString = (value) => typeof value === 'string' && value.trim() ? value : undefined;
13
+ const resolveNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
14
+ const resolveBoolean = (value) => typeof value === 'boolean' ? value : undefined;
15
+ const resolveTimestamp = (value) => {
16
+ const candidate = resolveString(value);
17
+ if (!candidate)
18
+ return undefined;
19
+ return Number.isNaN(Date.parse(candidate)) ? undefined : candidate;
20
+ };
21
+ const resolveStringArray = (value) => {
22
+ if (!Array.isArray(value))
23
+ return [];
24
+ return value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
25
+ };
26
+ const normalizeBaseUrl = (value, label) => {
27
+ const trimmed = value?.trim();
28
+ if (!trimmed) {
29
+ throw new Error(`${label} is required`);
30
+ }
31
+ let parsed;
32
+ try {
33
+ parsed = new URL(trimmed);
34
+ }
35
+ catch {
36
+ throw new Error(`${label} must be an absolute URL`);
37
+ }
38
+ return parsed.toString();
39
+ };
40
+ const normalizePositiveInt = (value, label, fallback) => {
41
+ if (value === undefined)
42
+ return fallback;
43
+ if (!Number.isFinite(value) || value <= 0) {
44
+ throw new Error(`${label} must be a positive integer`);
45
+ }
46
+ return Math.trunc(value);
47
+ };
48
+ const normalizeOptionalPositiveInt = (value, label) => {
49
+ if (value === undefined)
50
+ return undefined;
51
+ if (!Number.isFinite(value) || value <= 0) {
52
+ throw new Error(`${label} must be a positive integer`);
53
+ }
54
+ return Math.trunc(value);
55
+ };
56
+ const normalizeOptionalNonNegativeNumber = (value, label) => {
57
+ if (value === undefined)
58
+ return undefined;
59
+ if (!Number.isFinite(value) || value < 0) {
60
+ throw new Error(`${label} must be a non-negative number`);
61
+ }
62
+ return value;
63
+ };
64
+ const resolveOptions = async (options = {}) => {
65
+ const envTimeoutRaw = process.env.MCODA_MSWARM_TIMEOUT_MS;
66
+ const envTimeout = envTimeoutRaw
67
+ ? Number.parseInt(envTimeoutRaw, 10)
68
+ : undefined;
69
+ const directBaseUrl = options.baseUrl ?? process.env.MCODA_MSWARM_BASE_URL;
70
+ const directOpenAiBaseUrl = options.openAiBaseUrl ?? process.env.MCODA_MSWARM_OPENAI_BASE_URL;
71
+ const directApiKey = options.apiKey ?? process.env.MCODA_MSWARM_API_KEY;
72
+ const directTimeout = options.timeoutMs ?? envTimeout;
73
+ const directAgentSlugPrefix = options.agentSlugPrefix ?? process.env.MCODA_MSWARM_AGENT_SLUG_PREFIX;
74
+ const needsStoredFallback = directBaseUrl === undefined ||
75
+ directApiKey === undefined ||
76
+ directTimeout === undefined ||
77
+ directAgentSlugPrefix === undefined;
78
+ const stored = needsStoredFallback
79
+ ? await new MswarmConfigStore().readState()
80
+ : {};
81
+ return {
82
+ baseUrl: normalizeBaseUrl(directBaseUrl ?? stored.baseUrl ?? DEFAULT_BASE_URL, 'MCODA_MSWARM_BASE_URL'),
83
+ openAiBaseUrl: directOpenAiBaseUrl
84
+ ? normalizeBaseUrl(directOpenAiBaseUrl, 'MCODA_MSWARM_OPENAI_BASE_URL')
85
+ : undefined,
86
+ apiKey: resolveString(directApiKey ?? stored.apiKey),
87
+ timeoutMs: normalizePositiveInt(directTimeout ?? stored.timeoutMs, 'MCODA_MSWARM_TIMEOUT_MS', DEFAULT_TIMEOUT_MS),
88
+ agentSlugPrefix: resolveString(directAgentSlugPrefix ?? stored.agentSlugPrefix) ??
89
+ DEFAULT_AGENT_SLUG_PREFIX,
90
+ };
91
+ };
92
+ const uniqueStrings = (values) => Array.from(new Set(values.filter((value) => value.trim().length > 0)));
93
+ const resolveFromRecordOrShape = (record, keys, parser) => {
94
+ const sources = [
95
+ record,
96
+ isRecord(record.mcoda_shape) ? record.mcoda_shape : undefined,
97
+ ].filter(isRecord);
98
+ for (const source of sources) {
99
+ for (const key of keys) {
100
+ const resolved = parser(source[key]);
101
+ if (resolved !== undefined)
102
+ return resolved;
103
+ }
104
+ }
105
+ return undefined;
106
+ };
107
+ const resolveStringArrayFromRecordOrShape = (record, keys) => {
108
+ const sources = [
109
+ record,
110
+ isRecord(record.mcoda_shape) ? record.mcoda_shape : undefined,
111
+ ].filter(isRecord);
112
+ const values = sources.flatMap((source) => keys.flatMap((key) => resolveStringArray(source[key])));
113
+ return uniqueStrings(values);
114
+ };
115
+ const hasCapabilityFragment = (capabilities, fragments) => capabilities.some((capability) => fragments.some((fragment) => capability.includes(fragment)));
116
+ const inferCloudBestUsage = (agent) => {
117
+ const capabilities = agent.capabilities.map((capability) => capability.trim().toLowerCase());
118
+ const model = agent.default_model.trim().toLowerCase();
119
+ if (hasCapabilityFragment(capabilities, ['code_review', 'review']))
120
+ return 'code_review';
121
+ if (hasCapabilityFragment(capabilities, ['qa', 'test']))
122
+ return 'qa_testing';
123
+ if (hasCapabilityFragment(capabilities, ['research', 'search', 'discover']))
124
+ return 'deep_research';
125
+ if (hasCapabilityFragment(capabilities, [
126
+ 'code_write',
127
+ 'coding',
128
+ 'tool_runner',
129
+ 'iterative_coding',
130
+ 'structured_output',
131
+ ]) ||
132
+ model.includes('codex')) {
133
+ return 'code_write';
134
+ }
135
+ if (hasCapabilityFragment(capabilities, ['architect', 'plan']))
136
+ return 'system_architecture';
137
+ if (hasCapabilityFragment(capabilities, ['doc']))
138
+ return 'doc_generation';
139
+ return 'general';
140
+ };
141
+ const DEFAULT_CONTEXT_WINDOW = 8192;
142
+ const DEFAULT_MAX_OUTPUT_TOKENS = 2048;
143
+ const DEFAULT_MAX_COMPLEXITY = 5;
144
+ const toSyncedAgentInput = (existing, agent, localSlug, config, syncedAt) => {
145
+ const rating = existing?.rating ?? agent.rating;
146
+ const reasoningRating = existing?.reasoningRating ?? agent.reasoning_rating ?? rating;
147
+ const maxComplexity = existing?.maxComplexity ?? agent.max_complexity ?? DEFAULT_MAX_COMPLEXITY;
148
+ const ratingSamples = existing?.ratingSamples ?? agent.rating_samples ?? 0;
149
+ const ratingLastScore = existing?.ratingLastScore ?? agent.rating_last_score ?? rating;
150
+ const ratingUpdatedAt = existing?.ratingUpdatedAt ?? agent.rating_updated_at ?? syncedAt;
151
+ const complexitySamples = existing?.complexitySamples ?? agent.complexity_samples ?? 0;
152
+ const complexityUpdatedAt = existing?.complexityUpdatedAt ?? agent.complexity_updated_at ?? syncedAt;
153
+ return {
154
+ slug: localSlug,
155
+ adapter: 'openai-api',
156
+ defaultModel: agent.default_model,
157
+ openaiCompatible: true,
158
+ contextWindow: agent.context_window ?? existing?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
159
+ maxOutputTokens: agent.max_output_tokens ??
160
+ existing?.maxOutputTokens ??
161
+ DEFAULT_MAX_OUTPUT_TOKENS,
162
+ supportsTools: agent.supports_tools,
163
+ rating,
164
+ reasoningRating,
165
+ bestUsage: agent.best_usage ?? existing?.bestUsage ?? inferCloudBestUsage(agent),
166
+ costPerMillion: agent.cost_per_million ?? existing?.costPerMillion,
167
+ maxComplexity,
168
+ ratingSamples,
169
+ ratingLastScore,
170
+ ratingUpdatedAt,
171
+ complexitySamples,
172
+ complexityUpdatedAt,
173
+ config,
174
+ capabilities: uniqueStrings(agent.capabilities),
175
+ };
176
+ };
177
+ const toManagedLocalSlug = (prefix, remoteSlug) => {
178
+ const normalized = remoteSlug
179
+ .trim()
180
+ .toLowerCase()
181
+ .replace(/[^a-z0-9]+/g, '-')
182
+ .replace(/^-+|-+$/g, '');
183
+ return `${prefix}-${normalized || 'agent'}`;
184
+ };
185
+ const toHealthStatus = (value) => {
186
+ const normalized = value?.trim().toLowerCase();
187
+ if (!normalized)
188
+ return undefined;
189
+ if (normalized === 'healthy')
190
+ return 'healthy';
191
+ if (normalized === 'degraded' ||
192
+ normalized === 'unknown' ||
193
+ normalized === 'limited')
194
+ return 'degraded';
195
+ if (normalized === 'unreachable' || normalized === 'offline')
196
+ return 'unreachable';
197
+ return undefined;
198
+ };
199
+ const isSyncManagedHealth = (health) => isRecord(health?.details) &&
200
+ (health.details.source === 'mswarm' ||
201
+ health.details.source === 'mswarm_catalog');
202
+ const isAuthMissingManagedHealth = (health) => {
203
+ if (!isRecord(health?.details))
204
+ return false;
205
+ const reason = resolveString(health.details.reason);
206
+ const error = resolveString(health.details.error) ?? '';
207
+ return (reason === 'missing_api_key' ||
208
+ /AUTH_REQUIRED/i.test(error) ||
209
+ /missing the synced API key/i.test(error));
210
+ };
211
+ const shouldReplaceManagedHealth = (health) => !health || isSyncManagedHealth(health) || isAuthMissingManagedHealth(health);
212
+ const isManagedMswarmConfig = (config) => {
213
+ if (!isRecord(config))
214
+ return false;
215
+ if (!isRecord(config.mswarmCloud))
216
+ return false;
217
+ return config.mswarmCloud.managed === true;
218
+ };
219
+ const toManagedConfig = (existingConfig, catalogBaseUrl, openAiBaseUrl, agent, syncedAt) => {
220
+ const nextConfig = {
221
+ ...(existingConfig ?? {}),
222
+ baseUrl: openAiBaseUrl,
223
+ apiBaseUrl: openAiBaseUrl,
224
+ mswarmCloud: {
225
+ managed: true,
226
+ remoteSlug: agent.slug,
227
+ provider: agent.provider,
228
+ modelId: agent.model_id,
229
+ displayName: agent.display_name,
230
+ description: agent.description,
231
+ supportsReasoning: agent.supports_reasoning,
232
+ pricingSnapshotId: agent.pricing_snapshot_id,
233
+ pricingVersion: agent.pricing_version,
234
+ catalogBaseUrl,
235
+ openAiBaseUrl,
236
+ sync: isRecord(agent.sync) ? agent.sync : undefined,
237
+ syncedAt,
238
+ },
239
+ };
240
+ return nextConfig;
241
+ };
242
+ const toManagedSyncRecord = (config, localSlug, defaultModel, action) => ({
243
+ remoteSlug: config.mswarmCloud.remoteSlug,
244
+ localSlug,
245
+ action,
246
+ provider: config.mswarmCloud.provider,
247
+ defaultModel,
248
+ pricingVersion: config.mswarmCloud.pricingVersion,
249
+ });
250
+ const toCloudAgent = (value) => {
251
+ if (!isRecord(value)) {
252
+ throw new Error('mswarm returned an invalid cloud-agent payload');
253
+ }
254
+ const slug = resolveFromRecordOrShape(value, ['slug'], resolveString);
255
+ const provider = resolveFromRecordOrShape(value, ['provider'], resolveString);
256
+ const defaultModel = resolveFromRecordOrShape(value, ['default_model', 'defaultModel'], resolveString);
257
+ const supportsTools = resolveFromRecordOrShape(value, ['supports_tools', 'supportsTools'], resolveBoolean);
258
+ if (!slug || !provider || !defaultModel || supportsTools === undefined) {
259
+ throw new Error('mswarm cloud-agent payload is missing required fields');
260
+ }
261
+ return {
262
+ slug,
263
+ provider,
264
+ default_model: defaultModel,
265
+ cost_per_million: resolveFromRecordOrShape(value, ['cost_per_million', 'costPerMillion'], resolveNumber),
266
+ rating: resolveFromRecordOrShape(value, ['rating'], resolveNumber),
267
+ reasoning_rating: resolveFromRecordOrShape(value, ['reasoning_rating', 'reasoningRating'], resolveNumber),
268
+ max_complexity: resolveFromRecordOrShape(value, ['max_complexity', 'maxComplexity'], resolveNumber),
269
+ capabilities: resolveStringArrayFromRecordOrShape(value, ['capabilities']),
270
+ health_status: resolveFromRecordOrShape(value, ['health_status', 'healthStatus'], resolveString),
271
+ context_window: resolveFromRecordOrShape(value, ['context_window', 'contextWindow'], resolveNumber),
272
+ max_output_tokens: resolveFromRecordOrShape(value, ['max_output_tokens', 'maxOutputTokens'], resolveNumber),
273
+ supports_tools: supportsTools,
274
+ best_usage: resolveFromRecordOrShape(value, ['best_usage', 'bestUsage'], resolveString),
275
+ model_id: resolveFromRecordOrShape(value, ['model_id', 'modelId'], resolveString),
276
+ display_name: resolveFromRecordOrShape(value, ['display_name', 'displayName'], resolveString),
277
+ description: resolveFromRecordOrShape(value, ['description'], resolveString),
278
+ supports_reasoning: resolveFromRecordOrShape(value, ['supports_reasoning', 'supportsReasoning'], resolveBoolean),
279
+ pricing_snapshot_id: resolveFromRecordOrShape(value, ['pricing_snapshot_id', 'pricingSnapshotId'], resolveString),
280
+ pricing_version: resolveFromRecordOrShape(value, ['pricing_version', 'pricingVersion'], resolveString),
281
+ rating_samples: resolveFromRecordOrShape(value, ['rating_samples', 'ratingSamples'], resolveNumber),
282
+ rating_last_score: resolveFromRecordOrShape(value, ['rating_last_score', 'ratingLastScore'], resolveNumber),
283
+ rating_updated_at: resolveFromRecordOrShape(value, ['rating_updated_at', 'ratingUpdatedAt'], resolveTimestamp),
284
+ complexity_samples: resolveFromRecordOrShape(value, ['complexity_samples', 'complexitySamples'], resolveNumber),
285
+ complexity_updated_at: resolveFromRecordOrShape(value, ['complexity_updated_at', 'complexityUpdatedAt'], resolveTimestamp),
286
+ sync: isRecord(value.sync) ? value.sync : undefined,
287
+ };
288
+ };
289
+ const toCloudAgentDetail = (value) => {
290
+ const agent = toCloudAgent(value);
291
+ const record = isRecord(value) ? value : {};
292
+ return {
293
+ ...agent,
294
+ pricing: isRecord(record.pricing) ? record.pricing : undefined,
295
+ supported_parameters: resolveStringArray(record.supported_parameters),
296
+ status: resolveString(record.status),
297
+ moderation_status: resolveString(record.moderation_status),
298
+ mcoda_shape: isRecord(record.mcoda_shape) ? record.mcoda_shape : undefined,
299
+ };
300
+ };
301
+ const hasAdvancedCloudAgentSelection = (options) => options.maxCostPerMillion !== undefined ||
302
+ options.minContextWindow !== undefined ||
303
+ options.minReasoningRating !== undefined ||
304
+ options.sortByCatalogRating === true;
305
+ const sortCloudAgentsByCatalogRating = (agents) => [...agents].sort((left, right) => {
306
+ const ratingDelta = (right.rating ?? Number.NEGATIVE_INFINITY) -
307
+ (left.rating ?? Number.NEGATIVE_INFINITY);
308
+ if (ratingDelta !== 0)
309
+ return ratingDelta;
310
+ return left.slug.localeCompare(right.slug);
311
+ });
312
+ const applyCloudAgentListOptions = (agents, options) => {
313
+ const maxCostPerMillion = normalizeOptionalNonNegativeNumber(options.maxCostPerMillion, 'maxCostPerMillion');
314
+ const minContextWindow = normalizeOptionalPositiveInt(options.minContextWindow, 'minContextWindow');
315
+ const minReasoningRating = normalizeOptionalNonNegativeNumber(options.minReasoningRating, 'minReasoningRating');
316
+ const limit = normalizeOptionalPositiveInt(options.limit, 'limit');
317
+ let next = [...agents];
318
+ if (maxCostPerMillion !== undefined) {
319
+ next = next.filter((agent) => agent.cost_per_million !== undefined &&
320
+ agent.cost_per_million <= maxCostPerMillion);
321
+ }
322
+ if (minContextWindow !== undefined) {
323
+ next = next.filter((agent) => agent.context_window !== undefined &&
324
+ agent.context_window >= minContextWindow);
325
+ }
326
+ if (minReasoningRating !== undefined) {
327
+ next = next.filter((agent) => agent.reasoning_rating !== undefined &&
328
+ agent.reasoning_rating >= minReasoningRating);
329
+ }
330
+ if (options.sortByCatalogRating) {
331
+ next = sortCloudAgentsByCatalogRating(next);
332
+ }
333
+ if (limit !== undefined) {
334
+ next = next.slice(0, limit);
335
+ }
336
+ return next;
337
+ };
338
+ const toAgentModels = (agentId, entry) => [
339
+ {
340
+ agentId,
341
+ modelName: entry.default_model,
342
+ isDefault: true,
343
+ config: {
344
+ provider: entry.provider,
345
+ remoteSlug: entry.slug,
346
+ modelId: entry.model_id,
347
+ pricingVersion: entry.pricing_version,
348
+ },
349
+ },
350
+ ];
351
+ export class MswarmApi {
352
+ constructor(repo, options) {
353
+ this.repo = repo;
354
+ this.options = options;
355
+ this.baseUrl = options.baseUrl;
356
+ this.agentSlugPrefix = options.agentSlugPrefix;
357
+ }
358
+ static async create(options = {}) {
359
+ const repo = await GlobalRepository.create();
360
+ return new MswarmApi(repo, await resolveOptions(options));
361
+ }
362
+ static async refreshManagedAgentAuth(apiKey) {
363
+ const trimmed = apiKey.trim();
364
+ if (!trimmed) {
365
+ throw new Error('mswarm api key is required');
366
+ }
367
+ const repo = await GlobalRepository.create();
368
+ try {
369
+ const encryptedApiKey = await CryptoHelper.encryptSecret(trimmed);
370
+ const agents = await repo.listAgents();
371
+ const managedAgents = agents.filter((agent) => isManagedMswarmConfig(agent.config));
372
+ for (const agent of managedAgents) {
373
+ await repo.setAgentAuth(agent.id, encryptedApiKey);
374
+ }
375
+ return {
376
+ updated: managedAgents.length,
377
+ agents: managedAgents.map((agent) => agent.slug),
378
+ };
379
+ }
380
+ finally {
381
+ await repo.close();
382
+ }
383
+ }
384
+ async close() {
385
+ await this.repo.close();
386
+ }
387
+ async refreshManagedAgentAuth() {
388
+ return MswarmApi.refreshManagedAgentAuth(this.requireApiKey());
389
+ }
390
+ requireApiKey() {
391
+ if (!this.options.apiKey) {
392
+ throw new Error('MCODA_MSWARM_API_KEY is required');
393
+ }
394
+ return this.options.apiKey;
395
+ }
396
+ async requestJson(pathname, query, init) {
397
+ const url = new URL(pathname, this.options.baseUrl);
398
+ if (query) {
399
+ for (const [key, value] of Object.entries(query)) {
400
+ if (value === undefined)
401
+ continue;
402
+ url.searchParams.set(key, String(value));
403
+ }
404
+ }
405
+ const controller = new AbortController();
406
+ const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs);
407
+ try {
408
+ const headers = {
409
+ accept: 'application/json',
410
+ ...(init?.headers ?? {}),
411
+ };
412
+ if (this.options.apiKey) {
413
+ headers['x-api-key'] = this.options.apiKey;
414
+ }
415
+ let body;
416
+ if (init?.body !== undefined) {
417
+ headers['content-type'] = 'application/json';
418
+ body = JSON.stringify(init.body);
419
+ }
420
+ const response = await fetch(url.toString(), {
421
+ method: init?.method ?? 'GET',
422
+ headers,
423
+ body,
424
+ signal: controller.signal,
425
+ });
426
+ if (!response.ok) {
427
+ const body = await response.text().catch(() => '');
428
+ throw new Error(`mswarm request failed (${response.status}): ${body || response.statusText}`);
429
+ }
430
+ try {
431
+ return (await response.json());
432
+ }
433
+ catch (error) {
434
+ throw new Error(`mswarm response was not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
435
+ }
436
+ }
437
+ catch (error) {
438
+ if (error instanceof Error && error.name === 'AbortError') {
439
+ throw new Error(`mswarm request timed out after ${this.options.timeoutMs}ms`);
440
+ }
441
+ throw error;
442
+ }
443
+ finally {
444
+ clearTimeout(timeout);
445
+ }
446
+ }
447
+ async listCloudAgents(options = {}) {
448
+ const remoteLimit = hasAdvancedCloudAgentSelection(options)
449
+ ? undefined
450
+ : options.limit;
451
+ const payload = await this.requestJson('/v1/swarm/cloud/agents', {
452
+ shape: 'mcoda',
453
+ provider: options.provider,
454
+ limit: remoteLimit,
455
+ });
456
+ const agents = Array.isArray(payload.agents) ? payload.agents : [];
457
+ return applyCloudAgentListOptions(agents.map(toCloudAgent), options);
458
+ }
459
+ async getCloudAgent(slug) {
460
+ if (!slug.trim()) {
461
+ throw new Error('Cloud-agent slug is required');
462
+ }
463
+ const payload = await this.requestJson(`/v1/swarm/cloud/agents/${encodeURIComponent(slug)}`);
464
+ return toCloudAgentDetail(payload);
465
+ }
466
+ async syncCloudAgents(options = {}) {
467
+ if (options.pruneMissing &&
468
+ (options.limit !== undefined || hasAdvancedCloudAgentSelection(options))) {
469
+ throw new Error('pruneMissing cannot be combined with limit or advanced cloud-agent filters');
470
+ }
471
+ const agents = await this.listCloudAgents(options);
472
+ const openAiBaseUrl = this.options.openAiBaseUrl ??
473
+ new URL('/v1/swarm/openai/', this.options.baseUrl).toString();
474
+ const syncedAt = new Date().toISOString();
475
+ const encryptedApiKey = await CryptoHelper.encryptSecret(this.requireApiKey());
476
+ const records = [];
477
+ for (const agent of agents) {
478
+ const localSlug = toManagedLocalSlug(this.options.agentSlugPrefix, agent.slug);
479
+ const existing = await this.repo.getAgentBySlug(localSlug);
480
+ if (existing &&
481
+ (!isManagedMswarmConfig(existing.config) ||
482
+ existing.config.mswarmCloud.remoteSlug !== agent.slug)) {
483
+ throw new Error(`Refusing to overwrite non-mswarm agent ${localSlug}`);
484
+ }
485
+ const existingConfig = existing && isRecord(existing.config)
486
+ ? existing.config
487
+ : undefined;
488
+ const nextConfig = toManagedConfig(existingConfig, this.options.baseUrl, openAiBaseUrl, agent, syncedAt);
489
+ const createInput = toSyncedAgentInput(existing, agent, localSlug, nextConfig, syncedAt);
490
+ const { slug: _ignoredSlug, ...updateInput } = createInput;
491
+ const stored = existing
492
+ ? await this.repo.updateAgent(existing.id, updateInput)
493
+ : await this.repo.createAgent(createInput);
494
+ if (!stored) {
495
+ throw new Error(`Failed to persist synced agent ${localSlug}`);
496
+ }
497
+ await this.repo.setAgentModels(stored.id, toAgentModels(stored.id, agent));
498
+ await this.repo.setAgentAuth(stored.id, encryptedApiKey);
499
+ const existingHealth = existing
500
+ ? await this.repo.getAgentHealth(existing.id)
501
+ : undefined;
502
+ const mappedHealth = toHealthStatus(agent.health_status);
503
+ if (mappedHealth && shouldReplaceManagedHealth(existingHealth)) {
504
+ const health = {
505
+ agentId: stored.id,
506
+ status: mappedHealth,
507
+ lastCheckedAt: syncedAt,
508
+ details: {
509
+ source: 'mswarm',
510
+ remoteSlug: agent.slug,
511
+ remoteHealthStatus: agent.health_status,
512
+ },
513
+ };
514
+ await this.repo.setAgentHealth(health);
515
+ }
516
+ records.push(toManagedSyncRecord(nextConfig, localSlug, agent.default_model, existing ? 'updated' : 'created'));
517
+ }
518
+ if (options.pruneMissing) {
519
+ const remoteSlugs = new Set(agents.map((agent) => agent.slug));
520
+ const localAgents = await this.repo.listAgents();
521
+ for (const localAgent of localAgents) {
522
+ const managedConfig = isManagedMswarmConfig(localAgent.config)
523
+ ? localAgent.config
524
+ : undefined;
525
+ if (!managedConfig)
526
+ continue;
527
+ if (options.provider &&
528
+ managedConfig.mswarmCloud.provider !== options.provider) {
529
+ continue;
530
+ }
531
+ if (remoteSlugs.has(managedConfig.mswarmCloud.remoteSlug))
532
+ continue;
533
+ await this.repo.deleteAgent(localAgent.id);
534
+ records.push(toManagedSyncRecord(managedConfig, localAgent.slug, localAgent.defaultModel ?? managedConfig.mswarmCloud.modelId ?? '-', 'deleted'));
535
+ }
536
+ }
537
+ return {
538
+ created: records.filter((record) => record.action === 'created').length,
539
+ updated: records.filter((record) => record.action === 'updated').length,
540
+ deleted: records.filter((record) => record.action === 'deleted').length,
541
+ agents: records,
542
+ };
543
+ }
544
+ async issuePaidConsent(policyVersion = MSWARM_CONSENT_POLICY_VERSION) {
545
+ const apiKey = this.requireApiKey();
546
+ return this.requestJson('/v1/swarm/consent/issue', undefined, {
547
+ method: 'POST',
548
+ body: {
549
+ consent_types: [...MCODA_CONSENT_TYPES],
550
+ policy_version: policyVersion,
551
+ timestamp_ms: Date.now(),
552
+ proof: {
553
+ type: 'api_key',
554
+ value: apiKey,
555
+ },
556
+ },
557
+ });
558
+ }
559
+ async registerFreeMcodaClient(options) {
560
+ return this.requestJson('/v1/swarm/mcoda/free-client/register', undefined, {
561
+ method: 'POST',
562
+ body: {
563
+ client_id: options.clientId,
564
+ product: MCODA_PRODUCT_SLUG,
565
+ product_version: options.productVersion,
566
+ policy_version: options.policyVersion ?? MSWARM_CONSENT_POLICY_VERSION,
567
+ timestamp_ms: Date.now(),
568
+ consent_types: [...MCODA_CONSENT_TYPES],
569
+ },
570
+ });
571
+ }
572
+ async revokeConsent(consentToken, reason) {
573
+ return this.requestJson('/v1/swarm/consent/revoke', undefined, {
574
+ method: 'POST',
575
+ body: {
576
+ consent_token: consentToken,
577
+ reason,
578
+ },
579
+ });
580
+ }
581
+ async requestDataDeletion(input) {
582
+ return this.requestJson('/v1/swarm/data/deletion-request', undefined, {
583
+ method: 'POST',
584
+ body: {
585
+ consent_token: input.consentToken,
586
+ product: input.product,
587
+ client_id: input.clientId,
588
+ client_type: input.clientType,
589
+ reason: input.reason,
590
+ },
591
+ });
592
+ }
593
+ }
@@ -0,0 +1,53 @@
1
+ export interface StoredMswarmConfigState {
2
+ baseUrl?: string;
3
+ encryptedApiKey?: string;
4
+ timeoutMs?: number;
5
+ agentSlugPrefix?: string;
6
+ consentAccepted?: boolean;
7
+ consentPolicyVersion?: string;
8
+ consentToken?: string;
9
+ clientId?: string;
10
+ clientType?: string;
11
+ registeredAtMs?: number;
12
+ uploadSigningSecret?: string;
13
+ deletionRequestedAtMs?: number;
14
+ }
15
+ export interface MswarmConfigFileState extends Record<string, unknown> {
16
+ mswarm?: StoredMswarmConfigState;
17
+ }
18
+ export interface MswarmConfigState {
19
+ baseUrl?: string;
20
+ apiKey?: string;
21
+ timeoutMs?: number;
22
+ agentSlugPrefix?: string;
23
+ consentAccepted?: boolean;
24
+ consentPolicyVersion?: string;
25
+ consentToken?: string;
26
+ clientId?: string;
27
+ clientType?: string;
28
+ registeredAtMs?: number;
29
+ uploadSigningSecret?: string;
30
+ deletionRequestedAtMs?: number;
31
+ }
32
+ export interface MswarmConsentState {
33
+ consentAccepted: boolean;
34
+ consentPolicyVersion?: string;
35
+ consentToken?: string;
36
+ clientId?: string;
37
+ clientType?: string;
38
+ registeredAtMs?: number;
39
+ uploadSigningSecret?: string;
40
+ deletionRequestedAtMs?: number;
41
+ }
42
+ export declare class MswarmConfigStore {
43
+ private readonly configFilePath;
44
+ constructor(configFilePath?: string);
45
+ configPath(): string;
46
+ readState(): Promise<MswarmConfigState>;
47
+ saveApiKey(apiKey: string): Promise<MswarmConfigState>;
48
+ saveConsentState(consent: MswarmConsentState): Promise<MswarmConfigState>;
49
+ clearConsentState(): Promise<MswarmConfigState>;
50
+ private readConfigFile;
51
+ private writeConfigFile;
52
+ }
53
+ //# sourceMappingURL=MswarmConfigStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MswarmConfigStore.d.ts","sourceRoot":"","sources":["../../src/api/MswarmConfigStore.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,qBAAsB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACpE,MAAM,CAAC,EAAE,uBAAuB,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,OAAO,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,cAAc;gBAAd,cAAc,GAAE,MAAyC;IAG5E,UAAU,IAAI,MAAM;IAId,SAAS,IAAI,OAAO,CAAC,iBAAiB,CAAC;IA6BvC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAiBtD,gBAAgB,CACpB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC;IAyBvB,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,CAAC;YAcvC,cAAc;YAad,eAAe;CAQ9B"}