@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
|
@@ -54,22 +54,60 @@ export function importConfig(manifest: PortableManifest): ImportResult {
|
|
|
54
54
|
idMap,
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// ---
|
|
57
|
+
// --- Projects first (agents, skills, schedules, and goals may reference them) ---
|
|
58
|
+
if (manifest.projects && manifest.projects.length) {
|
|
59
|
+
const existingProjects = loadProjects() as Record<string, Project>
|
|
60
|
+
const existingProjectNames = new Set(Object.values(existingProjects).map((p) => p.name))
|
|
61
|
+
for (const portable of manifest.projects) {
|
|
62
|
+
const name = deduplicateName(portable.name, existingProjectNames)
|
|
63
|
+
const id = genId()
|
|
64
|
+
idMap[portable.originalId] = id
|
|
65
|
+
existingProjectNames.add(name)
|
|
66
|
+
const now = Date.now()
|
|
67
|
+
const project: Project = {
|
|
68
|
+
id,
|
|
69
|
+
name,
|
|
70
|
+
description: portable.description ?? '',
|
|
71
|
+
color: portable.color,
|
|
72
|
+
objective: portable.objective,
|
|
73
|
+
audience: portable.audience,
|
|
74
|
+
priorities: portable.priorities,
|
|
75
|
+
openObjectives: portable.openObjectives,
|
|
76
|
+
capabilityHints: portable.capabilityHints,
|
|
77
|
+
credentialRequirements: portable.credentialRequirements,
|
|
78
|
+
successMetrics: portable.successMetrics,
|
|
79
|
+
heartbeatPrompt: portable.heartbeatPrompt,
|
|
80
|
+
heartbeatIntervalSec: portable.heartbeatIntervalSec,
|
|
81
|
+
createdAt: now,
|
|
82
|
+
updatedAt: now,
|
|
83
|
+
}
|
|
84
|
+
existingProjects[id] = project
|
|
85
|
+
result.projects.created++
|
|
86
|
+
result.projects.names.push(name)
|
|
87
|
+
}
|
|
88
|
+
saveProjects(existingProjects)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Skills (agents may reference them) ---
|
|
58
92
|
const existingSkills = loadSkills()
|
|
59
93
|
const existingSkillNames = new Set(Object.values(existingSkills).map((s) => s.name))
|
|
94
|
+
const pendingSkillAgentLinks: Array<{ skillId: string; originalAgentIds: string[] }> = []
|
|
60
95
|
for (const portable of manifest.skills) {
|
|
61
96
|
const name = deduplicateName(portable.name, existingSkillNames)
|
|
62
97
|
const id = genId()
|
|
63
98
|
idMap[portable.originalId] = id
|
|
64
99
|
existingSkillNames.add(name)
|
|
100
|
+
const originalProjectId = portable.originalProjectId ?? (portable as { projectId?: string | null }).projectId ?? null
|
|
65
101
|
const skill: Skill = {
|
|
66
102
|
id,
|
|
67
103
|
name,
|
|
68
104
|
filename: `${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.md`,
|
|
69
105
|
content: portable.content,
|
|
106
|
+
projectId: originalProjectId ? idMap[originalProjectId] || originalProjectId : undefined,
|
|
70
107
|
description: portable.description,
|
|
71
108
|
tags: portable.tags,
|
|
72
109
|
scope: portable.scope || 'global',
|
|
110
|
+
agentIds: portable.originalAgentIds ? [] : undefined,
|
|
73
111
|
author: portable.author,
|
|
74
112
|
version: portable.version,
|
|
75
113
|
primaryEnv: portable.primaryEnv,
|
|
@@ -80,47 +118,51 @@ export function importConfig(manifest: PortableManifest): ImportResult {
|
|
|
80
118
|
updatedAt: Date.now(),
|
|
81
119
|
}
|
|
82
120
|
saveSkill(id, skill)
|
|
121
|
+
if (portable.originalAgentIds?.length) {
|
|
122
|
+
pendingSkillAgentLinks.push({ skillId: id, originalAgentIds: portable.originalAgentIds })
|
|
123
|
+
}
|
|
83
124
|
result.skills.created++
|
|
84
125
|
result.skills.names.push(name)
|
|
85
126
|
}
|
|
86
127
|
|
|
87
|
-
// ---
|
|
88
|
-
if (manifest.
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
for (const portable of manifest.
|
|
92
|
-
const name = deduplicateName(portable.name,
|
|
128
|
+
// --- MCP Servers (agents may reference them) ---
|
|
129
|
+
if (manifest.mcpServers && manifest.mcpServers.length) {
|
|
130
|
+
const existingMcp = loadMcpServers() as Record<string, McpServerConfig>
|
|
131
|
+
const existingMcpNames = new Set(Object.values(existingMcp).map((s) => s.name))
|
|
132
|
+
for (const portable of manifest.mcpServers) {
|
|
133
|
+
const name = deduplicateName(portable.name, existingMcpNames)
|
|
93
134
|
const id = genId()
|
|
94
135
|
idMap[portable.originalId] = id
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
existingMcpNames.add(name)
|
|
137
|
+
const env: Record<string, string> = {}
|
|
138
|
+
for (const key of portable.envKeys || []) env[key] = ''
|
|
139
|
+
const headers: Record<string, string> = {}
|
|
140
|
+
for (const key of portable.headerKeys || []) headers[key] = ''
|
|
141
|
+
existingMcp[id] = {
|
|
142
|
+
id, name,
|
|
143
|
+
transport: portable.transport,
|
|
144
|
+
command: portable.command,
|
|
145
|
+
args: portable.args,
|
|
146
|
+
cwd: portable.cwd,
|
|
147
|
+
url: portable.url,
|
|
148
|
+
env: Object.keys(env).length ? env : undefined,
|
|
149
|
+
headers: Object.keys(headers).length ? headers : undefined,
|
|
150
|
+
createdAt: Date.now(),
|
|
151
|
+
updatedAt: Date.now(),
|
|
152
|
+
} as McpServerConfig
|
|
153
|
+
result.mcpServers.created++
|
|
154
|
+
result.mcpServers.names.push(name)
|
|
155
|
+
if ((portable.envKeys?.length || 0) + (portable.headerKeys?.length || 0) > 0) {
|
|
156
|
+
result.mcpServers.needsCredentials.push(name)
|
|
113
157
|
}
|
|
114
|
-
existingProjects[id] = project
|
|
115
|
-
result.projects.created++
|
|
116
|
-
result.projects.names.push(name)
|
|
117
158
|
}
|
|
118
|
-
|
|
159
|
+
saveMcpServers(existingMcp)
|
|
119
160
|
}
|
|
120
161
|
|
|
121
162
|
// --- Agents ---
|
|
122
163
|
const existingAgents = loadAgents()
|
|
123
164
|
const existingAgentNames = new Set(Object.values(existingAgents).map((a) => a.name))
|
|
165
|
+
const pendingAgentGoalLinks: Array<{ agentId: string; originalGoalId: string }> = []
|
|
124
166
|
for (const portable of manifest.agents) {
|
|
125
167
|
const name = deduplicateName(portable.name, existingAgentNames)
|
|
126
168
|
const id = genId()
|
|
@@ -128,13 +170,17 @@ export function importConfig(manifest: PortableManifest): ImportResult {
|
|
|
128
170
|
idMap[portable.originalId] = id
|
|
129
171
|
existingAgentNames.add(name)
|
|
130
172
|
const remappedSkillIds = (portable.skillIds || []).map((sid) => idMap[sid] || sid)
|
|
173
|
+
const remappedMcpServerIds = (portable.mcpServerIds || []).map((sid) => idMap[sid] || sid)
|
|
131
174
|
const remappedProjectId = portable.projectId && idMap[portable.projectId] ? idMap[portable.projectId] : portable.projectId
|
|
175
|
+
const originalGoalId = portable.goalId || null
|
|
132
176
|
const agent: Agent = {
|
|
133
177
|
...(portable as Omit<PortableAgent, 'originalId'>),
|
|
134
178
|
id,
|
|
135
179
|
name,
|
|
136
180
|
skillIds: remappedSkillIds,
|
|
181
|
+
mcpServerIds: remappedMcpServerIds,
|
|
137
182
|
projectId: remappedProjectId,
|
|
183
|
+
goalId: originalGoalId && idMap[originalGoalId] ? idMap[originalGoalId] : originalGoalId,
|
|
138
184
|
threadSessionId: null,
|
|
139
185
|
lastUsedAt: undefined,
|
|
140
186
|
totalCost: undefined,
|
|
@@ -148,9 +194,23 @@ export function importConfig(manifest: PortableManifest): ImportResult {
|
|
|
148
194
|
existingAgents[id] = agent
|
|
149
195
|
result.agents.created++
|
|
150
196
|
result.agents.names.push(name)
|
|
197
|
+
if (originalGoalId) pendingAgentGoalLinks.push({ agentId: id, originalGoalId })
|
|
151
198
|
}
|
|
152
199
|
saveAgents(existingAgents)
|
|
153
200
|
|
|
201
|
+
if (pendingSkillAgentLinks.length) {
|
|
202
|
+
const skills = loadSkills()
|
|
203
|
+
for (const pending of pendingSkillAgentLinks) {
|
|
204
|
+
const skill = skills[pending.skillId]
|
|
205
|
+
if (!skill) continue
|
|
206
|
+
skill.agentIds = pending.originalAgentIds
|
|
207
|
+
.map((agentId) => idMap[agentId])
|
|
208
|
+
.filter((agentId): agentId is string => Boolean(agentId))
|
|
209
|
+
skill.updatedAt = Date.now()
|
|
210
|
+
saveSkill(pending.skillId, skill)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
154
214
|
// --- Schedules (need agent ID mapping) ---
|
|
155
215
|
const existingSchedules = loadSchedules()
|
|
156
216
|
const existingScheduleNames = new Set(Object.values(existingSchedules).map((s) => s.name))
|
|
@@ -163,9 +223,21 @@ export function importConfig(manifest: PortableManifest): ImportResult {
|
|
|
163
223
|
existingScheduleNames.add(name)
|
|
164
224
|
const schedule: Schedule = {
|
|
165
225
|
id, name, agentId: newAgentId,
|
|
226
|
+
projectId: portable.projectId ? idMap[portable.projectId] || portable.projectId : undefined,
|
|
166
227
|
taskPrompt: portable.taskPrompt,
|
|
167
228
|
taskMode: portable.taskMode,
|
|
168
229
|
message: portable.message,
|
|
230
|
+
protocolTemplateId: portable.protocolTemplateId,
|
|
231
|
+
protocolParticipantAgentIds: (portable.protocolParticipantAgentIds || [])
|
|
232
|
+
.map((agentId) => idMap[agentId])
|
|
233
|
+
.filter((agentId): agentId is string => Boolean(agentId)),
|
|
234
|
+
protocolFacilitatorAgentId: portable.protocolFacilitatorAgentId
|
|
235
|
+
? idMap[portable.protocolFacilitatorAgentId] || null
|
|
236
|
+
: null,
|
|
237
|
+
protocolObserverAgentIds: (portable.protocolObserverAgentIds || [])
|
|
238
|
+
.map((agentId) => idMap[agentId])
|
|
239
|
+
.filter((agentId): agentId is string => Boolean(agentId)),
|
|
240
|
+
protocolConfig: portable.protocolConfig,
|
|
169
241
|
description: portable.description,
|
|
170
242
|
scheduleType: portable.scheduleType,
|
|
171
243
|
frequency: portable.frequency,
|
|
@@ -184,41 +256,48 @@ export function importConfig(manifest: PortableManifest): ImportResult {
|
|
|
184
256
|
result.schedules.names.push(name)
|
|
185
257
|
}
|
|
186
258
|
|
|
187
|
-
// ---
|
|
188
|
-
if (manifest.
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
for (const portable of manifest.
|
|
192
|
-
const name = deduplicateName(portable.name,
|
|
259
|
+
// --- Chatrooms ---
|
|
260
|
+
if (manifest.chatrooms && manifest.chatrooms.length) {
|
|
261
|
+
const existingChatrooms = loadChatrooms()
|
|
262
|
+
const existingChatroomNames = new Set(Object.values(existingChatrooms).map((c) => c.name))
|
|
263
|
+
for (const portable of manifest.chatrooms) {
|
|
264
|
+
const name = deduplicateName(portable.name, existingChatroomNames)
|
|
193
265
|
const id = genId()
|
|
194
266
|
idMap[portable.originalId] = id
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
267
|
+
existingChatroomNames.add(name)
|
|
268
|
+
const now = Date.now()
|
|
269
|
+
const remappedAgentIds = portable.originalAgentIds
|
|
270
|
+
.map((aid) => idMap[aid])
|
|
271
|
+
.filter((aid): aid is string => Boolean(aid))
|
|
272
|
+
const remappedRules = (portable.routingRules || []).map((r, idx) => ({
|
|
273
|
+
id: `route-${idx + 1}`,
|
|
274
|
+
type: r.type,
|
|
275
|
+
pattern: r.pattern,
|
|
276
|
+
keywords: r.keywords,
|
|
277
|
+
agentId: idMap[r.originalAgentId] || r.originalAgentId,
|
|
278
|
+
priority: r.priority,
|
|
279
|
+
}))
|
|
280
|
+
const chatroom: Chatroom = {
|
|
201
281
|
id, name,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
result.mcpServers.names.push(name)
|
|
214
|
-
if ((portable.envKeys?.length || 0) + (portable.headerKeys?.length || 0) > 0) {
|
|
215
|
-
result.mcpServers.needsCredentials.push(name)
|
|
282
|
+
description: portable.description,
|
|
283
|
+
agentIds: remappedAgentIds,
|
|
284
|
+
messages: [],
|
|
285
|
+
chatMode: portable.chatMode,
|
|
286
|
+
autoAddress: portable.autoAddress,
|
|
287
|
+
routingGuidance: portable.routingGuidance,
|
|
288
|
+
routingRules: remappedRules,
|
|
289
|
+
temporary: portable.temporary,
|
|
290
|
+
topic: portable.topic,
|
|
291
|
+
createdAt: now,
|
|
292
|
+
updatedAt: now,
|
|
216
293
|
}
|
|
294
|
+
upsertChatroom(id, chatroom)
|
|
295
|
+
result.chatrooms.created++
|
|
296
|
+
result.chatrooms.names.push(name)
|
|
217
297
|
}
|
|
218
|
-
saveMcpServers(existingMcp)
|
|
219
298
|
}
|
|
220
299
|
|
|
221
|
-
// --- Connectors ---
|
|
300
|
+
// --- Connectors (after chatrooms so room-bound connectors can remap) ---
|
|
222
301
|
if (manifest.connectors && manifest.connectors.length) {
|
|
223
302
|
const existingConnectors = loadConnectors()
|
|
224
303
|
const existingConnectorNames = new Set(Object.values(existingConnectors).map((c) => c.name))
|
|
@@ -256,47 +335,6 @@ export function importConfig(manifest: PortableManifest): ImportResult {
|
|
|
256
335
|
}
|
|
257
336
|
}
|
|
258
337
|
|
|
259
|
-
// --- Chatrooms ---
|
|
260
|
-
if (manifest.chatrooms && manifest.chatrooms.length) {
|
|
261
|
-
const existingChatrooms = loadChatrooms()
|
|
262
|
-
const existingChatroomNames = new Set(Object.values(existingChatrooms).map((c) => c.name))
|
|
263
|
-
for (const portable of manifest.chatrooms) {
|
|
264
|
-
const name = deduplicateName(portable.name, existingChatroomNames)
|
|
265
|
-
const id = genId()
|
|
266
|
-
idMap[portable.originalId] = id
|
|
267
|
-
existingChatroomNames.add(name)
|
|
268
|
-
const now = Date.now()
|
|
269
|
-
const remappedAgentIds = portable.originalAgentIds
|
|
270
|
-
.map((aid) => idMap[aid])
|
|
271
|
-
.filter((aid): aid is string => Boolean(aid))
|
|
272
|
-
const remappedRules = (portable.routingRules || []).map((r, idx) => ({
|
|
273
|
-
id: `route-${idx + 1}`,
|
|
274
|
-
type: r.type,
|
|
275
|
-
pattern: r.pattern,
|
|
276
|
-
keywords: r.keywords,
|
|
277
|
-
agentId: idMap[r.originalAgentId] || r.originalAgentId,
|
|
278
|
-
priority: r.priority,
|
|
279
|
-
}))
|
|
280
|
-
const chatroom: Chatroom = {
|
|
281
|
-
id, name,
|
|
282
|
-
description: portable.description,
|
|
283
|
-
agentIds: remappedAgentIds,
|
|
284
|
-
messages: [],
|
|
285
|
-
chatMode: portable.chatMode,
|
|
286
|
-
autoAddress: portable.autoAddress,
|
|
287
|
-
routingGuidance: portable.routingGuidance,
|
|
288
|
-
routingRules: remappedRules,
|
|
289
|
-
temporary: portable.temporary,
|
|
290
|
-
topic: portable.topic,
|
|
291
|
-
createdAt: now,
|
|
292
|
-
updatedAt: now,
|
|
293
|
-
}
|
|
294
|
-
upsertChatroom(id, chatroom)
|
|
295
|
-
result.chatrooms.created++
|
|
296
|
-
result.chatrooms.names.push(name)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
338
|
// --- Goals (after projects + agents so refs can be remapped) ---
|
|
301
339
|
if (manifest.goals && manifest.goals.length) {
|
|
302
340
|
// Two-pass to handle parent goal refs.
|
|
@@ -336,6 +374,18 @@ export function importConfig(manifest: PortableManifest): ImportResult {
|
|
|
336
374
|
}
|
|
337
375
|
}
|
|
338
376
|
|
|
377
|
+
if (pendingAgentGoalLinks.length) {
|
|
378
|
+
const agents = loadAgents()
|
|
379
|
+
for (const pending of pendingAgentGoalLinks) {
|
|
380
|
+
const remappedGoalId = idMap[pending.originalGoalId]
|
|
381
|
+
const agent = agents[pending.agentId]
|
|
382
|
+
if (!agent || !remappedGoalId) continue
|
|
383
|
+
agent.goalId = remappedGoalId
|
|
384
|
+
agent.updatedAt = Date.now()
|
|
385
|
+
}
|
|
386
|
+
saveAgents(agents)
|
|
387
|
+
}
|
|
388
|
+
|
|
339
389
|
logActivity({
|
|
340
390
|
entityType: 'system',
|
|
341
391
|
entityId: 'portability',
|
|
@@ -619,6 +619,8 @@ const PortableSkillSchema = z.object({
|
|
|
619
619
|
originalId: z.string().min(1),
|
|
620
620
|
name: z.string().min(1),
|
|
621
621
|
content: z.string(),
|
|
622
|
+
originalProjectId: z.string().nullable().optional(),
|
|
623
|
+
originalAgentIds: z.array(z.string()).optional(),
|
|
622
624
|
}).passthrough()
|
|
623
625
|
|
|
624
626
|
const PortableScheduleSchema = z.object({
|
|
@@ -627,13 +629,64 @@ const PortableScheduleSchema = z.object({
|
|
|
627
629
|
name: z.string().min(1),
|
|
628
630
|
}).passthrough()
|
|
629
631
|
|
|
632
|
+
const PortableConnectorSchema = z.object({
|
|
633
|
+
originalId: z.string().min(1),
|
|
634
|
+
name: z.string().min(1),
|
|
635
|
+
platform: z.string().min(1),
|
|
636
|
+
}).passthrough()
|
|
637
|
+
|
|
638
|
+
const PortableChatroomSchema = z.object({
|
|
639
|
+
originalId: z.string().min(1),
|
|
640
|
+
originalAgentIds: z.array(z.string()),
|
|
641
|
+
name: z.string().min(1),
|
|
642
|
+
}).passthrough()
|
|
643
|
+
|
|
644
|
+
const PortableMcpServerSchema = z.object({
|
|
645
|
+
originalId: z.string().min(1),
|
|
646
|
+
name: z.string().min(1),
|
|
647
|
+
transport: z.string().min(1),
|
|
648
|
+
}).passthrough()
|
|
649
|
+
|
|
650
|
+
const PortableProjectSchema = z.object({
|
|
651
|
+
originalId: z.string().min(1),
|
|
652
|
+
name: z.string().min(1),
|
|
653
|
+
}).passthrough()
|
|
654
|
+
|
|
655
|
+
const PortableGoalSchema = z.object({
|
|
656
|
+
originalId: z.string().min(1),
|
|
657
|
+
title: z.string().min(1),
|
|
658
|
+
level: z.string().min(1),
|
|
659
|
+
objective: z.string().min(1),
|
|
660
|
+
status: z.string().min(1),
|
|
661
|
+
}).passthrough()
|
|
662
|
+
|
|
663
|
+
const PortableExtensionRefSchema = z.object({
|
|
664
|
+
name: z.string().min(1),
|
|
665
|
+
}).passthrough()
|
|
666
|
+
|
|
667
|
+
const PortableManifestScopeSchema = z.discriminatedUnion('kind', [
|
|
668
|
+
z.object({ kind: z.literal('all') }).passthrough(),
|
|
669
|
+
z.object({
|
|
670
|
+
kind: z.literal('project'),
|
|
671
|
+
originalProjectId: z.string().min(1),
|
|
672
|
+
projectName: z.string().min(1),
|
|
673
|
+
}).passthrough(),
|
|
674
|
+
])
|
|
675
|
+
|
|
630
676
|
export const PortableManifestSchema = z.object({
|
|
631
677
|
formatVersion: z.number().int().nonnegative(),
|
|
632
678
|
exportedAt: z.string().optional(),
|
|
679
|
+
scope: PortableManifestScopeSchema.optional(),
|
|
633
680
|
agents: z.array(PortableAgentSchema),
|
|
634
681
|
skills: z.array(PortableSkillSchema),
|
|
635
682
|
schedules: z.array(PortableScheduleSchema),
|
|
636
|
-
|
|
683
|
+
connectors: z.array(PortableConnectorSchema).optional(),
|
|
684
|
+
chatrooms: z.array(PortableChatroomSchema).optional(),
|
|
685
|
+
mcpServers: z.array(PortableMcpServerSchema).optional(),
|
|
686
|
+
projects: z.array(PortableProjectSchema).optional(),
|
|
687
|
+
goals: z.array(PortableGoalSchema).optional(),
|
|
688
|
+
extensions: z.array(PortableExtensionRefSchema).optional(),
|
|
689
|
+
}).passthrough()
|
|
637
690
|
|
|
638
691
|
/** Format ZodError into a 400-friendly payload */
|
|
639
692
|
export function formatZodError(err: z.ZodError) {
|