@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 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.4",
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
- if (searchParams.get('download') === 'true') {
9
- return new NextResponse(JSON.stringify(manifest, null, 2), {
10
- headers: {
11
- 'content-type': 'application/json; charset=utf-8',
12
- 'content-disposition': `attachment; filename="${buildPortableExportFilename(manifest)}"`,
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
- const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../../..')
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 (/^Unsupported format version /i.test(message)) {
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 })