@mcoda/core 0.1.38 → 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.
@@ -1,17 +1,27 @@
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/";
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
5
  const DEFAULT_TIMEOUT_MS = 15000;
6
- const DEFAULT_AGENT_SLUG_PREFIX = "mswarm-cloud";
7
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8
- const resolveString = (value) => typeof value === "string" && value.trim() ? value : undefined;
9
- const resolveNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
10
- const resolveBoolean = (value) => typeof value === "boolean" ? value : undefined;
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
+ };
11
21
  const resolveStringArray = (value) => {
12
22
  if (!Array.isArray(value))
13
23
  return [];
14
- return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
24
+ return value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
15
25
  };
16
26
  const normalizeBaseUrl = (value, label) => {
17
27
  const trimmed = value?.trim();
@@ -35,9 +45,27 @@ const normalizePositiveInt = (value, label, fallback) => {
35
45
  }
36
46
  return Math.trunc(value);
37
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
+ };
38
64
  const resolveOptions = async (options = {}) => {
39
65
  const envTimeoutRaw = process.env.MCODA_MSWARM_TIMEOUT_MS;
40
- const envTimeout = envTimeoutRaw ? Number.parseInt(envTimeoutRaw, 10) : undefined;
66
+ const envTimeout = envTimeoutRaw
67
+ ? Number.parseInt(envTimeoutRaw, 10)
68
+ : undefined;
41
69
  const directBaseUrl = options.baseUrl ?? process.env.MCODA_MSWARM_BASE_URL;
42
70
  const directOpenAiBaseUrl = options.openAiBaseUrl ?? process.env.MCODA_MSWARM_OPENAI_BASE_URL;
43
71
  const directApiKey = options.apiKey ?? process.env.MCODA_MSWARM_API_KEY;
@@ -47,40 +75,140 @@ const resolveOptions = async (options = {}) => {
47
75
  directApiKey === undefined ||
48
76
  directTimeout === undefined ||
49
77
  directAgentSlugPrefix === undefined;
50
- const stored = needsStoredFallback ? await new MswarmConfigStore().readState() : {};
78
+ const stored = needsStoredFallback
79
+ ? await new MswarmConfigStore().readState()
80
+ : {};
51
81
  return {
52
- baseUrl: normalizeBaseUrl(directBaseUrl ?? stored.baseUrl ?? DEFAULT_BASE_URL, "MCODA_MSWARM_BASE_URL"),
82
+ baseUrl: normalizeBaseUrl(directBaseUrl ?? stored.baseUrl ?? DEFAULT_BASE_URL, 'MCODA_MSWARM_BASE_URL'),
53
83
  openAiBaseUrl: directOpenAiBaseUrl
54
- ? normalizeBaseUrl(directOpenAiBaseUrl, "MCODA_MSWARM_OPENAI_BASE_URL")
84
+ ? normalizeBaseUrl(directOpenAiBaseUrl, 'MCODA_MSWARM_OPENAI_BASE_URL')
55
85
  : undefined,
56
- apiKey: resolveString(directApiKey ?? stored.apiKey) ?? (() => {
57
- throw new Error("MCODA_MSWARM_API_KEY is required");
58
- })(),
59
- timeoutMs: normalizePositiveInt(directTimeout ?? stored.timeoutMs, "MCODA_MSWARM_TIMEOUT_MS", DEFAULT_TIMEOUT_MS),
60
- agentSlugPrefix: resolveString(directAgentSlugPrefix ?? stored.agentSlugPrefix) ?? DEFAULT_AGENT_SLUG_PREFIX,
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,
61
90
  };
62
91
  };
63
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
+ };
64
177
  const toManagedLocalSlug = (prefix, remoteSlug) => {
65
178
  const normalized = remoteSlug
66
179
  .trim()
67
180
  .toLowerCase()
68
- .replace(/[^a-z0-9]+/g, "-")
69
- .replace(/^-+|-+$/g, "");
70
- return `${prefix}-${normalized || "agent"}`;
181
+ .replace(/[^a-z0-9]+/g, '-')
182
+ .replace(/^-+|-+$/g, '');
183
+ return `${prefix}-${normalized || 'agent'}`;
71
184
  };
72
185
  const toHealthStatus = (value) => {
73
186
  const normalized = value?.trim().toLowerCase();
74
187
  if (!normalized)
75
188
  return undefined;
76
- if (normalized === "healthy")
77
- return "healthy";
78
- if (normalized === "degraded" || normalized === "unknown" || normalized === "limited")
79
- return "degraded";
80
- if (normalized === "unreachable" || normalized === "offline")
81
- return "unreachable";
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';
82
197
  return undefined;
83
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);
84
212
  const isManagedMswarmConfig = (config) => {
85
213
  if (!isRecord(config))
86
214
  return false;
@@ -111,35 +239,50 @@ const toManagedConfig = (existingConfig, catalogBaseUrl, openAiBaseUrl, agent, s
111
239
  };
112
240
  return nextConfig;
113
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
+ });
114
250
  const toCloudAgent = (value) => {
115
251
  if (!isRecord(value)) {
116
- throw new Error("mswarm returned an invalid cloud-agent payload");
252
+ throw new Error('mswarm returned an invalid cloud-agent payload');
117
253
  }
118
- const slug = resolveString(value.slug);
119
- const provider = resolveString(value.provider);
120
- const defaultModel = resolveString(value.default_model);
121
- const supportsTools = resolveBoolean(value.supports_tools);
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);
122
258
  if (!slug || !provider || !defaultModel || supportsTools === undefined) {
123
- throw new Error("mswarm cloud-agent payload is missing required fields");
259
+ throw new Error('mswarm cloud-agent payload is missing required fields');
124
260
  }
125
261
  return {
126
262
  slug,
127
263
  provider,
128
264
  default_model: defaultModel,
129
- cost_per_million: resolveNumber(value.cost_per_million),
130
- rating: resolveNumber(value.rating),
131
- reasoning_rating: resolveNumber(value.reasoning_rating),
132
- max_complexity: resolveNumber(value.max_complexity),
133
- capabilities: resolveStringArray(value.capabilities),
134
- health_status: resolveString(value.health_status),
135
- context_window: resolveNumber(value.context_window),
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),
136
273
  supports_tools: supportsTools,
137
- model_id: resolveString(value.model_id),
138
- display_name: resolveString(value.display_name),
139
- description: resolveString(value.description),
140
- supports_reasoning: resolveBoolean(value.supports_reasoning),
141
- pricing_snapshot_id: resolveString(value.pricing_snapshot_id),
142
- pricing_version: resolveString(value.pricing_version),
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),
143
286
  sync: isRecord(value.sync) ? value.sync : undefined,
144
287
  };
145
288
  };
@@ -155,6 +298,43 @@ const toCloudAgentDetail = (value) => {
155
298
  mcoda_shape: isRecord(record.mcoda_shape) ? record.mcoda_shape : undefined,
156
299
  };
157
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
+ };
158
338
  const toAgentModels = (agentId, entry) => [
159
339
  {
160
340
  agentId,
@@ -179,10 +359,41 @@ export class MswarmApi {
179
359
  const repo = await GlobalRepository.create();
180
360
  return new MswarmApi(repo, await resolveOptions(options));
181
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
+ }
182
384
  async close() {
183
385
  await this.repo.close();
184
386
  }
185
- async requestJson(pathname, query) {
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) {
186
397
  const url = new URL(pathname, this.options.baseUrl);
187
398
  if (query) {
188
399
  for (const [key, value] of Object.entries(query)) {
@@ -194,15 +405,26 @@ export class MswarmApi {
194
405
  const controller = new AbortController();
195
406
  const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs);
196
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
+ }
197
420
  const response = await fetch(url.toString(), {
198
- headers: {
199
- accept: "application/json",
200
- "x-api-key": this.options.apiKey,
201
- },
421
+ method: init?.method ?? 'GET',
422
+ headers,
423
+ body,
202
424
  signal: controller.signal,
203
425
  });
204
426
  if (!response.ok) {
205
- const body = await response.text().catch(() => "");
427
+ const body = await response.text().catch(() => '');
206
428
  throw new Error(`mswarm request failed (${response.status}): ${body || response.statusText}`);
207
429
  }
208
430
  try {
@@ -213,7 +435,7 @@ export class MswarmApi {
213
435
  }
214
436
  }
215
437
  catch (error) {
216
- if (error instanceof Error && error.name === "AbortError") {
438
+ if (error instanceof Error && error.name === 'AbortError') {
217
439
  throw new Error(`mswarm request timed out after ${this.options.timeoutMs}ms`);
218
440
  }
219
441
  throw error;
@@ -223,84 +445,149 @@ export class MswarmApi {
223
445
  }
224
446
  }
225
447
  async listCloudAgents(options = {}) {
226
- const payload = await this.requestJson("/v1/swarm/cloud/agents", {
227
- shape: "mcoda",
448
+ const remoteLimit = hasAdvancedCloudAgentSelection(options)
449
+ ? undefined
450
+ : options.limit;
451
+ const payload = await this.requestJson('/v1/swarm/cloud/agents', {
452
+ shape: 'mcoda',
228
453
  provider: options.provider,
229
- limit: options.limit,
454
+ limit: remoteLimit,
230
455
  });
231
456
  const agents = Array.isArray(payload.agents) ? payload.agents : [];
232
- return agents.map(toCloudAgent);
457
+ return applyCloudAgentListOptions(agents.map(toCloudAgent), options);
233
458
  }
234
459
  async getCloudAgent(slug) {
235
460
  if (!slug.trim()) {
236
- throw new Error("Cloud-agent slug is required");
461
+ throw new Error('Cloud-agent slug is required');
237
462
  }
238
463
  const payload = await this.requestJson(`/v1/swarm/cloud/agents/${encodeURIComponent(slug)}`);
239
464
  return toCloudAgentDetail(payload);
240
465
  }
241
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
+ }
242
471
  const agents = await this.listCloudAgents(options);
243
- const openAiBaseUrl = this.options.openAiBaseUrl ?? new URL("/v1/swarm/openai/", this.options.baseUrl).toString();
472
+ const openAiBaseUrl = this.options.openAiBaseUrl ??
473
+ new URL('/v1/swarm/openai/', this.options.baseUrl).toString();
244
474
  const syncedAt = new Date().toISOString();
245
- const encryptedApiKey = await CryptoHelper.encryptSecret(this.options.apiKey);
475
+ const encryptedApiKey = await CryptoHelper.encryptSecret(this.requireApiKey());
246
476
  const records = [];
247
477
  for (const agent of agents) {
248
478
  const localSlug = toManagedLocalSlug(this.options.agentSlugPrefix, agent.slug);
249
479
  const existing = await this.repo.getAgentBySlug(localSlug);
250
- if (existing && (!isManagedMswarmConfig(existing.config) || existing.config.mswarmCloud.remoteSlug !== agent.slug)) {
480
+ if (existing &&
481
+ (!isManagedMswarmConfig(existing.config) ||
482
+ existing.config.mswarmCloud.remoteSlug !== agent.slug)) {
251
483
  throw new Error(`Refusing to overwrite non-mswarm agent ${localSlug}`);
252
484
  }
253
- const existingConfig = existing && isRecord(existing.config) ? existing.config : undefined;
485
+ const existingConfig = existing && isRecord(existing.config)
486
+ ? existing.config
487
+ : undefined;
254
488
  const nextConfig = toManagedConfig(existingConfig, this.options.baseUrl, openAiBaseUrl, agent, syncedAt);
255
- const baseInput = {
256
- slug: localSlug,
257
- adapter: "openai-api",
258
- defaultModel: agent.default_model,
259
- openaiCompatible: true,
260
- contextWindow: agent.context_window,
261
- supportsTools: agent.supports_tools,
262
- rating: agent.rating,
263
- reasoningRating: agent.reasoning_rating,
264
- costPerMillion: agent.cost_per_million,
265
- maxComplexity: agent.max_complexity,
266
- config: nextConfig,
267
- capabilities: uniqueStrings(agent.capabilities),
268
- };
489
+ const createInput = toSyncedAgentInput(existing, agent, localSlug, nextConfig, syncedAt);
490
+ const { slug: _ignoredSlug, ...updateInput } = createInput;
269
491
  const stored = existing
270
- ? await this.repo.updateAgent(existing.id, baseInput)
271
- : await this.repo.createAgent(baseInput);
492
+ ? await this.repo.updateAgent(existing.id, updateInput)
493
+ : await this.repo.createAgent(createInput);
272
494
  if (!stored) {
273
495
  throw new Error(`Failed to persist synced agent ${localSlug}`);
274
496
  }
275
497
  await this.repo.setAgentModels(stored.id, toAgentModels(stored.id, agent));
276
498
  await this.repo.setAgentAuth(stored.id, encryptedApiKey);
499
+ const existingHealth = existing
500
+ ? await this.repo.getAgentHealth(existing.id)
501
+ : undefined;
277
502
  const mappedHealth = toHealthStatus(agent.health_status);
278
- if (mappedHealth) {
503
+ if (mappedHealth && shouldReplaceManagedHealth(existingHealth)) {
279
504
  const health = {
280
505
  agentId: stored.id,
281
506
  status: mappedHealth,
282
507
  lastCheckedAt: syncedAt,
283
508
  details: {
284
- source: "mswarm",
509
+ source: 'mswarm',
285
510
  remoteSlug: agent.slug,
286
511
  remoteHealthStatus: agent.health_status,
287
512
  },
288
513
  };
289
514
  await this.repo.setAgentHealth(health);
290
515
  }
291
- records.push({
292
- remoteSlug: agent.slug,
293
- localSlug,
294
- action: existing ? "updated" : "created",
295
- provider: agent.provider,
296
- defaultModel: agent.default_model,
297
- pricingVersion: agent.pricing_version,
298
- });
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
+ }
299
536
  }
300
537
  return {
301
- created: records.filter((record) => record.action === "created").length,
302
- updated: records.filter((record) => record.action === "updated").length,
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,
303
541
  agents: records,
304
542
  };
305
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
+ }
306
593
  }
@@ -3,6 +3,14 @@ export interface StoredMswarmConfigState {
3
3
  encryptedApiKey?: string;
4
4
  timeoutMs?: number;
5
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;
6
14
  }
7
15
  export interface MswarmConfigFileState extends Record<string, unknown> {
8
16
  mswarm?: StoredMswarmConfigState;
@@ -12,6 +20,24 @@ export interface MswarmConfigState {
12
20
  apiKey?: string;
13
21
  timeoutMs?: number;
14
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;
15
41
  }
16
42
  export declare class MswarmConfigStore {
17
43
  private readonly configFilePath;
@@ -19,6 +45,8 @@ export declare class MswarmConfigStore {
19
45
  configPath(): string;
20
46
  readState(): Promise<MswarmConfigState>;
21
47
  saveApiKey(apiKey: string): Promise<MswarmConfigState>;
48
+ saveConsentState(consent: MswarmConsentState): Promise<MswarmConfigState>;
49
+ clearConsentState(): Promise<MswarmConfigState>;
22
50
  private readConfigFile;
23
51
  private writeConfigFile;
24
52
  }
@@ -1 +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;CAC1B;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;CAC1B;AAED,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,cAAc;gBAAd,cAAc,GAAE,MAAyC;IAEtF,UAAU,IAAI,MAAM;IAId,SAAS,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAqBvC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;YAiB9C,cAAc;YAad,eAAe;CAI9B"}
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"}