@swarmclawai/swarmclaw 1.9.4 → 1.9.5
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/README.md +10 -0
- package/package.json +2 -2
- package/src/app/api/portability/export/route.test.ts +225 -0
- package/src/app/api/portability/export/route.ts +18 -9
- package/src/app/api/portability/import/route.test.ts +232 -31
- package/src/app/api/portability/import/route.ts +2 -2
- package/src/lib/server/portability/export.ts +244 -38
- package/src/lib/server/portability/import.ts +148 -98
- package/src/lib/validation/schemas.ts +54 -1
|
@@ -23,6 +23,7 @@ export const PORTABILITY_FORMAT_VERSION = 2
|
|
|
23
23
|
export interface PortableManifest {
|
|
24
24
|
formatVersion: number
|
|
25
25
|
exportedAt: string
|
|
26
|
+
scope?: PortableManifestScope
|
|
26
27
|
agents: PortableAgent[]
|
|
27
28
|
skills: PortableSkill[]
|
|
28
29
|
schedules: PortableSchedule[]
|
|
@@ -34,13 +35,44 @@ export interface PortableManifest {
|
|
|
34
35
|
extensions?: PortableExtensionRef[]
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export
|
|
38
|
+
export type PortableManifestScope =
|
|
39
|
+
| { kind: 'all' }
|
|
40
|
+
| { kind: 'project'; originalProjectId: string; projectName: string }
|
|
41
|
+
|
|
42
|
+
export interface ExportConfigOptions {
|
|
43
|
+
projectId?: string | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toSafeFilenameSegment(value: string): string {
|
|
47
|
+
let segment = ''
|
|
48
|
+
let lastWasDash = false
|
|
49
|
+
for (const char of value.toLowerCase()) {
|
|
50
|
+
const code = char.charCodeAt(0)
|
|
51
|
+
const isLowerAlpha = code >= 97 && code <= 122
|
|
52
|
+
const isDigit = code >= 48 && code <= 57
|
|
53
|
+
if (isLowerAlpha || isDigit) {
|
|
54
|
+
segment += char
|
|
55
|
+
lastWasDash = false
|
|
56
|
+
} else if (!lastWasDash && segment.length > 0) {
|
|
57
|
+
segment += '-'
|
|
58
|
+
lastWasDash = true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return (lastWasDash ? segment.slice(0, -1) : segment) || 'project'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildPortableExportFilename(
|
|
65
|
+
manifest: Pick<PortableManifest, 'exportedAt' | 'scope'> = { exportedAt: new Date().toISOString() },
|
|
66
|
+
): string {
|
|
38
67
|
const safeStamp = manifest.exportedAt
|
|
39
68
|
.replaceAll(':', '')
|
|
40
69
|
.replaceAll('.', '')
|
|
41
70
|
.replaceAll('-', '')
|
|
42
71
|
.replace('T', '-')
|
|
43
72
|
.replace('Z', 'Z')
|
|
73
|
+
if (manifest.scope?.kind === 'project') {
|
|
74
|
+
return `swarmclaw-project-${toSafeFilenameSegment(manifest.scope.projectName)}-${safeStamp}.json`
|
|
75
|
+
}
|
|
44
76
|
return `swarmclaw-export-${safeStamp}.json`
|
|
45
77
|
}
|
|
46
78
|
|
|
@@ -58,12 +90,17 @@ export type PortableSkill = Pick<Skill,
|
|
|
58
90
|
| 'toolNames' | 'frontmatter'
|
|
59
91
|
> & {
|
|
60
92
|
originalId: string
|
|
93
|
+
originalProjectId?: string | null
|
|
94
|
+
originalAgentIds?: string[]
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
export type PortableSchedule = Pick<Schedule,
|
|
64
98
|
| 'name' | 'taskPrompt' | 'taskMode' | 'message' | 'description'
|
|
65
99
|
| 'scheduleType' | 'frequency' | 'cron' | 'atTime' | 'intervalMs'
|
|
66
|
-
| 'timezone' | 'action' | 'path' | 'command'
|
|
100
|
+
| 'timezone' | 'action' | 'path' | 'command' | 'projectId'
|
|
101
|
+
| 'protocolTemplateId' | 'protocolParticipantAgentIds'
|
|
102
|
+
| 'protocolFacilitatorAgentId' | 'protocolObserverAgentIds'
|
|
103
|
+
| 'protocolConfig'
|
|
67
104
|
> & {
|
|
68
105
|
originalId: string
|
|
69
106
|
originalAgentId: string
|
|
@@ -149,7 +186,143 @@ function scrubSecretValues(obj: Record<string, unknown> | null | undefined): Rec
|
|
|
149
186
|
return out
|
|
150
187
|
}
|
|
151
188
|
|
|
152
|
-
|
|
189
|
+
function scheduleAgentRefs(schedule: Schedule): string[] {
|
|
190
|
+
return [
|
|
191
|
+
schedule.agentId,
|
|
192
|
+
...(schedule.protocolParticipantAgentIds || []),
|
|
193
|
+
...(schedule.protocolObserverAgentIds || []),
|
|
194
|
+
...(schedule.protocolFacilitatorAgentId ? [schedule.protocolFacilitatorAgentId] : []),
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function chatroomAgentRefs(chatroom: Chatroom): string[] {
|
|
199
|
+
return [
|
|
200
|
+
...(chatroom.agentIds || []),
|
|
201
|
+
...(chatroom.routingRules || []).map((rule) => rule.agentId),
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function hasAnyRef(ids: Iterable<string | null | undefined>, includedIds: Set<string>): boolean {
|
|
206
|
+
for (const id of ids) {
|
|
207
|
+
if (id && includedIds.has(id)) return true
|
|
208
|
+
}
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function includeGoalAncestors(goals: Record<string, Goal>, includedGoalIds: Set<string>): void {
|
|
213
|
+
let changed = true
|
|
214
|
+
while (changed) {
|
|
215
|
+
changed = false
|
|
216
|
+
for (const goalId of [...includedGoalIds]) {
|
|
217
|
+
const parentGoalId = goals[goalId]?.parentGoalId
|
|
218
|
+
if (parentGoalId && goals[parentGoalId] && !includedGoalIds.has(parentGoalId)) {
|
|
219
|
+
includedGoalIds.add(parentGoalId)
|
|
220
|
+
changed = true
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function createProjectScope(
|
|
227
|
+
options: ExportConfigOptions,
|
|
228
|
+
agents: Record<string, Agent>,
|
|
229
|
+
schedules: Record<string, Schedule>,
|
|
230
|
+
chatrooms: Record<string, Chatroom>,
|
|
231
|
+
connectors: Record<string, Connector>,
|
|
232
|
+
mcpServers: Record<string, McpServerConfig>,
|
|
233
|
+
projects: Record<string, Project>,
|
|
234
|
+
goals: Record<string, Goal>,
|
|
235
|
+
) {
|
|
236
|
+
const requestedProjectId = options.projectId?.trim() || null
|
|
237
|
+
if (!requestedProjectId) {
|
|
238
|
+
return {
|
|
239
|
+
scope: { kind: 'all' } as PortableManifestScope,
|
|
240
|
+
agentIds: null,
|
|
241
|
+
skillIds: null,
|
|
242
|
+
scheduleIds: null,
|
|
243
|
+
connectorIds: null,
|
|
244
|
+
chatroomIds: null,
|
|
245
|
+
mcpServerIds: null,
|
|
246
|
+
projectIds: null,
|
|
247
|
+
goalIds: null,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const project = projects[requestedProjectId]
|
|
252
|
+
if (!project) throw new Error(`Project not found: ${requestedProjectId}`)
|
|
253
|
+
|
|
254
|
+
const activeSchedules = Object.values(schedules).filter((schedule) => schedule.status !== 'archived')
|
|
255
|
+
const projectSchedules = activeSchedules.filter((schedule) => schedule.projectId === requestedProjectId)
|
|
256
|
+
const agentIds = new Set(
|
|
257
|
+
Object.values(agents)
|
|
258
|
+
.filter((agent) => !agent.trashedAt && !agent.disabled && agent.projectId === requestedProjectId)
|
|
259
|
+
.map((agent) => agent.id),
|
|
260
|
+
)
|
|
261
|
+
for (const schedule of projectSchedules) {
|
|
262
|
+
for (const agentId of scheduleAgentRefs(schedule)) {
|
|
263
|
+
if (agents[agentId] && !agents[agentId].trashedAt && !agents[agentId].disabled) {
|
|
264
|
+
agentIds.add(agentId)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const scheduleIds = new Set(
|
|
270
|
+
activeSchedules
|
|
271
|
+
.filter((schedule) => {
|
|
272
|
+
if (schedule.projectId === requestedProjectId) return agentIds.has(schedule.agentId)
|
|
273
|
+
if (schedule.projectId) return false
|
|
274
|
+
return hasAnyRef(scheduleAgentRefs(schedule), agentIds)
|
|
275
|
+
})
|
|
276
|
+
.map((schedule) => schedule.id),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
const skillIds = new Set<string>()
|
|
280
|
+
for (const agentId of agentIds) {
|
|
281
|
+
for (const skillId of agents[agentId]?.skillIds || []) skillIds.add(skillId)
|
|
282
|
+
}
|
|
283
|
+
const mcpServerIds = new Set<string>()
|
|
284
|
+
for (const agentId of agentIds) {
|
|
285
|
+
for (const serverId of agents[agentId]?.mcpServerIds || []) {
|
|
286
|
+
if (mcpServers[serverId]) mcpServerIds.add(serverId)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const projectIds = new Set([requestedProjectId])
|
|
291
|
+
const chatroomIds = new Set(
|
|
292
|
+
Object.values(chatrooms)
|
|
293
|
+
.filter((chatroom) => !chatroom.archivedAt && !chatroom.temporary && hasAnyRef(chatroomAgentRefs(chatroom), agentIds))
|
|
294
|
+
.map((chatroom) => chatroom.id),
|
|
295
|
+
)
|
|
296
|
+
const connectorIds = new Set(
|
|
297
|
+
Object.values(connectors)
|
|
298
|
+
.filter((connector) => {
|
|
299
|
+
if (connector.agentId && agentIds.has(connector.agentId)) return true
|
|
300
|
+
if (connector.chatroomId && chatroomIds.has(connector.chatroomId)) return true
|
|
301
|
+
return false
|
|
302
|
+
})
|
|
303
|
+
.map((connector) => connector.id),
|
|
304
|
+
)
|
|
305
|
+
const goalIds = new Set(
|
|
306
|
+
Object.values(goals)
|
|
307
|
+
.filter((goal) => goal.projectId === requestedProjectId || (goal.agentId ? agentIds.has(goal.agentId) : false))
|
|
308
|
+
.map((goal) => goal.id),
|
|
309
|
+
)
|
|
310
|
+
includeGoalAncestors(goals, goalIds)
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
scope: { kind: 'project', originalProjectId: project.id, projectName: project.name } as PortableManifestScope,
|
|
314
|
+
agentIds,
|
|
315
|
+
skillIds,
|
|
316
|
+
scheduleIds,
|
|
317
|
+
connectorIds,
|
|
318
|
+
chatroomIds,
|
|
319
|
+
mcpServerIds,
|
|
320
|
+
projectIds,
|
|
321
|
+
goalIds,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function exportConfig(options: ExportConfigOptions = {}): PortableManifest {
|
|
153
326
|
const agents = loadAgents()
|
|
154
327
|
const skills = loadSkills()
|
|
155
328
|
const schedules = loadSchedules()
|
|
@@ -158,38 +331,57 @@ export function exportConfig(): PortableManifest {
|
|
|
158
331
|
const mcpServers = loadMcpServers() as Record<string, McpServerConfig>
|
|
159
332
|
const projects = loadProjects() as Record<string, Project>
|
|
160
333
|
const goals = loadGoals() as Record<string, Goal>
|
|
334
|
+
const scope = createProjectScope(options, agents, schedules, chatrooms, connectors, mcpServers, projects, goals)
|
|
161
335
|
|
|
162
336
|
const portableAgents: PortableAgent[] = Object.values(agents)
|
|
163
337
|
.filter((a) => !a.trashedAt && !a.disabled)
|
|
338
|
+
.filter((a) => !scope.agentIds || scope.agentIds.has(a.id))
|
|
164
339
|
.map((agent) => {
|
|
165
340
|
const portable = { ...agent, originalId: agent.id } as Record<string, unknown>
|
|
166
341
|
for (const key of AGENT_STRIP_KEYS) delete portable[key]
|
|
167
342
|
return portable as PortableAgent
|
|
168
343
|
})
|
|
169
344
|
|
|
170
|
-
const portableSkills: PortableSkill[] = Object.values(skills)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
345
|
+
const portableSkills: PortableSkill[] = Object.values(skills)
|
|
346
|
+
.map((skill) => ({
|
|
347
|
+
originalId: skill.id,
|
|
348
|
+
originalProjectId: skill.projectId ?? null,
|
|
349
|
+
originalAgentIds: skill.agentIds ? [...skill.agentIds] : undefined,
|
|
350
|
+
name: skill.name,
|
|
351
|
+
content: skill.content,
|
|
352
|
+
description: skill.description,
|
|
353
|
+
tags: skill.tags,
|
|
354
|
+
scope: skill.scope,
|
|
355
|
+
author: skill.author,
|
|
356
|
+
version: skill.version,
|
|
357
|
+
primaryEnv: skill.primaryEnv,
|
|
358
|
+
capabilities: skill.capabilities,
|
|
359
|
+
toolNames: skill.toolNames,
|
|
360
|
+
frontmatter: skill.frontmatter,
|
|
361
|
+
}))
|
|
362
|
+
.filter((skill) => {
|
|
363
|
+
if (!scope.skillIds) return true
|
|
364
|
+
const scopedProjectId = scope.scope.kind === 'project' ? scope.scope.originalProjectId : null
|
|
365
|
+
if (scopedProjectId && skill.originalProjectId === scopedProjectId) return true
|
|
366
|
+
if (skill.originalAgentIds && scope.agentIds && hasAnyRef(skill.originalAgentIds, scope.agentIds)) return true
|
|
367
|
+
return scope.skillIds.has(skill.originalId)
|
|
368
|
+
})
|
|
184
369
|
|
|
185
370
|
const portableSchedules: PortableSchedule[] = Object.values(schedules)
|
|
186
371
|
.filter((s) => s.status !== 'archived')
|
|
372
|
+
.filter((s) => !scope.scheduleIds || scope.scheduleIds.has(s.id))
|
|
187
373
|
.map((schedule) => ({
|
|
188
374
|
originalId: schedule.id,
|
|
189
375
|
originalAgentId: schedule.agentId,
|
|
376
|
+
projectId: schedule.projectId,
|
|
190
377
|
name: schedule.name,
|
|
191
378
|
taskPrompt: schedule.taskPrompt,
|
|
192
379
|
taskMode: schedule.taskMode,
|
|
380
|
+
protocolTemplateId: schedule.protocolTemplateId,
|
|
381
|
+
protocolParticipantAgentIds: schedule.protocolParticipantAgentIds,
|
|
382
|
+
protocolFacilitatorAgentId: schedule.protocolFacilitatorAgentId,
|
|
383
|
+
protocolObserverAgentIds: schedule.protocolObserverAgentIds,
|
|
384
|
+
protocolConfig: schedule.protocolConfig,
|
|
193
385
|
message: schedule.message,
|
|
194
386
|
description: schedule.description,
|
|
195
387
|
scheduleType: schedule.scheduleType,
|
|
@@ -203,22 +395,27 @@ export function exportConfig(): PortableManifest {
|
|
|
203
395
|
command: schedule.command,
|
|
204
396
|
}))
|
|
205
397
|
|
|
206
|
-
const portableConnectors: PortableConnector[] = Object.values(connectors)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
398
|
+
const portableConnectors: PortableConnector[] = Object.values(connectors)
|
|
399
|
+
.filter((c) => !scope.connectorIds || scope.connectorIds.has(c.id))
|
|
400
|
+
.map((c) => ({
|
|
401
|
+
originalId: c.id,
|
|
402
|
+
originalAgentId: !scope.agentIds || (c.agentId && scope.agentIds.has(c.agentId)) ? c.agentId ?? null : null,
|
|
403
|
+
originalChatroomId: c.chatroomId ?? null,
|
|
404
|
+
name: c.name,
|
|
405
|
+
platform: c.platform,
|
|
406
|
+
isEnabled: false,
|
|
407
|
+
config: scrubSecretValues(c.config),
|
|
408
|
+
credentialsScrubbed: true,
|
|
409
|
+
}))
|
|
216
410
|
|
|
217
411
|
const portableChatrooms: PortableChatroom[] = Object.values(chatrooms)
|
|
218
412
|
.filter((c) => !c.archivedAt && !c.temporary)
|
|
413
|
+
.filter((c) => !scope.chatroomIds || scope.chatroomIds.has(c.id))
|
|
219
414
|
.map((c) => ({
|
|
220
415
|
originalId: c.id,
|
|
221
|
-
originalAgentIds:
|
|
416
|
+
originalAgentIds: scope.agentIds
|
|
417
|
+
? (c.agentIds || []).filter((agentId) => scope.agentIds?.has(agentId))
|
|
418
|
+
: [...(c.agentIds || [])],
|
|
222
419
|
name: c.name,
|
|
223
420
|
description: c.description,
|
|
224
421
|
chatMode: c.chatMode,
|
|
@@ -226,16 +423,20 @@ export function exportConfig(): PortableManifest {
|
|
|
226
423
|
routingGuidance: c.routingGuidance ?? null,
|
|
227
424
|
temporary: c.temporary,
|
|
228
425
|
topic: c.topic,
|
|
229
|
-
routingRules: (c.routingRules || [])
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
426
|
+
routingRules: (c.routingRules || [])
|
|
427
|
+
.filter((r) => !scope.agentIds || scope.agentIds.has(r.agentId))
|
|
428
|
+
.map((r) => ({
|
|
429
|
+
type: r.type,
|
|
430
|
+
pattern: r.pattern,
|
|
431
|
+
keywords: r.keywords,
|
|
432
|
+
originalAgentId: r.agentId,
|
|
433
|
+
priority: r.priority,
|
|
434
|
+
})),
|
|
236
435
|
}))
|
|
237
436
|
|
|
238
|
-
const portableMcpServers: PortableMcpServer[] = Object.values(mcpServers)
|
|
437
|
+
const portableMcpServers: PortableMcpServer[] = Object.values(mcpServers)
|
|
438
|
+
.filter((s) => !scope.mcpServerIds || scope.mcpServerIds.has(s.id))
|
|
439
|
+
.map((s) => ({
|
|
239
440
|
originalId: s.id,
|
|
240
441
|
name: s.name,
|
|
241
442
|
transport: s.transport,
|
|
@@ -248,7 +449,9 @@ export function exportConfig(): PortableManifest {
|
|
|
248
449
|
credentialsScrubbed: true,
|
|
249
450
|
}))
|
|
250
451
|
|
|
251
|
-
const portableProjects: PortableProject[] = Object.values(projects)
|
|
452
|
+
const portableProjects: PortableProject[] = Object.values(projects)
|
|
453
|
+
.filter((p) => !scope.projectIds || scope.projectIds.has(p.id))
|
|
454
|
+
.map((p) => ({
|
|
252
455
|
originalId: p.id,
|
|
253
456
|
name: p.name,
|
|
254
457
|
description: p.description,
|
|
@@ -264,7 +467,9 @@ export function exportConfig(): PortableManifest {
|
|
|
264
467
|
heartbeatIntervalSec: p.heartbeatIntervalSec,
|
|
265
468
|
}))
|
|
266
469
|
|
|
267
|
-
const portableGoals: PortableGoal[] = Object.values(goals)
|
|
470
|
+
const portableGoals: PortableGoal[] = Object.values(goals)
|
|
471
|
+
.filter((g) => !scope.goalIds || scope.goalIds.has(g.id))
|
|
472
|
+
.map((g) => ({
|
|
268
473
|
originalId: g.id,
|
|
269
474
|
originalParentGoalId: g.parentGoalId ?? null,
|
|
270
475
|
originalProjectId: g.projectId ?? null,
|
|
@@ -302,6 +507,7 @@ export function exportConfig(): PortableManifest {
|
|
|
302
507
|
return {
|
|
303
508
|
formatVersion: PORTABILITY_FORMAT_VERSION,
|
|
304
509
|
exportedAt: new Date().toISOString(),
|
|
510
|
+
scope: scope.scope,
|
|
305
511
|
agents: portableAgents,
|
|
306
512
|
skills: portableSkills,
|
|
307
513
|
schedules: portableSchedules,
|