@swarmclawai/swarmclaw 1.9.3 → 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,19 +399,39 @@ 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
+
412
+ ### v1.9.4 Highlights
413
+
414
+ Bundled runtime-environment release: gateway execution visibility, task context handoff, and operator triage in one release cycle.
415
+
416
+ - **OpenClaw environments.** Gateway topology now calls `environments.list`, stores available environment counts, exposes `/api/gateways/:id/environments`, and adds CLI commands for list/status checks.
417
+ - **Provider dashboard visibility.** The Providers screen now shows fleet-wide and per-gateway execution environment availability alongside nodes, sessions, presence, and pairings.
418
+ - **Task context packets.** Prepared task workspaces now write `context.json` with task, preview, runtime, blocker, tag, and upstream-result context for external workers.
419
+ - **Runtime env handoff.** Workspaces now include `.env.swarmclaw` plus SwarmClaw, portable task/workspace, and `AGENT_HOME` env hints without embedding secrets.
420
+ - **Operations Pulse triage.** Gateway actions now surface zero-available-environment states as high-priority operator work.
421
+
402
422
  ### v1.9.3 Highlights
403
423
 
404
- Bundled extension-orchestration release: Paperclip-style managed plugin resources, Hermes-style gateway/setup declarations, and safer local folder access in one release cycle.
424
+ Bundled extension-orchestration release: managed plugin resources, gateway/setup declarations, and safer local folder access in one release cycle.
405
425
 
406
- - **Managed extension resources.** Extensions can now declare provisionable agents, schedules/routines, local folders, gateway platforms, and setup checks through `managedResources` or Paperclip-compatible top-level aliases.
426
+ - **Managed extension resources.** Extensions can now declare provisionable agents, schedules/routines, local folders, gateway platforms, and setup checks through `managedResources` or top-level manifest aliases.
407
427
  - **Deterministic reconciliation.** `/api/extensions/managed-resources` can preview and reconcile extension-owned agents and routines with stable IDs and `managedByExtension` markers.
408
428
  - **Trusted local folders.** Extension-declared local folders support root-bounded inspection and recursive listing with traversal and symlink-escape protection.
409
429
  - **Operator UI.** The Extensions screen now shows managed-resource badges and a Managed tab with totals plus per-extension reconcile controls.
410
- - **Extension authoring spec.** `extension_creator` now documents managed resources, gateway declarations, setup checks, and Paperclip-compatible manifest aliases.
430
+ - **Extension authoring spec.** `extension_creator` now documents managed resources, gateway declarations, setup checks, and manifest aliases.
411
431
 
412
432
  ### v1.9.2 Highlights
413
433
 
414
- Bundled competitor-parity release: Hermes-style reasoning hygiene, deterministic delegation routing, Mission Control task workflow polish, OpenClaw export hardening, and Paperclip-style timeout hygiene.
434
+ Bundled runtime-polish release: reasoning hygiene, deterministic delegation routing, task workflow polish, OpenClaw export hardening, and timeout hygiene.
415
435
 
416
436
  - **Stateful reasoning tag scrubber.** String-streamed `<think>`, `<thinking>`, `<reasoning>`, `<thought>`, and `<REASONING_SCRATCHPAD>` blocks are removed across split deltas and routed into SwarmClaw's thinking stream instead of leaking into visible answers.
417
437
  - **Deterministic delegation profiles.** `manage_tasks` now accepts explicit `workType` and `requiredCapabilities` routing hints, returns a stable `routeKey`, and can auto-assign unowned work without a classifier call when the profile is explicit.
@@ -421,7 +441,7 @@ Bundled competitor-parity release: Hermes-style reasoning hygiene, deterministic
421
441
 
422
442
  ### v1.9.1 Highlights
423
443
 
424
- Task execution workspace release: the first Paperclip-style work-control slice for task-scoped workspaces, preview handoffs, and liveness evidence.
444
+ Task execution workspace release: task-scoped workspaces, preview handoffs, and liveness evidence.
425
445
 
426
446
  - **Task-scoped execution workspaces.** Tasks can now provision a deterministic workspace under the SwarmClaw workspace root, preserving source cwd and project context while creating a task-local README for artifacts and handoffs.
427
447
  - **Preview and runtime metadata.** Tasks can carry preview links and runtime services, and the task board surfaces those links directly on task cards and sheets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.3",
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",
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { getOpenClawGatewayEnvironmentStatus } from '@/lib/server/gateways/gateway-topology'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export async function GET(
9
+ _req: Request,
10
+ { params }: { params: Promise<{ id: string; environmentId: string }> },
11
+ ) {
12
+ const { id, environmentId } = await params
13
+ const snapshot = await getOpenClawGatewayEnvironmentStatus(id, decodeURIComponent(environmentId))
14
+ if (!snapshot) return notFound()
15
+ return NextResponse.json(snapshot, { status: snapshot.errors.length > 0 ? 502 : 200 })
16
+ }
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { listOpenClawGatewayEnvironments } from '@/lib/server/gateways/gateway-topology'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = await params
10
+ const snapshot = await listOpenClawGatewayEnvironments(id)
11
+ if (!snapshot) return notFound()
12
+ return NextResponse.json(snapshot)
13
+ }
@@ -18,6 +18,36 @@ test('gateway topology route returns 404 for unknown profiles', () => {
18
18
  assert.equal(output.body.error, 'Not found')
19
19
  })
20
20
 
21
+ test('gateway environments route returns 404 for unknown profiles', () => {
22
+ const output = runWithTempDataDir<{ status: number; body: { error: string } }>(`
23
+ const routeMod = await import('./src/app/api/gateways/[id]/environments/route')
24
+ const route = routeMod.default || routeMod
25
+ const response = await route.GET(
26
+ new Request('http://local/api/gateways/missing/environments'),
27
+ { params: Promise.resolve({ id: 'missing' }) },
28
+ )
29
+ console.log(JSON.stringify({ status: response.status, body: await response.json() }))
30
+ `, { prefix: 'swarmclaw-gateway-environments-route-test-' })
31
+
32
+ assert.equal(output.status, 404)
33
+ assert.equal(output.body.error, 'Not found')
34
+ })
35
+
36
+ test('gateway environment status route returns 404 for unknown profiles', () => {
37
+ const output = runWithTempDataDir<{ status: number; body: { error: string } }>(`
38
+ const routeMod = await import('./src/app/api/gateways/[id]/environments/[environmentId]/route')
39
+ const route = routeMod.default || routeMod
40
+ const response = await route.GET(
41
+ new Request('http://local/api/gateways/missing/environments/gateway'),
42
+ { params: Promise.resolve({ id: 'missing', environmentId: 'gateway' }) },
43
+ )
44
+ console.log(JSON.stringify({ status: response.status, body: await response.json() }))
45
+ `, { prefix: 'swarmclaw-gateway-environment-status-route-test-' })
46
+
47
+ assert.equal(output.status, 404)
48
+ assert.equal(output.body.error, 'Not found')
49
+ })
50
+
21
51
  test('gateway fleet route reports empty totals when no OpenClaw profiles exist', () => {
22
52
  const output = runWithTempDataDir<{
23
53
  status: number
@@ -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
  }