@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.
@@ -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 function buildPortableExportFilename(manifest: Pick<PortableManifest, 'exportedAt'> = { exportedAt: new Date().toISOString() }): string {
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
- export function exportConfig(): PortableManifest {
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).map((skill) => ({
171
- originalId: skill.id,
172
- name: skill.name,
173
- content: skill.content,
174
- description: skill.description,
175
- tags: skill.tags,
176
- scope: skill.scope,
177
- author: skill.author,
178
- version: skill.version,
179
- primaryEnv: skill.primaryEnv,
180
- capabilities: skill.capabilities,
181
- toolNames: skill.toolNames,
182
- frontmatter: skill.frontmatter,
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).map((c) => ({
207
- originalId: c.id,
208
- originalAgentId: c.agentId ?? null,
209
- originalChatroomId: c.chatroomId ?? null,
210
- name: c.name,
211
- platform: c.platform,
212
- isEnabled: false,
213
- config: scrubSecretValues(c.config),
214
- credentialsScrubbed: true,
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: [...(c.agentIds || [])],
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 || []).map((r) => ({
230
- type: r.type,
231
- pattern: r.pattern,
232
- keywords: r.keywords,
233
- originalAgentId: r.agentId,
234
- priority: r.priority,
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).map((s) => ({
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).map((p) => ({
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).map((g) => ({
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,