@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.
- package/dist/api/MswarmApi.d.ts +64 -3
- package/dist/api/MswarmApi.d.ts.map +1 -1
- package/dist/api/MswarmApi.js +378 -91
- package/dist/api/MswarmConfigStore.d.ts +28 -0
- package/dist/api/MswarmConfigStore.d.ts.map +1 -1
- package/dist/api/MswarmConfigStore.js +55 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +1 -11
- package/dist/services/execution/AddTestsService.d.ts.map +1 -1
- package/dist/services/execution/AddTestsService.js +2 -2
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +3 -2
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +2 -6
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +1 -11
- package/dist/services/review/CodeReviewService.js +2 -2
- package/dist/services/shared/GitBranch.d.ts +6 -0
- package/dist/services/shared/GitBranch.d.ts.map +1 -0
- package/dist/services/shared/GitBranch.js +62 -0
- package/package.json +6 -6
package/dist/api/MswarmApi.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
import { GlobalRepository } from
|
|
2
|
-
import { CryptoHelper, } from
|
|
3
|
-
import { MswarmConfigStore } from
|
|
4
|
-
const DEFAULT_BASE_URL =
|
|
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 =
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
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 ===
|
|
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
|
|
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
|
|
78
|
+
const stored = needsStoredFallback
|
|
79
|
+
? await new MswarmConfigStore().readState()
|
|
80
|
+
: {};
|
|
51
81
|
return {
|
|
52
|
-
baseUrl: normalizeBaseUrl(directBaseUrl ?? stored.baseUrl ?? DEFAULT_BASE_URL,
|
|
82
|
+
baseUrl: normalizeBaseUrl(directBaseUrl ?? stored.baseUrl ?? DEFAULT_BASE_URL, 'MCODA_MSWARM_BASE_URL'),
|
|
53
83
|
openAiBaseUrl: directOpenAiBaseUrl
|
|
54
|
-
? normalizeBaseUrl(directOpenAiBaseUrl,
|
|
84
|
+
? normalizeBaseUrl(directOpenAiBaseUrl, 'MCODA_MSWARM_OPENAI_BASE_URL')
|
|
55
85
|
: undefined,
|
|
56
|
-
apiKey: resolveString(directApiKey ?? stored.apiKey)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 ||
|
|
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 ===
|
|
77
|
-
return
|
|
78
|
-
if (normalized ===
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return
|
|
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(
|
|
252
|
+
throw new Error('mswarm returned an invalid cloud-agent payload');
|
|
117
253
|
}
|
|
118
|
-
const slug =
|
|
119
|
-
const provider =
|
|
120
|
-
const defaultModel =
|
|
121
|
-
const supportsTools =
|
|
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(
|
|
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:
|
|
130
|
-
rating:
|
|
131
|
-
reasoning_rating:
|
|
132
|
-
max_complexity:
|
|
133
|
-
capabilities:
|
|
134
|
-
health_status:
|
|
135
|
-
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 ===
|
|
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
|
|
227
|
-
|
|
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:
|
|
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(
|
|
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 ??
|
|
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.
|
|
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 &&
|
|
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)
|
|
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
|
|
256
|
-
|
|
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,
|
|
271
|
-
: await this.repo.createAgent(
|
|
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:
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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 ===
|
|
302
|
-
updated: records.filter((record) => record.action ===
|
|
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;
|
|
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"}
|