/ src / lib / server / portability / import.ts
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  }