(input: CloneAgentInput)
| 42 | * forward — the new agent starts with an empty thread list). |
| 43 | */ |
| 44 | export async function cloneAgent(input: CloneAgentInput): Promise<CloneAgentResult> { |
| 45 | let newSlug: string; |
| 46 | if (input.slug_is_canonical) { |
| 47 | if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.new_slug)) { |
| 48 | throw new Error(`Invalid canonical slug: ${input.new_slug}`); |
| 49 | } |
| 50 | newSlug = input.new_slug; |
| 51 | } else { |
| 52 | const slugResult = slugify(input.new_slug); |
| 53 | if (!slugResult.ok) { |
| 54 | throw new Error(`Invalid agent slug: ${slugResult.reason}`); |
| 55 | } |
| 56 | newSlug = slugResult.slug; |
| 57 | } |
| 58 | const newAgentId = `${input.project_slug}-${newSlug}`; |
| 59 | if (agentExistsInProject(input.project_slug, newSlug)) { |
| 60 | throw new Error( |
| 61 | `An agent named "${newSlug}" already exists in this project. Pick a different name.`, |
| 62 | ); |
| 63 | } |
| 64 | |
| 65 | const srcDir = workspaceDirFor(input.source_agent_id); |
| 66 | const dstDir = workspaceDirFor(newAgentId); |
| 67 | let filesCopied = 0; |
| 68 | if (existsSync(srcDir)) { |
| 69 | await cp(srcDir, dstDir, { recursive: true }); |
| 70 | filesCopied = 1; // we don't enumerate; the dir is intact |
| 71 | } |
| 72 | |
| 73 | // Clone scheduled jobs. |
| 74 | const srcJobs = listAgentScheduledJobs(input.project_slug, input.source_agent_id); |
| 75 | const sourceCrons: CloneSourceCron[] = srcJobs.map((j) => ({ |
| 76 | id: j.id, |
| 77 | name: j.name, |
| 78 | disabled: j.enabled === 0, |
| 79 | })); |
| 80 | const newCronIds: string[] = []; |
| 81 | for (const j of srcJobs) { |
| 82 | try { |
| 83 | const cloned = createScheduledJob({ |
| 84 | project_slug: input.project_slug, |
| 85 | agent_id: newAgentId, |
| 86 | name: j.name, |
| 87 | cron_expr: j.cron_expr, |
| 88 | message: j.message, |
| 89 | enabled: j.enabled === 1, |
| 90 | }); |
| 91 | newCronIds.push(cloned.id); |
| 92 | } catch (err) { |
| 93 | console.error("[clone] failed to copy cron:", err); |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | const displayName = (input.display_name ?? input.source_agent_id).trim() || input.source_agent_id; |
| 98 | const sourceMeta = readAgentMeta(input.source_agent_id); |
| 99 | await writeAgentMeta({ |
| 100 | agent_id: newAgentId, |
| 101 | project_slug: input.project_slug, |
no test coverage detected