import.ts
1 import { genId } from '@/lib/id' 2 import { loadAgents, saveAgents } from '@/lib/server/agents/agent-repository' 3 import { loadSkills, saveSkill } from '@/lib/server/skills/skill-repository' 4 import { loadSchedules, upsertSchedule } from '@/lib/server/schedules/schedule-repository' 5 import { loadConnectors, upsertConnector } from '@/lib/server/connectors/connector-repository' 6 import { loadChatrooms, upsertChatroom } from '@/lib/server/chatrooms/chatroom-repository' 7 import { loadMcpServers, saveMcpServers, loadProjects, saveProjects } from '@/lib/server/storage' 8 import { saveGoal } from '@/lib/server/goals/goal-repository' 9 import { logActivity } from '@/lib/server/activity/activity-log' 10 import type { Agent } from '@/types/agent' 11 import type { Skill } from '@/types/skill' 12 import type { Schedule } from '@/types/schedule' 13 import type { Connector } from '@/types/connector' 14 import type { Chatroom, McpServerConfig, Project } from '@/types' 15 import type { Goal } from '@/types/goal' 16 import type { PortableManifest, PortableAgent } from './export' 17 import { PORTABILITY_FORMAT_VERSION } from './export' 18 19 export interface ImportResult { 20 agents: { created: number; skipped: number; names: string[] } 21 skills: { created: number; skipped: number; names: string[] } 22 schedules: { created: number; skipped: number; names: string[] } 23 connectors: { created: number; skipped: number; names: string[]; needsCredentials: string[] } 24 chatrooms: { created: number; skipped: number; names: string[] } 25 mcpServers: { created: number; skipped: number; names: string[]; needsCredentials: string[] } 26 projects: { created: number; skipped: number; names: string[] } 27 goals: { created: number; skipped: number; titles: string[] } 28 /** Maps original IDs to new IDs for reference */ 29 idMap: Record<string, string> 30 } 31 32 function deduplicateName(name: string, existingNames: Set<string>): string { 33 if (!existingNames.has(name)) return name 34 let suffix = 2 35 while (existingNames.has(`${name} (${suffix})`)) suffix++ 36 return `${name} (${suffix})` 37 } 38 39 export function importConfig(manifest: PortableManifest): ImportResult { 40 if (manifest.formatVersion > PORTABILITY_FORMAT_VERSION) { 41 throw new Error(`Unsupported format version ${manifest.formatVersion} (max supported: ${PORTABILITY_FORMAT_VERSION})`) 42 } 43 44 const idMap: Record<string, string> = {} 45 const result: ImportResult = { 46 agents: { created: 0, skipped: 0, names: [] }, 47 skills: { created: 0, skipped: 0, names: [] }, 48 schedules: { created: 0, skipped: 0, names: [] }, 49 connectors: { created: 0, skipped: 0, names: [], needsCredentials: [] }, 50 chatrooms: { created: 0, skipped: 0, names: [] }, 51 mcpServers: { created: 0, skipped: 0, names: [], needsCredentials: [] }, 52 projects: { created: 0, skipped: 0, names: [] }, 53 goals: { created: 0, skipped: 0, titles: [] }, 54 idMap, 55 } 56 57 // --- Skills first (agents may reference them) --- 58 const existingSkills = loadSkills() 59 const existingSkillNames = new Set(Object.values(existingSkills).map((s) => s.name)) 60 for (const portable of manifest.skills) { 61 const name = deduplicateName(portable.name, existingSkillNames) 62 const id = genId() 63 idMap[portable.originalId] = id 64 existingSkillNames.add(name) 65 const skill: Skill = { 66 id, 67 name, 68 filename: `${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.md`, 69 content: portable.content, 70 description: portable.description, 71 tags: portable.tags, 72 scope: portable.scope || 'global', 73 author: portable.author, 74 version: portable.version, 75 primaryEnv: portable.primaryEnv, 76 capabilities: portable.capabilities, 77 toolNames: portable.toolNames, 78 frontmatter: portable.frontmatter, 79 createdAt: Date.now(), 80 updatedAt: Date.now(), 81 } 82 saveSkill(id, skill) 83 result.skills.created++ 84 result.skills.names.push(name) 85 } 86 87 // --- Projects (agents and goals may reference them) --- 88 if (manifest.projects && manifest.projects.length) { 89 const existingProjects = loadProjects() as Record<string, Project> 90 const existingProjectNames = new Set(Object.values(existingProjects).map((p) => p.name)) 91 for (const portable of manifest.projects) { 92 const name = deduplicateName(portable.name, existingProjectNames) 93 const id = genId() 94 idMap[portable.originalId] = id 95 existingProjectNames.add(name) 96 const now = Date.now() 97 const project: Project = { 98 id, 99 name, 100 description: portable.description ?? '', 101 color: portable.color, 102 objective: portable.objective, 103 audience: portable.audience, 104 priorities: portable.priorities, 105 openObjectives: portable.openObjectives, 106 capabilityHints: portable.capabilityHints, 107 credentialRequirements: portable.credentialRequirements, 108 successMetrics: portable.successMetrics, 109 heartbeatPrompt: portable.heartbeatPrompt, 110 heartbeatIntervalSec: portable.heartbeatIntervalSec, 111 createdAt: now, 112 updatedAt: now, 113 } 114 existingProjects[id] = project 115 result.projects.created++ 116 result.projects.names.push(name) 117 } 118 saveProjects(existingProjects) 119 } 120 121 // --- Agents --- 122 const existingAgents = loadAgents() 123 const existingAgentNames = new Set(Object.values(existingAgents).map((a) => a.name)) 124 for (const portable of manifest.agents) { 125 const name = deduplicateName(portable.name, existingAgentNames) 126 const id = genId() 127 const now = Date.now() 128 idMap[portable.originalId] = id 129 existingAgentNames.add(name) 130 const remappedSkillIds = (portable.skillIds || []).map((sid) => idMap[sid] || sid) 131 const remappedProjectId = portable.projectId && idMap[portable.projectId] ? idMap[portable.projectId] : portable.projectId 132 const agent: Agent = { 133 ...(portable as Omit<PortableAgent, 'originalId'>), 134 id, 135 name, 136 skillIds: remappedSkillIds, 137 projectId: remappedProjectId, 138 threadSessionId: null, 139 lastUsedAt: undefined, 140 totalCost: undefined, 141 trashedAt: undefined, 142 credentialId: null, 143 fallbackCredentialIds: [], 144 apiEndpoint: null, 145 createdAt: typeof portable.createdAt === 'number' ? portable.createdAt : now, 146 updatedAt: now, 147 } 148 existingAgents[id] = agent 149 result.agents.created++ 150 result.agents.names.push(name) 151 } 152 saveAgents(existingAgents) 153 154 // --- Schedules (need agent ID mapping) --- 155 const existingSchedules = loadSchedules() 156 const existingScheduleNames = new Set(Object.values(existingSchedules).map((s) => s.name)) 157 for (const portable of manifest.schedules) { 158 const newAgentId = idMap[portable.originalAgentId] 159 if (!newAgentId) { result.schedules.skipped++; continue } 160 const name = deduplicateName(portable.name, existingScheduleNames) 161 const id = genId() 162 idMap[portable.originalId] = id 163 existingScheduleNames.add(name) 164 const schedule: Schedule = { 165 id, name, agentId: newAgentId, 166 taskPrompt: portable.taskPrompt, 167 taskMode: portable.taskMode, 168 message: portable.message, 169 description: portable.description, 170 scheduleType: portable.scheduleType, 171 frequency: portable.frequency, 172 cron: portable.cron, 173 atTime: portable.atTime, 174 intervalMs: portable.intervalMs, 175 timezone: portable.timezone, 176 action: portable.action, 177 path: portable.path, 178 command: portable.command, 179 status: 'paused', 180 createdAt: Date.now(), 181 } 182 upsertSchedule(id, schedule) 183 result.schedules.created++ 184 result.schedules.names.push(name) 185 } 186 187 // --- MCP Servers --- 188 if (manifest.mcpServers && manifest.mcpServers.length) { 189 const existingMcp = loadMcpServers() as Record<string, McpServerConfig> 190 const existingMcpNames = new Set(Object.values(existingMcp).map((s) => s.name)) 191 for (const portable of manifest.mcpServers) { 192 const name = deduplicateName(portable.name, existingMcpNames) 193 const id = genId() 194 idMap[portable.originalId] = id 195 existingMcpNames.add(name) 196 const env: Record<string, string> = {} 197 for (const key of portable.envKeys || []) env[key] = '' 198 const headers: Record<string, string> = {} 199 for (const key of portable.headerKeys || []) headers[key] = '' 200 existingMcp[id] = { 201 id, name, 202 transport: portable.transport, 203 command: portable.command, 204 args: portable.args, 205 cwd: portable.cwd, 206 url: portable.url, 207 env: Object.keys(env).length ? env : undefined, 208 headers: Object.keys(headers).length ? headers : undefined, 209 createdAt: Date.now(), 210 updatedAt: Date.now(), 211 } as McpServerConfig 212 result.mcpServers.created++ 213 result.mcpServers.names.push(name) 214 if ((portable.envKeys?.length || 0) + (portable.headerKeys?.length || 0) > 0) { 215 result.mcpServers.needsCredentials.push(name) 216 } 217 } 218 saveMcpServers(existingMcp) 219 } 220 221 // --- Connectors --- 222 if (manifest.connectors && manifest.connectors.length) { 223 const existingConnectors = loadConnectors() 224 const existingConnectorNames = new Set(Object.values(existingConnectors).map((c) => c.name)) 225 for (const portable of manifest.connectors) { 226 const name = deduplicateName(portable.name, existingConnectorNames) 227 const id = genId() 228 idMap[portable.originalId] = id 229 existingConnectorNames.add(name) 230 const now = Date.now() 231 const remappedAgentId = portable.originalAgentId && idMap[portable.originalAgentId] 232 ? idMap[portable.originalAgentId] 233 : null 234 const remappedChatroomId = portable.originalChatroomId && idMap[portable.originalChatroomId] 235 ? idMap[portable.originalChatroomId] 236 : null 237 const connector: Connector = { 238 id, name, 239 platform: portable.platform, 240 agentId: remappedAgentId, 241 chatroomId: remappedChatroomId, 242 credentialId: null, 243 config: { ...portable.config }, 244 isEnabled: false, 245 status: 'stopped', 246 lastError: null, 247 hasCredentials: false, 248 authenticated: false, 249 createdAt: now, 250 updatedAt: now, 251 } 252 upsertConnector(id, connector) 253 result.connectors.created++ 254 result.connectors.names.push(name) 255 result.connectors.needsCredentials.push(name) 256 } 257 } 258 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 // --- Goals (after projects + agents so refs can be remapped) --- 301 if (manifest.goals && manifest.goals.length) { 302 // Two-pass to handle parent goal refs. 303 const stagedGoals: Array<{ id: string; portable: typeof manifest.goals[number] }> = [] 304 for (const portable of manifest.goals) { 305 const id = genId() 306 idMap[portable.originalId] = id 307 stagedGoals.push({ id, portable }) 308 result.goals.created++ 309 result.goals.titles.push(portable.title) 310 } 311 for (const { id, portable } of stagedGoals) { 312 const goal: Goal = { 313 id, 314 title: portable.title, 315 description: portable.description, 316 level: portable.level, 317 objective: portable.objective, 318 constraints: portable.constraints, 319 successMetric: portable.successMetric, 320 budgetUsd: portable.budgetUsd, 321 deadlineAt: portable.deadlineAt, 322 status: portable.status, 323 parentGoalId: portable.originalParentGoalId 324 ? idMap[portable.originalParentGoalId] ?? null 325 : null, 326 projectId: portable.originalProjectId 327 ? idMap[portable.originalProjectId] ?? null 328 : null, 329 agentId: portable.originalAgentId 330 ? idMap[portable.originalAgentId] ?? null 331 : null, 332 createdAt: Date.now(), 333 updatedAt: Date.now(), 334 } 335 saveGoal(id, goal) 336 } 337 } 338 339 logActivity({ 340 entityType: 'system', 341 entityId: 'portability', 342 action: 'imported', 343 actor: 'user', 344 summary: `Imported ${result.agents.created} agents, ${result.skills.created} skills, ${result.schedules.created} schedules, ` 345 + `${result.connectors.created} connectors, ${result.chatrooms.created} chatrooms, ` 346 + `${result.mcpServers.created} MCP servers, ${result.projects.created} projects, ${result.goals.created} goals`, 347 }) 348 349 return result 350 }