@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
package/README.md
CHANGED
|
@@ -399,6 +399,16 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.9.5 Highlights
|
|
403
|
+
|
|
404
|
+
Bundled portability release: project-scoped workspace bundles, safer v2 imports, and preserved internal relationships for reusable teams.
|
|
405
|
+
|
|
406
|
+
- **Project bundle export.** `/api/portability/export?projectId=...` now emits a scoped workspace template with the selected project, active agents, pinned skills, schedules, chatrooms, connectors, MCP servers, and goals.
|
|
407
|
+
- **Downloadable project templates.** Project exports include a `scope` block and use readable `swarmclaw-project-...json` filenames for portable team handoff.
|
|
408
|
+
- **v2 import preservation.** The import route now validates and preserves v2 resources instead of dropping connectors, chatrooms, MCP servers, projects, goals, extensions, or scope metadata.
|
|
409
|
+
- **Reference remapping.** Imports now remap project, skill, MCP server, schedule, chatroom, connector, and goal relationships so restored bundles remain internally linked.
|
|
410
|
+
- **Credential-safe bundles.** Connector credentials, MCP env values, and sensitive config keys stay scrubbed while non-secret setup hints are retained.
|
|
411
|
+
|
|
402
412
|
### v1.9.4 Highlights
|
|
403
413
|
|
|
404
414
|
Bundled runtime-environment release: gateway execution visibility, task context handoff, and operator triage in one release cycle.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
88
88
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
89
89
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
90
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
90
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
91
91
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
92
92
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
93
93
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -3,6 +3,7 @@ import { describe, it } from 'node:test'
|
|
|
3
3
|
|
|
4
4
|
import { GET } from './route'
|
|
5
5
|
import { buildPortableExportFilename } from '@/lib/server/portability/export'
|
|
6
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
6
7
|
|
|
7
8
|
describe('GET /api/portability/export', () => {
|
|
8
9
|
it('returns a collision-resistant attachment filename for downloads', async () => {
|
|
@@ -14,4 +15,228 @@ describe('GET /api/portability/export', () => {
|
|
|
14
15
|
const body = await response.json()
|
|
15
16
|
assert.equal(disposition, `attachment; filename="${buildPortableExportFilename(body)}"`)
|
|
16
17
|
})
|
|
18
|
+
|
|
19
|
+
it('exports a scoped project bundle with scrubbed integrations', () => {
|
|
20
|
+
const output = runWithTempDataDir<{
|
|
21
|
+
status: number
|
|
22
|
+
disposition: string
|
|
23
|
+
scopeKind: string
|
|
24
|
+
projectNames: string[]
|
|
25
|
+
agentNames: string[]
|
|
26
|
+
skillNames: string[]
|
|
27
|
+
scheduleNames: string[]
|
|
28
|
+
chatroomNames: string[]
|
|
29
|
+
connectorNames: string[]
|
|
30
|
+
mcpServerNames: string[]
|
|
31
|
+
connectorConfig: Record<string, string>
|
|
32
|
+
connectorEnabled: boolean
|
|
33
|
+
missingStatus: number
|
|
34
|
+
missingError: string
|
|
35
|
+
}>(`
|
|
36
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
37
|
+
const agentRepoMod = await import('./src/lib/server/agents/agent-repository')
|
|
38
|
+
const skillRepoMod = await import('./src/lib/server/skills/skill-repository')
|
|
39
|
+
const scheduleRepoMod = await import('./src/lib/server/schedules/schedule-repository')
|
|
40
|
+
const chatroomRepoMod = await import('./src/lib/server/chatrooms/chatroom-repository')
|
|
41
|
+
const connectorRepoMod = await import('./src/lib/server/connectors/connector-repository')
|
|
42
|
+
const routeMod = await import('./src/app/api/portability/export/route')
|
|
43
|
+
const storage = storageMod.default || storageMod
|
|
44
|
+
const agentRepo = agentRepoMod.default || agentRepoMod
|
|
45
|
+
const skillRepo = skillRepoMod.default || skillRepoMod
|
|
46
|
+
const scheduleRepo = scheduleRepoMod.default || scheduleRepoMod
|
|
47
|
+
const chatroomRepo = chatroomRepoMod.default || chatroomRepoMod
|
|
48
|
+
const connectorRepo = connectorRepoMod.default || connectorRepoMod
|
|
49
|
+
const route = routeMod.default || routeMod
|
|
50
|
+
const { saveProjects, saveMcpServers } = storage
|
|
51
|
+
const { saveAgents } = agentRepo
|
|
52
|
+
const { saveSkills } = skillRepo
|
|
53
|
+
const { saveSchedules } = scheduleRepo
|
|
54
|
+
const { upsertChatroom } = chatroomRepo
|
|
55
|
+
const { upsertConnector } = connectorRepo
|
|
56
|
+
const now = 1780000000000
|
|
57
|
+
|
|
58
|
+
saveProjects({
|
|
59
|
+
'project-a': {
|
|
60
|
+
id: 'project-a',
|
|
61
|
+
name: 'Launch Room',
|
|
62
|
+
description: 'Shipping workspace',
|
|
63
|
+
color: '#5b8def',
|
|
64
|
+
objective: 'Ship the next release',
|
|
65
|
+
createdAt: now,
|
|
66
|
+
updatedAt: now,
|
|
67
|
+
},
|
|
68
|
+
'project-b': {
|
|
69
|
+
id: 'project-b',
|
|
70
|
+
name: 'Backlog',
|
|
71
|
+
description: 'Separate workspace',
|
|
72
|
+
createdAt: now,
|
|
73
|
+
updatedAt: now,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
saveSkills({
|
|
77
|
+
'skill-a': {
|
|
78
|
+
id: 'skill-a',
|
|
79
|
+
name: 'Release Notes',
|
|
80
|
+
filename: 'release-notes.md',
|
|
81
|
+
content: 'Summarize shipped changes',
|
|
82
|
+
projectId: 'project-a',
|
|
83
|
+
scope: 'global',
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
},
|
|
87
|
+
'global-skill': {
|
|
88
|
+
id: 'global-skill',
|
|
89
|
+
name: 'Risk Scan',
|
|
90
|
+
filename: 'risk-scan.md',
|
|
91
|
+
content: 'Find release risks',
|
|
92
|
+
scope: 'global',
|
|
93
|
+
createdAt: now,
|
|
94
|
+
updatedAt: now,
|
|
95
|
+
},
|
|
96
|
+
'skill-b': {
|
|
97
|
+
id: 'skill-b',
|
|
98
|
+
name: 'Backlog Grooming',
|
|
99
|
+
filename: 'backlog-grooming.md',
|
|
100
|
+
content: 'Sort the backlog',
|
|
101
|
+
projectId: 'project-b',
|
|
102
|
+
scope: 'global',
|
|
103
|
+
createdAt: now,
|
|
104
|
+
updatedAt: now,
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
saveMcpServers({
|
|
108
|
+
'mcp-a': {
|
|
109
|
+
id: 'mcp-a',
|
|
110
|
+
name: 'Local Tools',
|
|
111
|
+
transport: 'stdio',
|
|
112
|
+
command: 'node',
|
|
113
|
+
args: ['tool.js'],
|
|
114
|
+
env: { API_TOKEN: 'secret-token' },
|
|
115
|
+
createdAt: now,
|
|
116
|
+
updatedAt: now,
|
|
117
|
+
},
|
|
118
|
+
'mcp-b': {
|
|
119
|
+
id: 'mcp-b',
|
|
120
|
+
name: 'Backlog Tools',
|
|
121
|
+
transport: 'stdio',
|
|
122
|
+
command: 'node',
|
|
123
|
+
createdAt: now,
|
|
124
|
+
updatedAt: now,
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
saveAgents({
|
|
128
|
+
'agent-a': {
|
|
129
|
+
id: 'agent-a',
|
|
130
|
+
name: 'Release Lead',
|
|
131
|
+
description: 'Owns launch execution',
|
|
132
|
+
systemPrompt: 'Ship safely',
|
|
133
|
+
provider: 'openai',
|
|
134
|
+
model: 'gpt-4o-mini',
|
|
135
|
+
projectId: 'project-a',
|
|
136
|
+
skillIds: ['skill-a', 'global-skill'],
|
|
137
|
+
mcpServerIds: ['mcp-a'],
|
|
138
|
+
createdAt: now,
|
|
139
|
+
updatedAt: now,
|
|
140
|
+
},
|
|
141
|
+
'agent-b': {
|
|
142
|
+
id: 'agent-b',
|
|
143
|
+
name: 'Backlog Lead',
|
|
144
|
+
description: 'Owns backlog',
|
|
145
|
+
systemPrompt: 'Plan later',
|
|
146
|
+
provider: 'openai',
|
|
147
|
+
model: 'gpt-4o-mini',
|
|
148
|
+
projectId: 'project-b',
|
|
149
|
+
skillIds: ['skill-b'],
|
|
150
|
+
mcpServerIds: ['mcp-b'],
|
|
151
|
+
createdAt: now,
|
|
152
|
+
updatedAt: now,
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
saveSchedules({
|
|
156
|
+
'schedule-a': {
|
|
157
|
+
id: 'schedule-a',
|
|
158
|
+
name: 'Daily Launch Check',
|
|
159
|
+
agentId: 'agent-a',
|
|
160
|
+
projectId: 'project-a',
|
|
161
|
+
taskPrompt: 'Check release readiness',
|
|
162
|
+
scheduleType: 'interval',
|
|
163
|
+
intervalMs: 60000,
|
|
164
|
+
status: 'active',
|
|
165
|
+
createdAt: now,
|
|
166
|
+
updatedAt: now,
|
|
167
|
+
},
|
|
168
|
+
'schedule-b': {
|
|
169
|
+
id: 'schedule-b',
|
|
170
|
+
name: 'Backlog Check',
|
|
171
|
+
agentId: 'agent-b',
|
|
172
|
+
projectId: 'project-b',
|
|
173
|
+
taskPrompt: 'Review backlog',
|
|
174
|
+
scheduleType: 'interval',
|
|
175
|
+
intervalMs: 60000,
|
|
176
|
+
status: 'active',
|
|
177
|
+
createdAt: now,
|
|
178
|
+
updatedAt: now,
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
upsertChatroom('room-a', {
|
|
182
|
+
id: 'room-a',
|
|
183
|
+
name: 'Launch Room Chat',
|
|
184
|
+
agentIds: ['agent-a'],
|
|
185
|
+
messages: [],
|
|
186
|
+
chatMode: 'parallel',
|
|
187
|
+
temporary: false,
|
|
188
|
+
createdAt: now,
|
|
189
|
+
updatedAt: now,
|
|
190
|
+
})
|
|
191
|
+
upsertConnector('connector-a', {
|
|
192
|
+
id: 'connector-a',
|
|
193
|
+
name: 'Launch Slack',
|
|
194
|
+
platform: 'slack',
|
|
195
|
+
agentId: 'agent-a',
|
|
196
|
+
chatroomId: 'room-a',
|
|
197
|
+
credentialId: 'credential-a',
|
|
198
|
+
config: { channel: 'launch', botToken: 'secret-token' },
|
|
199
|
+
isEnabled: true,
|
|
200
|
+
status: 'running',
|
|
201
|
+
createdAt: now,
|
|
202
|
+
updatedAt: now,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const response = await route.GET(new Request('http://local/api/portability/export?projectId=project-a&download=true'))
|
|
206
|
+
const body = await response.json()
|
|
207
|
+
const missingResponse = await route.GET(new Request('http://local/api/portability/export?projectId=missing-project'))
|
|
208
|
+
const missingPayload = await missingResponse.json()
|
|
209
|
+
console.log(JSON.stringify({
|
|
210
|
+
status: response.status,
|
|
211
|
+
disposition: response.headers.get('content-disposition') || '',
|
|
212
|
+
scopeKind: body.scope?.kind || null,
|
|
213
|
+
projectNames: (body.projects || []).map((project) => project.name),
|
|
214
|
+
agentNames: body.agents.map((agent) => agent.name),
|
|
215
|
+
skillNames: body.skills.map((skill) => skill.name).sort(),
|
|
216
|
+
scheduleNames: body.schedules.map((schedule) => schedule.name),
|
|
217
|
+
chatroomNames: (body.chatrooms || []).map((chatroom) => chatroom.name),
|
|
218
|
+
connectorNames: (body.connectors || []).map((connector) => connector.name),
|
|
219
|
+
mcpServerNames: (body.mcpServers || []).map((server) => server.name),
|
|
220
|
+
connectorConfig: body.connectors?.[0]?.config || {},
|
|
221
|
+
connectorEnabled: body.connectors?.[0]?.isEnabled ?? null,
|
|
222
|
+
missingStatus: missingResponse.status,
|
|
223
|
+
missingError: missingPayload.error,
|
|
224
|
+
}))
|
|
225
|
+
`)
|
|
226
|
+
|
|
227
|
+
assert.equal(output.status, 200)
|
|
228
|
+
assert.match(output.disposition, /^attachment; filename="swarmclaw-project-launch-room-\d{8}-\d{6}\d{3}Z\.json"$/)
|
|
229
|
+
assert.equal(output.scopeKind, 'project')
|
|
230
|
+
assert.deepEqual(output.projectNames, ['Launch Room'])
|
|
231
|
+
assert.deepEqual(output.agentNames, ['Release Lead'])
|
|
232
|
+
assert.deepEqual(output.skillNames, ['Release Notes', 'Risk Scan'])
|
|
233
|
+
assert.deepEqual(output.scheduleNames, ['Daily Launch Check'])
|
|
234
|
+
assert.deepEqual(output.chatroomNames, ['Launch Room Chat'])
|
|
235
|
+
assert.deepEqual(output.connectorNames, ['Launch Slack'])
|
|
236
|
+
assert.deepEqual(output.mcpServerNames, ['Local Tools'])
|
|
237
|
+
assert.deepEqual(output.connectorConfig, { channel: 'launch' })
|
|
238
|
+
assert.equal(output.connectorEnabled, false)
|
|
239
|
+
assert.equal(output.missingStatus, 404)
|
|
240
|
+
assert.equal(output.missingError, 'Project not found: missing-project')
|
|
241
|
+
})
|
|
17
242
|
})
|
|
@@ -3,15 +3,24 @@ import { buildPortableExportFilename, exportConfig } from '@/lib/server/portabil
|
|
|
3
3
|
export const dynamic = 'force-dynamic'
|
|
4
4
|
|
|
5
5
|
export async function GET(req: Request) {
|
|
6
|
-
const manifest = exportConfig()
|
|
7
6
|
const { searchParams } = new URL(req.url)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
const projectId = searchParams.get('projectId')?.trim() || null
|
|
8
|
+
try {
|
|
9
|
+
const manifest = exportConfig({ projectId })
|
|
10
|
+
if (searchParams.get('download') === 'true') {
|
|
11
|
+
return new NextResponse(JSON.stringify(manifest, null, 2), {
|
|
12
|
+
headers: {
|
|
13
|
+
'content-type': 'application/json; charset=utf-8',
|
|
14
|
+
'content-disposition': `attachment; filename="${buildPortableExportFilename(manifest)}"`,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
return NextResponse.json(manifest)
|
|
19
|
+
} catch (err) {
|
|
20
|
+
const message = err instanceof Error ? err.message : 'Failed to export manifest'
|
|
21
|
+
if (message.startsWith('Project not found: ')) {
|
|
22
|
+
return NextResponse.json({ error: message }, { status: 404 })
|
|
23
|
+
}
|
|
24
|
+
return NextResponse.json({ error: message }, { status: 500 })
|
|
15
25
|
}
|
|
16
|
-
return NextResponse.json(manifest)
|
|
17
26
|
}
|
|
@@ -1,39 +1,18 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
2
|
import test from 'node:test'
|
|
7
3
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function runWithTempDataDir(script: string) {
|
|
11
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-portability-import-'))
|
|
12
|
-
try {
|
|
13
|
-
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
-
cwd: repoRoot,
|
|
15
|
-
env: {
|
|
16
|
-
...process.env,
|
|
17
|
-
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
},
|
|
20
|
-
encoding: 'utf-8',
|
|
21
|
-
})
|
|
22
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
-
const lines = (result.stdout || '')
|
|
24
|
-
.trim()
|
|
25
|
-
.split('\n')
|
|
26
|
-
.map((line) => line.trim())
|
|
27
|
-
.filter(Boolean)
|
|
28
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
-
return JSON.parse(jsonLine || '{}')
|
|
30
|
-
} finally {
|
|
31
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
-
}
|
|
33
|
-
}
|
|
4
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
34
5
|
|
|
35
6
|
test('POST /api/portability/import validates manifest arrays before importing', () => {
|
|
36
|
-
const output = runWithTempDataDir
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
invalidStatus: number
|
|
9
|
+
invalidError: string | null
|
|
10
|
+
invalidPaths: string[]
|
|
11
|
+
validStatus: number
|
|
12
|
+
validAgentsCreated: number | null
|
|
13
|
+
validSkillsCreated: number | null
|
|
14
|
+
validSchedulesCreated: number | null
|
|
15
|
+
}>(`
|
|
37
16
|
const routeMod = await import('./src/app/api/portability/import/route')
|
|
38
17
|
const route = routeMod.default || routeMod
|
|
39
18
|
|
|
@@ -78,3 +57,225 @@ test('POST /api/portability/import validates manifest arrays before importing',
|
|
|
78
57
|
assert.equal(output.validSkillsCreated, 0)
|
|
79
58
|
assert.equal(output.validSchedulesCreated, 0)
|
|
80
59
|
})
|
|
60
|
+
|
|
61
|
+
test('POST /api/portability/import preserves v2 bundle resources after validation', () => {
|
|
62
|
+
const output = runWithTempDataDir<{
|
|
63
|
+
status: number
|
|
64
|
+
created: Record<string, number>
|
|
65
|
+
projectId: string | null
|
|
66
|
+
agentId: string | null
|
|
67
|
+
agentProjectId: string | null
|
|
68
|
+
agentSkillIds: string[]
|
|
69
|
+
agentMcpServerIds: string[]
|
|
70
|
+
agentGoalId: string | null
|
|
71
|
+
skillId: string | null
|
|
72
|
+
skillProjectId: string | null
|
|
73
|
+
skillAgentIds: string[]
|
|
74
|
+
scheduleProjectId: string | null
|
|
75
|
+
scheduleParticipantIds: string[]
|
|
76
|
+
scheduleFacilitatorId: string | null
|
|
77
|
+
scheduleObserverIds: string[]
|
|
78
|
+
chatroomId: string | null
|
|
79
|
+
chatroomAgentIds: string[]
|
|
80
|
+
connectorAgentId: string | null
|
|
81
|
+
connectorChatroomId: string | null
|
|
82
|
+
connectorEnabled: boolean | null
|
|
83
|
+
mcpId: string | null
|
|
84
|
+
mcpEnvKeys: string[]
|
|
85
|
+
goalId: string | null
|
|
86
|
+
goalProjectId: string | null
|
|
87
|
+
goalAgentId: string | null
|
|
88
|
+
needsCredentials: string[]
|
|
89
|
+
}>(`
|
|
90
|
+
const routeMod = await import('./src/app/api/portability/import/route')
|
|
91
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
92
|
+
const agentRepoMod = await import('./src/lib/server/agents/agent-repository')
|
|
93
|
+
const skillRepoMod = await import('./src/lib/server/skills/skill-repository')
|
|
94
|
+
const scheduleRepoMod = await import('./src/lib/server/schedules/schedule-repository')
|
|
95
|
+
const chatroomRepoMod = await import('./src/lib/server/chatrooms/chatroom-repository')
|
|
96
|
+
const connectorRepoMod = await import('./src/lib/server/connectors/connector-repository')
|
|
97
|
+
const route = routeMod.default || routeMod
|
|
98
|
+
const storage = storageMod.default || storageMod
|
|
99
|
+
const agentRepo = agentRepoMod.default || agentRepoMod
|
|
100
|
+
const skillRepo = skillRepoMod.default || skillRepoMod
|
|
101
|
+
const scheduleRepo = scheduleRepoMod.default || scheduleRepoMod
|
|
102
|
+
const chatroomRepo = chatroomRepoMod.default || chatroomRepoMod
|
|
103
|
+
const connectorRepo = connectorRepoMod.default || connectorRepoMod
|
|
104
|
+
const { loadProjects, loadMcpServers, loadGoals } = storage
|
|
105
|
+
const { loadAgents } = agentRepo
|
|
106
|
+
const { loadSkills } = skillRepo
|
|
107
|
+
const { loadSchedules } = scheduleRepo
|
|
108
|
+
const { loadChatrooms } = chatroomRepo
|
|
109
|
+
const { loadConnectors } = connectorRepo
|
|
110
|
+
|
|
111
|
+
const response = await route.POST(new Request('http://local/api/portability/import', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'content-type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
formatVersion: 2,
|
|
116
|
+
exportedAt: '2026-05-05T00:00:00.000Z',
|
|
117
|
+
scope: { kind: 'project', originalProjectId: 'project-1', projectName: 'Launch Room' },
|
|
118
|
+
projects: [{
|
|
119
|
+
originalId: 'project-1',
|
|
120
|
+
name: 'Launch Room',
|
|
121
|
+
description: 'Shipping workspace',
|
|
122
|
+
objective: 'Ship the fix',
|
|
123
|
+
}],
|
|
124
|
+
skills: [{
|
|
125
|
+
originalId: 'skill-1',
|
|
126
|
+
originalProjectId: 'project-1',
|
|
127
|
+
originalAgentIds: ['agent-1'],
|
|
128
|
+
name: 'Release Skill',
|
|
129
|
+
content: 'Ship carefully',
|
|
130
|
+
scope: 'agent',
|
|
131
|
+
}],
|
|
132
|
+
mcpServers: [{
|
|
133
|
+
originalId: 'mcp-1',
|
|
134
|
+
name: 'Local Tools',
|
|
135
|
+
transport: 'stdio',
|
|
136
|
+
command: 'node',
|
|
137
|
+
args: ['tool.js'],
|
|
138
|
+
envKeys: ['API_TOKEN'],
|
|
139
|
+
credentialsScrubbed: true,
|
|
140
|
+
}],
|
|
141
|
+
agents: [{
|
|
142
|
+
originalId: 'agent-1',
|
|
143
|
+
name: 'Release Lead',
|
|
144
|
+
description: 'Owns launch execution',
|
|
145
|
+
systemPrompt: 'Ship safely',
|
|
146
|
+
provider: 'openai',
|
|
147
|
+
model: 'gpt-4o-mini',
|
|
148
|
+
projectId: 'project-1',
|
|
149
|
+
skillIds: ['skill-1'],
|
|
150
|
+
mcpServerIds: ['mcp-1'],
|
|
151
|
+
goalId: 'goal-1',
|
|
152
|
+
}],
|
|
153
|
+
schedules: [{
|
|
154
|
+
originalId: 'schedule-1',
|
|
155
|
+
originalAgentId: 'agent-1',
|
|
156
|
+
name: 'Launch Check',
|
|
157
|
+
projectId: 'project-1',
|
|
158
|
+
taskPrompt: 'Check release readiness',
|
|
159
|
+
taskMode: 'protocol',
|
|
160
|
+
protocolTemplateId: 'template-1',
|
|
161
|
+
protocolParticipantAgentIds: ['agent-1'],
|
|
162
|
+
protocolFacilitatorAgentId: 'agent-1',
|
|
163
|
+
protocolObserverAgentIds: ['agent-1'],
|
|
164
|
+
protocolConfig: { phase: 'ship' },
|
|
165
|
+
scheduleType: 'interval',
|
|
166
|
+
intervalMs: 60000,
|
|
167
|
+
}],
|
|
168
|
+
chatrooms: [{
|
|
169
|
+
originalId: 'room-1',
|
|
170
|
+
originalAgentIds: ['agent-1'],
|
|
171
|
+
name: 'Launch Room Chat',
|
|
172
|
+
chatMode: 'parallel',
|
|
173
|
+
autoAddress: true,
|
|
174
|
+
routingRules: [{
|
|
175
|
+
type: 'keyword',
|
|
176
|
+
keywords: ['release'],
|
|
177
|
+
originalAgentId: 'agent-1',
|
|
178
|
+
priority: 1,
|
|
179
|
+
}],
|
|
180
|
+
}],
|
|
181
|
+
connectors: [{
|
|
182
|
+
originalId: 'connector-1',
|
|
183
|
+
originalAgentId: 'agent-1',
|
|
184
|
+
originalChatroomId: 'room-1',
|
|
185
|
+
name: 'Launch Slack',
|
|
186
|
+
platform: 'slack',
|
|
187
|
+
isEnabled: false,
|
|
188
|
+
config: { channel: 'launch' },
|
|
189
|
+
credentialsScrubbed: true,
|
|
190
|
+
}],
|
|
191
|
+
goals: [{
|
|
192
|
+
originalId: 'goal-1',
|
|
193
|
+
originalProjectId: 'project-1',
|
|
194
|
+
originalAgentId: 'agent-1',
|
|
195
|
+
title: 'Ship fix',
|
|
196
|
+
level: 'project',
|
|
197
|
+
objective: 'Release the portability fix',
|
|
198
|
+
status: 'active',
|
|
199
|
+
}],
|
|
200
|
+
extensions: [{ name: 'builtin-checks' }],
|
|
201
|
+
}),
|
|
202
|
+
}))
|
|
203
|
+
const payload = await response.json()
|
|
204
|
+
const project = Object.values(loadProjects()).find((item) => item.name === 'Launch Room')
|
|
205
|
+
const agent = Object.values(loadAgents()).find((item) => item.name === 'Release Lead')
|
|
206
|
+
const skill = Object.values(loadSkills()).find((item) => item.name === 'Release Skill')
|
|
207
|
+
const schedule = Object.values(loadSchedules()).find((item) => item.name === 'Launch Check')
|
|
208
|
+
const chatroom = Object.values(loadChatrooms()).find((item) => item.name === 'Launch Room Chat')
|
|
209
|
+
const connector = Object.values(loadConnectors()).find((item) => item.name === 'Launch Slack')
|
|
210
|
+
const mcp = Object.values(loadMcpServers()).find((item) => item.name === 'Local Tools')
|
|
211
|
+
const goal = Object.values(loadGoals()).find((item) => item.title === 'Ship fix')
|
|
212
|
+
|
|
213
|
+
console.log(JSON.stringify({
|
|
214
|
+
status: response.status,
|
|
215
|
+
created: {
|
|
216
|
+
agents: payload.agents.created,
|
|
217
|
+
skills: payload.skills.created,
|
|
218
|
+
schedules: payload.schedules.created,
|
|
219
|
+
connectors: payload.connectors.created,
|
|
220
|
+
chatrooms: payload.chatrooms.created,
|
|
221
|
+
mcpServers: payload.mcpServers.created,
|
|
222
|
+
projects: payload.projects.created,
|
|
223
|
+
goals: payload.goals.created,
|
|
224
|
+
},
|
|
225
|
+
projectId: project?.id || null,
|
|
226
|
+
agentId: agent?.id || null,
|
|
227
|
+
agentProjectId: agent?.projectId || null,
|
|
228
|
+
agentSkillIds: agent?.skillIds || [],
|
|
229
|
+
agentMcpServerIds: agent?.mcpServerIds || [],
|
|
230
|
+
agentGoalId: agent?.goalId || null,
|
|
231
|
+
skillId: skill?.id || null,
|
|
232
|
+
skillProjectId: skill?.projectId || null,
|
|
233
|
+
skillAgentIds: skill?.agentIds || [],
|
|
234
|
+
scheduleProjectId: schedule?.projectId || null,
|
|
235
|
+
scheduleParticipantIds: schedule?.protocolParticipantAgentIds || [],
|
|
236
|
+
scheduleFacilitatorId: schedule?.protocolFacilitatorAgentId || null,
|
|
237
|
+
scheduleObserverIds: schedule?.protocolObserverAgentIds || [],
|
|
238
|
+
chatroomId: chatroom?.id || null,
|
|
239
|
+
chatroomAgentIds: chatroom?.agentIds || [],
|
|
240
|
+
connectorAgentId: connector?.agentId || null,
|
|
241
|
+
connectorChatroomId: connector?.chatroomId || null,
|
|
242
|
+
connectorEnabled: connector?.isEnabled ?? null,
|
|
243
|
+
mcpId: mcp?.id || null,
|
|
244
|
+
mcpEnvKeys: Object.keys(mcp?.env || {}),
|
|
245
|
+
goalId: goal?.id || null,
|
|
246
|
+
goalProjectId: goal?.projectId || null,
|
|
247
|
+
goalAgentId: goal?.agentId || null,
|
|
248
|
+
needsCredentials: payload.mcpServers.needsCredentials,
|
|
249
|
+
}))
|
|
250
|
+
`)
|
|
251
|
+
|
|
252
|
+
assert.equal(output.status, 200)
|
|
253
|
+
assert.deepEqual(output.created, {
|
|
254
|
+
agents: 1,
|
|
255
|
+
skills: 1,
|
|
256
|
+
schedules: 1,
|
|
257
|
+
connectors: 1,
|
|
258
|
+
chatrooms: 1,
|
|
259
|
+
mcpServers: 1,
|
|
260
|
+
projects: 1,
|
|
261
|
+
goals: 1,
|
|
262
|
+
})
|
|
263
|
+
assert.equal(output.agentProjectId, output.projectId)
|
|
264
|
+
assert.deepEqual(output.agentSkillIds, [output.skillId])
|
|
265
|
+
assert.deepEqual(output.agentMcpServerIds, [output.mcpId])
|
|
266
|
+
assert.equal(output.agentGoalId, output.goalId)
|
|
267
|
+
assert.equal(output.skillProjectId, output.projectId)
|
|
268
|
+
assert.deepEqual(output.skillAgentIds, [output.agentId])
|
|
269
|
+
assert.equal(output.scheduleProjectId, output.projectId)
|
|
270
|
+
assert.deepEqual(output.scheduleParticipantIds, [output.agentId])
|
|
271
|
+
assert.equal(output.scheduleFacilitatorId, output.agentId)
|
|
272
|
+
assert.deepEqual(output.scheduleObserverIds, [output.agentId])
|
|
273
|
+
assert.deepEqual(output.chatroomAgentIds, [output.agentId])
|
|
274
|
+
assert.equal(output.connectorAgentId, output.agentId)
|
|
275
|
+
assert.equal(output.connectorChatroomId, output.chatroomId)
|
|
276
|
+
assert.equal(output.connectorEnabled, false)
|
|
277
|
+
assert.deepEqual(output.mcpEnvKeys, ['API_TOKEN'])
|
|
278
|
+
assert.equal(output.goalProjectId, output.projectId)
|
|
279
|
+
assert.equal(output.goalAgentId, output.agentId)
|
|
280
|
+
assert.deepEqual(output.needsCredentials, ['Local Tools'])
|
|
281
|
+
})
|
|
@@ -16,11 +16,11 @@ export async function POST(req: Request) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
|
-
const result = importConfig(parsed.data as PortableManifest)
|
|
19
|
+
const result = importConfig(parsed.data as unknown as PortableManifest)
|
|
20
20
|
return NextResponse.json(result)
|
|
21
21
|
} catch (err) {
|
|
22
22
|
const message = err instanceof Error ? err.message : 'Failed to import manifest'
|
|
23
|
-
if (
|
|
23
|
+
if (message.startsWith('Unsupported format version ')) {
|
|
24
24
|
return NextResponse.json({ error: message }, { status: 400 })
|
|
25
25
|
}
|
|
26
26
|
return NextResponse.json({ error: message }, { status: 500 })
|