@swarmclawai/swarmclaw 1.9.4 → 1.9.6
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 +20 -0
- package/package.json +2 -2
- package/src/app/api/eval/environments/route.ts +59 -0
- package/src/app/api/eval/run/route.ts +8 -1
- package/src/app/api/eval/suite/route.ts +6 -0
- 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/cli/index.js +2 -0
- package/src/components/quality/quality-workspace.tsx +149 -5
- package/src/lib/server/eval/environment-plan.test.ts +221 -0
- package/src/lib/server/eval/environment-plan.ts +498 -0
- package/src/lib/server/eval/runner.ts +53 -3
- package/src/lib/server/eval/scenarios.ts +18 -0
- package/src/lib/server/eval/types.ts +55 -0
- 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,26 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.9.6 Highlights
|
|
403
|
+
|
|
404
|
+
Bundled eval-environment release: validation preflights, deterministic eval workspaces, and clearer operator readiness before spending run budget.
|
|
405
|
+
|
|
406
|
+
- **Eval validation environments.** `/api/eval/environments` now resolves the selected agent route, gateway target, scenario tools, generated files, and readiness checks before an eval runs.
|
|
407
|
+
- **Workspace manifests.** Eval runs now write `environment.json`, `.env.swarmclaw-eval`, and a task-focused `README.md` into each isolated eval workspace without embedding secrets.
|
|
408
|
+
- **Scenario fixtures.** Eval scenarios can declare fixture files, and the package-analysis scenario now gets a deterministic `package.json` in its workspace.
|
|
409
|
+
- **Fail-fast readiness.** Blocked evals stop before model execution when the agent route, CLI provider, gateway profile, or execution environment is not ready.
|
|
410
|
+
- **Quality UI preflight.** The Eval Lab now shows target status, gateway environment, checks, tools, and generated files next to the selected scenario.
|
|
411
|
+
|
|
412
|
+
### v1.9.5 Highlights
|
|
413
|
+
|
|
414
|
+
Bundled portability release: project-scoped workspace bundles, safer v2 imports, and preserved internal relationships for reusable teams.
|
|
415
|
+
|
|
416
|
+
- **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.
|
|
417
|
+
- **Downloadable project templates.** Project exports include a `scope` block and use readable `swarmclaw-project-...json` filenames for portable team handoff.
|
|
418
|
+
- **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.
|
|
419
|
+
- **Reference remapping.** Imports now remap project, skill, MCP server, schedule, chatroom, connector, and goal relationships so restored bundles remain internally linked.
|
|
420
|
+
- **Credential-safe bundles.** Connector credentials, MCP env values, and sensitive config keys stay scrubbed while non-secret setup hints are retained.
|
|
421
|
+
|
|
402
422
|
### v1.9.4 Highlights
|
|
403
423
|
|
|
404
424
|
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.6",
|
|
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/eval/environment-plan.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,59 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { buildEvalEnvironmentPlan } from '@/lib/server/eval/environment-plan'
|
|
5
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
6
|
+
|
|
7
|
+
const PlanSchema = z.object({
|
|
8
|
+
agentId: z.string().min(1),
|
|
9
|
+
scenarioId: z.string().min(1).nullable().optional(),
|
|
10
|
+
suite: z.string().min(1).nullable().optional(),
|
|
11
|
+
gatewayProfileId: z.string().min(1).nullable().optional(),
|
|
12
|
+
environmentId: z.string().min(1).nullable().optional(),
|
|
13
|
+
refreshGateway: z.boolean().optional(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function readBoolean(value: string | null): boolean {
|
|
17
|
+
return value === '1' || value === 'true'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function GET(req: Request) {
|
|
21
|
+
try {
|
|
22
|
+
const { searchParams } = new URL(req.url)
|
|
23
|
+
const parsed = PlanSchema.safeParse({
|
|
24
|
+
agentId: searchParams.get('agentId') || '',
|
|
25
|
+
scenarioId: searchParams.get('scenarioId'),
|
|
26
|
+
suite: searchParams.get('suite'),
|
|
27
|
+
gatewayProfileId: searchParams.get('gatewayProfileId'),
|
|
28
|
+
environmentId: searchParams.get('environmentId'),
|
|
29
|
+
refreshGateway: readBoolean(searchParams.get('refreshGateway')),
|
|
30
|
+
})
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return NextResponse.json(
|
|
33
|
+
{ error: parsed.error.issues.map((issue) => issue.message).join(', ') },
|
|
34
|
+
{ status: 400 },
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
const plan = await buildEvalEnvironmentPlan(parsed.data)
|
|
38
|
+
return NextResponse.json(plan)
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
return NextResponse.json({ error: errorMessage(err) }, { status: 500 })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function POST(req: Request) {
|
|
45
|
+
try {
|
|
46
|
+
const body: unknown = await req.json()
|
|
47
|
+
const parsed = PlanSchema.safeParse(body)
|
|
48
|
+
if (!parsed.success) {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: parsed.error.issues.map((issue) => issue.message).join(', ') },
|
|
51
|
+
{ status: 400 },
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
const plan = await buildEvalEnvironmentPlan(parsed.data)
|
|
55
|
+
return NextResponse.json(plan)
|
|
56
|
+
} catch (err: unknown) {
|
|
57
|
+
return NextResponse.json({ error: errorMessage(err) }, { status: 500 })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -7,6 +7,9 @@ import { errorMessage } from '@/lib/shared-utils'
|
|
|
7
7
|
const RunSchema = z.object({
|
|
8
8
|
scenarioId: z.string().min(1),
|
|
9
9
|
agentId: z.string().min(1),
|
|
10
|
+
gatewayProfileId: z.string().min(1).nullable().optional(),
|
|
11
|
+
environmentId: z.string().min(1).nullable().optional(),
|
|
12
|
+
refreshGateway: z.boolean().optional(),
|
|
10
13
|
})
|
|
11
14
|
|
|
12
15
|
export async function POST(req: Request) {
|
|
@@ -20,7 +23,11 @@ export async function POST(req: Request) {
|
|
|
20
23
|
)
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
const result = await runEvalScenario(parsed.data.scenarioId, parsed.data.agentId
|
|
26
|
+
const result = await runEvalScenario(parsed.data.scenarioId, parsed.data.agentId, {
|
|
27
|
+
gatewayProfileId: parsed.data.gatewayProfileId || null,
|
|
28
|
+
environmentId: parsed.data.environmentId || null,
|
|
29
|
+
refreshGateway: parsed.data.refreshGateway === true,
|
|
30
|
+
})
|
|
24
31
|
return NextResponse.json(result)
|
|
25
32
|
} catch (err: unknown) {
|
|
26
33
|
return NextResponse.json(
|
|
@@ -7,6 +7,9 @@ const SuiteSchema = z.object({
|
|
|
7
7
|
agentId: z.string().min(1),
|
|
8
8
|
categories: z.array(z.string()).optional(),
|
|
9
9
|
suite: z.string().min(1).optional(),
|
|
10
|
+
gatewayProfileId: z.string().min(1).nullable().optional(),
|
|
11
|
+
environmentId: z.string().min(1).nullable().optional(),
|
|
12
|
+
refreshGateway: z.boolean().optional(),
|
|
10
13
|
})
|
|
11
14
|
|
|
12
15
|
export async function POST(req: Request) {
|
|
@@ -23,6 +26,9 @@ export async function POST(req: Request) {
|
|
|
23
26
|
const result = await runEvalSuite(parsed.data.agentId, {
|
|
24
27
|
categories: parsed.data.categories,
|
|
25
28
|
suite: parsed.data.suite,
|
|
29
|
+
gatewayProfileId: parsed.data.gatewayProfileId || null,
|
|
30
|
+
environmentId: parsed.data.environmentId || null,
|
|
31
|
+
refreshGateway: parsed.data.refreshGateway === true,
|
|
26
32
|
})
|
|
27
33
|
return NextResponse.json(result)
|
|
28
34
|
} catch (err: unknown) {
|
|
@@ -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
|
}
|