import { createHash } from "node:crypto"; import { promises as fs } from "node:fs"; import { execFile } from "node:child_process"; import path from "node:path"; import { promisify } from "node:util"; import type { Db } from "@paperclipai/db"; import type { CompanyPortabilityAgentManifestEntry, CompanyPortabilityCollisionStrategy, CompanyPortabilityEnvInput, CompanyPortabilityExport, CompanyPortabilityExportResult, CompanyPortabilityImport, CompanyPortabilityImportResult, CompanyPortabilityInclude, CompanyPortabilityManifest, CompanyPortabilityPreview, CompanyPortabilityPreviewAgentPlan, CompanyPortabilityPreviewResult, CompanyPortabilityProjectManifestEntry, CompanyPortabilityIssueManifestEntry, CompanyPortabilitySkillManifestEntry, CompanySkill, } from "@paperclipai/shared"; import { ISSUE_PRIORITIES, ISSUE_STATUSES, PROJECT_STATUSES, deriveProjectUrlKey, normalizeAgentUrlKey, } from "@paperclipai/shared"; import { readPaperclipSkillSyncPreference, writePaperclipSkillSyncPreference, } from "@paperclipai/adapter-utils/server-utils"; import { notFound, unprocessable } from "../errors.js"; import { accessService } from "./access.js"; import { agentService } from "./agents.js"; import { agentInstructionsService } from "./agent-instructions.js"; import { generateReadme } from "./company-export-readme.js"; import { companySkillService } from "./company-skills.js"; import { companyService } from "./companies.js"; import { issueService } from "./issues.js"; import { projectService } from "./projects.js"; const DEFAULT_INCLUDE: CompanyPortabilityInclude = { company: true, agents: true, projects: false, issues: false, }; const DEFAULT_COLLISION_STRATEGY: CompanyPortabilityCollisionStrategy = "rename"; const execFileAsync = promisify(execFile); let bundledSkillsCommitPromise: Promise | null = null; function normalizeSkillSlug(value: string | null | undefined) { return value ? normalizeAgentUrlKey(value) ?? null : null; } function normalizeSkillKey(value: string | null | undefined) { if (!value) return null; const segments = value .split("/") .map((segment) => normalizeSkillSlug(segment)) .filter((segment): segment is string => Boolean(segment)); return segments.length > 0 ? segments.join("/") : null; } function readSkillKey(frontmatter: Record) { const metadata = isPlainRecord(frontmatter.metadata) ? frontmatter.metadata : null; const paperclip = isPlainRecord(metadata?.paperclip) ? metadata?.paperclip as Record : null; return normalizeSkillKey( asString(frontmatter.key) ?? asString(frontmatter.skillKey) ?? asString(metadata?.skillKey) ?? asString(metadata?.canonicalKey) ?? asString(metadata?.paperclipSkillKey) ?? asString(paperclip?.skillKey) ?? asString(paperclip?.key), ); } function deriveManifestSkillKey( frontmatter: Record, fallbackSlug: string, metadata: Record | null, sourceType: string, sourceLocator: string | null, ) { const explicit = readSkillKey(frontmatter); if (explicit) return explicit; const slug = normalizeSkillSlug(asString(frontmatter.slug) ?? fallbackSlug) ?? "skill"; const sourceKind = asString(metadata?.sourceKind); const owner = normalizeSkillSlug(asString(metadata?.owner)); const repo = normalizeSkillSlug(asString(metadata?.repo)); if ((sourceType === "github" || sourceKind === "github") && owner && repo) { return `${owner}/${repo}/${slug}`; } if (sourceKind === "paperclip_bundled") { return `paperclipai/paperclip/${slug}`; } if (sourceType === "url" || sourceKind === "url") { try { const host = normalizeSkillSlug(sourceLocator ? new URL(sourceLocator).host : null) ?? "url"; return `url/${host}/${slug}`; } catch { return `url/unknown/${slug}`; } } return slug; } function hashSkillValue(value: string) { return createHash("sha256").update(value).digest("hex").slice(0, 8); } function normalizeExportPathSegment(value: string | null | undefined, preserveCase = false) { if (!value) return null; const trimmed = value.trim(); if (!trimmed) return null; const normalized = trimmed .replace(/[^A-Za-z0-9._-]+/g, "-") .replace(/-+/g, "-") .replace(/^-+|-+$/g, ""); if (!normalized) return null; return preserveCase ? normalized : normalized.toLowerCase(); } function readSkillSourceKind(skill: CompanySkill) { const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null; return asString(metadata?.sourceKind); } function deriveLocalExportNamespace(skill: CompanySkill, slug: string) { const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null; const candidates = [ asString(metadata?.projectName), asString(metadata?.workspaceName), ]; if (skill.sourceLocator) { const basename = path.basename(skill.sourceLocator); candidates.push(basename.toLowerCase() === "skill.md" ? path.basename(path.dirname(skill.sourceLocator)) : basename); } for (const value of candidates) { const normalized = normalizeSkillSlug(value); if (normalized && normalized !== slug) return normalized; } return null; } function derivePrimarySkillExportDir( skill: CompanySkill, slug: string, companyIssuePrefix: string | null | undefined, ) { const normalizedKey = normalizeSkillKey(skill.key); const keySegments = normalizedKey?.split("/") ?? []; const primaryNamespace = keySegments[0] ?? null; if (primaryNamespace === "company") { const companySegment = normalizeExportPathSegment(companyIssuePrefix, true) ?? normalizeExportPathSegment(keySegments[1], true) ?? "company"; return `skills/company/${companySegment}/${slug}`; } if (primaryNamespace === "local") { const localNamespace = deriveLocalExportNamespace(skill, slug); return localNamespace ? `skills/local/${localNamespace}/${slug}` : `skills/local/${slug}`; } if (primaryNamespace === "url") { let derivedHost: string | null = keySegments[1] ?? null; if (!derivedHost) { try { derivedHost = normalizeSkillSlug(skill.sourceLocator ? new URL(skill.sourceLocator).host : null); } catch { derivedHost = null; } } const host = derivedHost ?? "url"; return `skills/url/${host}/${slug}`; } if (keySegments.length > 1) { return `skills/${keySegments.join("/")}`; } return `skills/${slug}`; } function appendSkillExportDirSuffix(packageDir: string, suffix: string) { const lastSeparator = packageDir.lastIndexOf("/"); if (lastSeparator < 0) return `${packageDir}--${suffix}`; return `${packageDir.slice(0, lastSeparator + 1)}${packageDir.slice(lastSeparator + 1)}--${suffix}`; } function deriveSkillExportDirCandidates( skill: CompanySkill, slug: string, companyIssuePrefix: string | null | undefined, ) { const primaryDir = derivePrimarySkillExportDir(skill, slug, companyIssuePrefix); const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null; const sourceKind = readSkillSourceKind(skill); const suffixes = new Set(); const pushSuffix = (value: string | null | undefined, preserveCase = false) => { const normalized = normalizeExportPathSegment(value, preserveCase); if (normalized && normalized !== slug) { suffixes.add(normalized); } }; if (sourceKind === "paperclip_bundled") { pushSuffix("paperclip"); } if (skill.sourceType === "github") { pushSuffix(asString(metadata?.repo)); pushSuffix(asString(metadata?.owner)); pushSuffix("github"); } else if (skill.sourceType === "url") { try { pushSuffix(skill.sourceLocator ? new URL(skill.sourceLocator).host : null); } catch { // Ignore URL parse failures and fall through to generic suffixes. } pushSuffix("url"); } else if (skill.sourceType === "local_path") { pushSuffix(asString(metadata?.projectName)); pushSuffix(asString(metadata?.workspaceName)); pushSuffix(deriveLocalExportNamespace(skill, slug)); if (sourceKind === "managed_local") pushSuffix("company"); if (sourceKind === "project_scan") pushSuffix("project"); pushSuffix("local"); } else { pushSuffix(sourceKind); pushSuffix("skill"); } return [primaryDir, ...Array.from(suffixes, (suffix) => appendSkillExportDirSuffix(primaryDir, suffix))]; } function buildSkillExportDirMap(skills: CompanySkill[], companyIssuePrefix: string | null | undefined) { const usedDirs = new Set(); const keyToDir = new Map(); const orderedSkills = [...skills].sort((left, right) => left.key.localeCompare(right.key)); for (const skill of orderedSkills) { const slug = normalizeSkillSlug(skill.slug) ?? "skill"; const candidates = deriveSkillExportDirCandidates(skill, slug, companyIssuePrefix); let packageDir = candidates.find((candidate) => !usedDirs.has(candidate)) ?? null; if (!packageDir) { packageDir = appendSkillExportDirSuffix(candidates[0] ?? `skills/${slug}`, hashSkillValue(skill.key)); while (usedDirs.has(packageDir)) { packageDir = appendSkillExportDirSuffix( candidates[0] ?? `skills/${slug}`, hashSkillValue(`${skill.key}:${packageDir}`), ); } } usedDirs.add(packageDir); keyToDir.set(skill.key, packageDir); } return keyToDir; } function isSensitiveEnvKey(key: string) { const normalized = key.trim().toLowerCase(); return ( normalized === "token" || normalized.endsWith("_token") || normalized.endsWith("-token") || normalized.includes("api_key") || normalized.includes("api-key") || normalized.includes("access_token") || normalized.includes("access-token") || normalized.includes("auth_token") || normalized.includes("auth-token") || normalized.includes("authorization") || normalized.includes("bearer") || normalized.includes("secret") || normalized.includes("passwd") || normalized.includes("password") || normalized.includes("credential") || normalized.includes("jwt") || normalized.includes("private_key") || normalized.includes("private-key") || normalized.includes("cookie") || normalized.includes("connectionstring") ); } type ResolvedSource = { manifest: CompanyPortabilityManifest; files: Record; warnings: string[]; }; type MarkdownDoc = { frontmatter: Record; body: string; }; type CompanyPackageIncludeEntry = { path: string; }; type PaperclipExtensionDoc = { schema?: string; company?: Record | null; agents?: Record> | null; projects?: Record> | null; tasks?: Record> | null; }; type ProjectLike = { id: string; name: string; description: string | null; leadAgentId: string | null; targetDate: string | null; color: string | null; status: string; executionWorkspacePolicy: Record | null; metadata?: Record | null; }; type IssueLike = { id: string; identifier: string | null; title: string; description: string | null; projectId: string | null; assigneeAgentId: string | null; status: string; priority: string; labelIds?: string[]; billingCode: string | null; executionWorkspaceSettings: Record | null; assigneeAdapterOverrides: Record | null; }; type ImportPlanInternal = { preview: CompanyPortabilityPreviewResult; source: ResolvedSource; include: CompanyPortabilityInclude; collisionStrategy: CompanyPortabilityCollisionStrategy; selectedAgents: CompanyPortabilityAgentManifestEntry[]; }; type AgentLike = { id: string; name: string; adapterConfig: Record; }; type EnvInputRecord = { kind: "secret" | "plain"; requirement: "required" | "optional"; default?: string | null; description?: string | null; portability?: "portable" | "system_dependent"; }; const RUNTIME_DEFAULT_RULES: Array<{ path: string[]; value: unknown }> = [ { path: ["heartbeat", "cooldownSec"], value: 10 }, { path: ["heartbeat", "intervalSec"], value: 3600 }, { path: ["heartbeat", "wakeOnOnDemand"], value: true }, { path: ["heartbeat", "wakeOnAssignment"], value: true }, { path: ["heartbeat", "wakeOnAutomation"], value: true }, { path: ["heartbeat", "wakeOnDemand"], value: true }, { path: ["heartbeat", "maxConcurrentRuns"], value: 3 }, ]; const ADAPTER_DEFAULT_RULES_BY_TYPE: Record> = { codex_local: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, ], gemini_local: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, ], opencode_local: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, ], cursor: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, ], claude_local: [ { path: ["timeoutSec"], value: 0 }, { path: ["graceSec"], value: 15 }, { path: ["maxTurnsPerRun"], value: 300 }, ], openclaw_gateway: [ { path: ["timeoutSec"], value: 120 }, { path: ["waitTimeoutMs"], value: 120000 }, { path: ["sessionKeyStrategy"], value: "fixed" }, { path: ["sessionKey"], value: "paperclip" }, { path: ["role"], value: "operator" }, { path: ["scopes"], value: ["operator.admin"] }, ], }; function isPlainRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function asString(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function toSafeSlug(input: string, fallback: string) { return normalizeAgentUrlKey(input) ?? fallback; } function uniqueSlug(base: string, used: Set) { if (!used.has(base)) { used.add(base); return base; } let idx = 2; while (true) { const candidate = `${base}-${idx}`; if (!used.has(candidate)) { used.add(candidate); return candidate; } idx += 1; } } function uniqueNameBySlug(baseName: string, existingSlugs: Set) { const baseSlug = normalizeAgentUrlKey(baseName) ?? "agent"; if (!existingSlugs.has(baseSlug)) return baseName; let idx = 2; while (true) { const candidateName = `${baseName} ${idx}`; const candidateSlug = normalizeAgentUrlKey(candidateName) ?? `agent-${idx}`; if (!existingSlugs.has(candidateSlug)) return candidateName; idx += 1; } } function uniqueProjectName(baseName: string, existingProjectSlugs: Set) { const baseSlug = deriveProjectUrlKey(baseName, baseName); if (!existingProjectSlugs.has(baseSlug)) return baseName; let idx = 2; while (true) { const candidateName = `${baseName} ${idx}`; const candidateSlug = deriveProjectUrlKey(candidateName, candidateName); if (!existingProjectSlugs.has(candidateSlug)) return candidateName; idx += 1; } } function normalizeInclude(input?: Partial): CompanyPortabilityInclude { return { company: input?.company ?? DEFAULT_INCLUDE.company, agents: input?.agents ?? DEFAULT_INCLUDE.agents, projects: input?.projects ?? DEFAULT_INCLUDE.projects, issues: input?.issues ?? DEFAULT_INCLUDE.issues, }; } function normalizePortablePath(input: string) { const normalized = input.replace(/\\/g, "/").replace(/^\.\/+/, ""); const parts: string[] = []; for (const segment of normalized.split("/")) { if (!segment || segment === ".") continue; if (segment === "..") { if (parts.length > 0) parts.pop(); continue; } parts.push(segment); } return parts.join("/"); } function resolvePortablePath(fromPath: string, targetPath: string) { const baseDir = path.posix.dirname(fromPath.replace(/\\/g, "/")); return normalizePortablePath(path.posix.join(baseDir, targetPath.replace(/\\/g, "/"))); } function normalizeFileMap( files: Record, rootPath?: string | null, ): Record { const normalizedRoot = rootPath ? normalizePortablePath(rootPath) : null; const out: Record = {}; for (const [rawPath, content] of Object.entries(files)) { let nextPath = normalizePortablePath(rawPath); if (normalizedRoot && nextPath === normalizedRoot) { continue; } if (normalizedRoot && nextPath.startsWith(`${normalizedRoot}/`)) { nextPath = nextPath.slice(normalizedRoot.length + 1); } if (!nextPath) continue; out[nextPath] = content; } return out; } function findPaperclipExtensionPath(files: Record) { if (typeof files[".paperclip.yaml"] === "string") return ".paperclip.yaml"; if (typeof files[".paperclip.yml"] === "string") return ".paperclip.yml"; return Object.keys(files).find((entry) => entry.endsWith("/.paperclip.yaml") || entry.endsWith("/.paperclip.yml")) ?? null; } function ensureMarkdownPath(pathValue: string) { const normalized = pathValue.replace(/\\/g, "/"); if (!normalized.endsWith(".md")) { throw unprocessable(`Manifest file path must end in .md: ${pathValue}`); } return normalized; } function normalizePortableConfig( value: unknown, ): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) return {}; const input = value as Record; const next: Record = {}; for (const [key, entry] of Object.entries(input)) { if ( key === "cwd" || key === "instructionsFilePath" || key === "instructionsBundleMode" || key === "instructionsRootPath" || key === "instructionsEntryFile" || key === "promptTemplate" || key === "bootstrapPromptTemplate" || key === "paperclipSkillSync" ) continue; if (key === "env") continue; next[key] = entry; } return next; } function isAbsoluteCommand(value: string) { return path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value); } function extractPortableEnvInputs( agentSlug: string, envValue: unknown, warnings: string[], ): CompanyPortabilityEnvInput[] { if (!isPlainRecord(envValue)) return []; const env = envValue as Record; const inputs: CompanyPortabilityEnvInput[] = []; for (const [key, binding] of Object.entries(env)) { if (key.toUpperCase() === "PATH") { warnings.push(`Agent ${agentSlug} PATH override was omitted from export because it is system-dependent.`); continue; } if (isPlainRecord(binding) && binding.type === "secret_ref") { inputs.push({ key, description: `Provide ${key} for agent ${agentSlug}`, agentSlug, kind: "secret", requirement: "optional", defaultValue: "", portability: "portable", }); continue; } if (isPlainRecord(binding) && binding.type === "plain") { const defaultValue = asString(binding.value); const portability = defaultValue && isAbsoluteCommand(defaultValue) ? "system_dependent" : "portable"; if (portability === "system_dependent") { warnings.push(`Agent ${agentSlug} env ${key} default was exported as system-dependent.`); } inputs.push({ key, description: `Optional default for ${key} on agent ${agentSlug}`, agentSlug, kind: "plain", requirement: "optional", defaultValue: defaultValue ?? "", portability, }); continue; } if (typeof binding === "string") { const portability = isAbsoluteCommand(binding) ? "system_dependent" : "portable"; if (portability === "system_dependent") { warnings.push(`Agent ${agentSlug} env ${key} default was exported as system-dependent.`); } inputs.push({ key, description: `Optional default for ${key} on agent ${agentSlug}`, agentSlug, kind: isSensitiveEnvKey(key) ? "secret" : "plain", requirement: "optional", defaultValue: binding, portability, }); } } return inputs; } function jsonEqual(left: unknown, right: unknown): boolean { return JSON.stringify(left) === JSON.stringify(right); } function isPathDefault(pathSegments: string[], value: unknown, rules: Array<{ path: string[]; value: unknown }>) { return rules.some((rule) => jsonEqual(rule.path, pathSegments) && jsonEqual(rule.value, value)); } function pruneDefaultLikeValue( value: unknown, opts: { dropFalseBooleans: boolean; path?: string[]; defaultRules?: Array<{ path: string[]; value: unknown }>; }, ): unknown { const pathSegments = opts.path ?? []; if (opts.defaultRules && isPathDefault(pathSegments, value, opts.defaultRules)) { return undefined; } if (Array.isArray(value)) { return value.map((entry) => pruneDefaultLikeValue(entry, { ...opts, path: pathSegments })); } if (isPlainRecord(value)) { const out: Record = {}; for (const [key, entry] of Object.entries(value)) { const next = pruneDefaultLikeValue(entry, { ...opts, path: [...pathSegments, key], }); if (next === undefined) continue; out[key] = next; } return out; } if (value === undefined) return undefined; if (opts.dropFalseBooleans && value === false) return undefined; return value; } function renderYamlScalar(value: unknown): string { if (value === null) return "null"; if (typeof value === "boolean" || typeof value === "number") return String(value); if (typeof value === "string") return JSON.stringify(value); return JSON.stringify(value); } function isEmptyObject(value: unknown): boolean { return isPlainRecord(value) && Object.keys(value).length === 0; } function isEmptyArray(value: unknown): boolean { return Array.isArray(value) && value.length === 0; } function stripEmptyValues(value: unknown, opts?: { preserveEmptyStrings?: boolean }): unknown { if (Array.isArray(value)) { const next = value .map((entry) => stripEmptyValues(entry, opts)) .filter((entry) => entry !== undefined); return next.length > 0 ? next : undefined; } if (isPlainRecord(value)) { const next: Record = {}; for (const [key, entry] of Object.entries(value)) { const cleaned = stripEmptyValues(entry, opts); if (cleaned === undefined) continue; next[key] = cleaned; } return Object.keys(next).length > 0 ? next : undefined; } if ( value === undefined || value === null || (!opts?.preserveEmptyStrings && value === "") || isEmptyArray(value) || isEmptyObject(value) ) { return undefined; } return value; } const YAML_KEY_PRIORITY = [ "name", "description", "title", "schema", "kind", "slug", "reportsTo", "skills", "owner", "assignee", "project", "schedule", "version", "license", "authors", "homepage", "tags", "includes", "requirements", "role", "icon", "capabilities", "brandColor", "adapter", "runtime", "permissions", "budgetMonthlyCents", "metadata", ] as const; const YAML_KEY_PRIORITY_INDEX = new Map( YAML_KEY_PRIORITY.map((key, index) => [key, index]), ); function compareYamlKeys(left: string, right: string) { const leftPriority = YAML_KEY_PRIORITY_INDEX.get(left); const rightPriority = YAML_KEY_PRIORITY_INDEX.get(right); if (leftPriority !== undefined || rightPriority !== undefined) { if (leftPriority === undefined) return 1; if (rightPriority === undefined) return -1; if (leftPriority !== rightPriority) return leftPriority - rightPriority; } return left.localeCompare(right); } function orderedYamlEntries(value: Record) { return Object.entries(value).sort(([leftKey], [rightKey]) => compareYamlKeys(leftKey, rightKey)); } function renderYamlBlock(value: unknown, indentLevel: number): string[] { const indent = " ".repeat(indentLevel); if (Array.isArray(value)) { if (value.length === 0) return [`${indent}[]`]; const lines: string[] = []; for (const entry of value) { const scalar = entry === null || typeof entry === "string" || typeof entry === "boolean" || typeof entry === "number" || Array.isArray(entry) && entry.length === 0 || isEmptyObject(entry); if (scalar) { lines.push(`${indent}- ${renderYamlScalar(entry)}`); continue; } lines.push(`${indent}-`); lines.push(...renderYamlBlock(entry, indentLevel + 1)); } return lines; } if (isPlainRecord(value)) { const entries = orderedYamlEntries(value); if (entries.length === 0) return [`${indent}{}`]; const lines: string[] = []; for (const [key, entry] of entries) { const scalar = entry === null || typeof entry === "string" || typeof entry === "boolean" || typeof entry === "number" || Array.isArray(entry) && entry.length === 0 || isEmptyObject(entry); if (scalar) { lines.push(`${indent}${key}: ${renderYamlScalar(entry)}`); continue; } lines.push(`${indent}${key}:`); lines.push(...renderYamlBlock(entry, indentLevel + 1)); } return lines; } return [`${indent}${renderYamlScalar(value)}`]; } function renderFrontmatter(frontmatter: Record) { const lines: string[] = ["---"]; for (const [key, value] of orderedYamlEntries(frontmatter)) { // Skip null/undefined values — don't export empty fields if (value === null || value === undefined) continue; const scalar = typeof value === "string" || typeof value === "boolean" || typeof value === "number" || Array.isArray(value) && value.length === 0 || isEmptyObject(value); if (scalar) { lines.push(`${key}: ${renderYamlScalar(value)}`); continue; } lines.push(`${key}:`); lines.push(...renderYamlBlock(value, 1)); } lines.push("---"); return `${lines.join("\n")}\n`; } function buildMarkdown(frontmatter: Record, body: string) { const cleanBody = body.replace(/\r\n/g, "\n").trim(); if (!cleanBody) { return `${renderFrontmatter(frontmatter)}\n`; } return `${renderFrontmatter(frontmatter)}\n${cleanBody}\n`; } function normalizeSelectedFiles(selectedFiles?: string[]) { if (!selectedFiles) return null; return new Set( selectedFiles .map((entry) => normalizePortablePath(entry)) .filter((entry) => entry.length > 0), ); } function filterCompanyMarkdownIncludes( companyPath: string, markdown: string, selectedFiles: Set, ) { const parsed = parseFrontmatterMarkdown(markdown); const includeEntries = readIncludeEntries(parsed.frontmatter); const filteredIncludes = includeEntries.filter((entry) => selectedFiles.has(resolvePortablePath(companyPath, entry.path)), ); const nextFrontmatter: Record = { ...parsed.frontmatter }; if (filteredIncludes.length > 0) { nextFrontmatter.includes = filteredIncludes.map((entry) => entry.path); } else { delete nextFrontmatter.includes; } return buildMarkdown(nextFrontmatter, parsed.body); } function applySelectedFilesToSource(source: ResolvedSource, selectedFiles?: string[]): ResolvedSource { const normalizedSelection = normalizeSelectedFiles(selectedFiles); if (!normalizedSelection) return source; const companyPath = source.manifest.company ? ensureMarkdownPath(source.manifest.company.path) : Object.keys(source.files).find((entry) => entry.endsWith("/COMPANY.md") || entry === "COMPANY.md") ?? null; if (!companyPath) { throw unprocessable("Company package is missing COMPANY.md"); } const companyMarkdown = source.files[companyPath]; if (typeof companyMarkdown !== "string") { throw unprocessable("Company package is missing COMPANY.md"); } const effectiveFiles: Record = {}; for (const [filePath, content] of Object.entries(source.files)) { const normalizedPath = normalizePortablePath(filePath); if (!normalizedSelection.has(normalizedPath)) continue; effectiveFiles[normalizedPath] = content; } effectiveFiles[companyPath] = filterCompanyMarkdownIncludes( companyPath, companyMarkdown, normalizedSelection, ); const filtered = buildManifestFromPackageFiles(effectiveFiles, { sourceLabel: source.manifest.source, }); if (!normalizedSelection.has(companyPath)) { filtered.manifest.company = null; } filtered.manifest.includes = { company: filtered.manifest.company !== null, agents: filtered.manifest.agents.length > 0, projects: filtered.manifest.projects.length > 0, issues: filtered.manifest.issues.length > 0, }; return filtered; } async function resolveBundledSkillsCommit() { if (!bundledSkillsCommitPromise) { bundledSkillsCommitPromise = execFileAsync("git", ["rev-parse", "HEAD"], { cwd: process.cwd(), encoding: "utf8", }) .then(({ stdout }) => stdout.trim() || null) .catch(() => null); } return bundledSkillsCommitPromise; } async function buildSkillSourceEntry(skill: CompanySkill) { const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null; if (asString(metadata?.sourceKind) === "paperclip_bundled") { const commit = await resolveBundledSkillsCommit(); return { kind: "github-dir", repo: "paperclipai/paperclip", path: `skills/${skill.slug}`, commit, trackingRef: "master", url: `https://github.com/paperclipai/paperclip/tree/master/skills/${skill.slug}`, }; } if (skill.sourceType === "github") { const owner = asString(metadata?.owner); const repo = asString(metadata?.repo); const repoSkillDir = asString(metadata?.repoSkillDir); if (!owner || !repo || !repoSkillDir) return null; return { kind: "github-dir", repo: `${owner}/${repo}`, path: repoSkillDir, commit: skill.sourceRef ?? null, trackingRef: asString(metadata?.trackingRef), url: skill.sourceLocator, }; } if (skill.sourceType === "url" && skill.sourceLocator) { return { kind: "url", url: skill.sourceLocator, }; } return null; } function shouldReferenceSkillOnExport(skill: CompanySkill, expandReferencedSkills: boolean) { if (expandReferencedSkills) return false; const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null; if (asString(metadata?.sourceKind) === "paperclip_bundled") return true; return skill.sourceType === "github" || skill.sourceType === "url"; } async function buildReferencedSkillMarkdown(skill: CompanySkill) { const sourceEntry = await buildSkillSourceEntry(skill); const frontmatter: Record = { key: skill.key, slug: skill.slug, name: skill.name, description: skill.description ?? null, }; if (sourceEntry) { frontmatter.metadata = { sources: [sourceEntry], }; } return buildMarkdown(frontmatter, ""); } async function withSkillSourceMetadata(skill: CompanySkill, markdown: string) { const sourceEntry = await buildSkillSourceEntry(skill); const parsed = parseFrontmatterMarkdown(markdown); const metadata = isPlainRecord(parsed.frontmatter.metadata) ? { ...parsed.frontmatter.metadata } : {}; const existingSources = Array.isArray(metadata.sources) ? metadata.sources.filter((entry) => isPlainRecord(entry)) : []; if (sourceEntry) { metadata.sources = [...existingSources, sourceEntry]; } metadata.skillKey = skill.key; metadata.paperclipSkillKey = skill.key; metadata.paperclip = { ...(isPlainRecord(metadata.paperclip) ? metadata.paperclip : {}), skillKey: skill.key, slug: skill.slug, }; const frontmatter = { ...parsed.frontmatter, key: skill.key, slug: skill.slug, metadata, }; return buildMarkdown(frontmatter, parsed.body); } function renderCompanyAgentsSection(agentSummaries: Array<{ slug: string; name: string }>) { const lines = ["# Agents", ""]; if (agentSummaries.length === 0) { lines.push("- _none_"); return lines.join("\n"); } for (const agent of agentSummaries) { lines.push(`- ${agent.slug} - ${agent.name}`); } return lines.join("\n"); } function parseYamlScalar(rawValue: string): unknown { const trimmed = rawValue.trim(); if (trimmed === "") return ""; if (trimmed === "null" || trimmed === "~") return null; if (trimmed === "true") return true; if (trimmed === "false") return false; if (trimmed === "[]") return []; if (trimmed === "{}") return {}; if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed); if ( trimmed.startsWith("\"") || trimmed.startsWith("[") || trimmed.startsWith("{") ) { try { return JSON.parse(trimmed); } catch { return trimmed; } } return trimmed; } function prepareYamlLines(raw: string) { return raw .split("\n") .map((line) => ({ indent: line.match(/^ */)?.[0].length ?? 0, content: line.trim(), })) .filter((line) => line.content.length > 0 && !line.content.startsWith("#")); } function parseYamlBlock( lines: Array<{ indent: number; content: string }>, startIndex: number, indentLevel: number, ): { value: unknown; nextIndex: number } { let index = startIndex; while (index < lines.length && lines[index]!.content.length === 0) { index += 1; } if (index >= lines.length || lines[index]!.indent < indentLevel) { return { value: {}, nextIndex: index }; } const isArray = lines[index]!.indent === indentLevel && lines[index]!.content.startsWith("-"); if (isArray) { const values: unknown[] = []; while (index < lines.length) { const line = lines[index]!; if (line.indent < indentLevel) break; if (line.indent !== indentLevel || !line.content.startsWith("-")) break; const remainder = line.content.slice(1).trim(); index += 1; if (!remainder) { const nested = parseYamlBlock(lines, index, indentLevel + 2); values.push(nested.value); index = nested.nextIndex; continue; } const inlineObjectSeparator = remainder.indexOf(":"); if ( inlineObjectSeparator > 0 && !remainder.startsWith("\"") && !remainder.startsWith("{") && !remainder.startsWith("[") ) { const key = remainder.slice(0, inlineObjectSeparator).trim(); const rawValue = remainder.slice(inlineObjectSeparator + 1).trim(); const nextObject: Record = { [key]: parseYamlScalar(rawValue), }; if (index < lines.length && lines[index]!.indent > indentLevel) { const nested = parseYamlBlock(lines, index, indentLevel + 2); if (isPlainRecord(nested.value)) { Object.assign(nextObject, nested.value); } index = nested.nextIndex; } values.push(nextObject); continue; } values.push(parseYamlScalar(remainder)); } return { value: values, nextIndex: index }; } const record: Record = {}; while (index < lines.length) { const line = lines[index]!; if (line.indent < indentLevel) break; if (line.indent !== indentLevel) { index += 1; continue; } const separatorIndex = line.content.indexOf(":"); if (separatorIndex <= 0) { index += 1; continue; } const key = line.content.slice(0, separatorIndex).trim(); const remainder = line.content.slice(separatorIndex + 1).trim(); index += 1; if (!remainder) { const nested = parseYamlBlock(lines, index, indentLevel + 2); record[key] = nested.value; index = nested.nextIndex; continue; } record[key] = parseYamlScalar(remainder); } return { value: record, nextIndex: index }; } function parseYamlFrontmatter(raw: string): Record { const prepared = prepareYamlLines(raw); if (prepared.length === 0) return {}; const parsed = parseYamlBlock(prepared, 0, prepared[0]!.indent); return isPlainRecord(parsed.value) ? parsed.value : {}; } function parseYamlFile(raw: string): Record { return parseYamlFrontmatter(raw); } function buildYamlFile(value: Record, opts?: { preserveEmptyStrings?: boolean }) { const cleaned = stripEmptyValues(value, opts); if (!isPlainRecord(cleaned)) return "{}\n"; return renderYamlBlock(cleaned, 0).join("\n") + "\n"; } function parseFrontmatterMarkdown(raw: string): MarkdownDoc { const normalized = raw.replace(/\r\n/g, "\n"); if (!normalized.startsWith("---\n")) { return { frontmatter: {}, body: normalized.trim() }; } const closing = normalized.indexOf("\n---\n", 4); if (closing < 0) { return { frontmatter: {}, body: normalized.trim() }; } const frontmatterRaw = normalized.slice(4, closing).trim(); const body = normalized.slice(closing + 5).trim(); return { frontmatter: parseYamlFrontmatter(frontmatterRaw), body, }; } async function fetchText(url: string) { const response = await fetch(url); if (!response.ok) { throw unprocessable(`Failed to fetch ${url}: ${response.status}`); } return response.text(); } async function fetchOptionalText(url: string) { const response = await fetch(url); if (response.status === 404) return null; if (!response.ok) { throw unprocessable(`Failed to fetch ${url}: ${response.status}`); } return response.text(); } async function fetchJson(url: string): Promise { const response = await fetch(url, { headers: { accept: "application/vnd.github+json", }, }); if (!response.ok) { throw unprocessable(`Failed to fetch ${url}: ${response.status}`); } return response.json() as Promise; } function dedupeEnvInputs(values: CompanyPortabilityManifest["envInputs"]) { const seen = new Set(); const out: CompanyPortabilityManifest["envInputs"] = []; for (const value of values) { const key = `${value.agentSlug ?? ""}:${value.key.toUpperCase()}`; if (seen.has(key)) continue; seen.add(key); out.push(value); } return out; } function buildEnvInputMap(inputs: CompanyPortabilityEnvInput[]) { const env: Record> = {}; for (const input of inputs) { const entry: Record = { kind: input.kind, requirement: input.requirement, }; if (input.defaultValue !== null) entry.default = input.defaultValue; if (input.description) entry.description = input.description; if (input.portability === "system_dependent") entry.portability = "system_dependent"; env[input.key] = entry; } return env; } function readCompanyApprovalDefault(_frontmatter: Record) { return true; } function readIncludeEntries(frontmatter: Record): CompanyPackageIncludeEntry[] { const includes = frontmatter.includes; if (!Array.isArray(includes)) return []; return includes.flatMap((entry) => { if (typeof entry === "string") { return [{ path: entry }]; } if (isPlainRecord(entry)) { const pathValue = asString(entry.path); return pathValue ? [{ path: pathValue }] : []; } return []; }); } function readAgentEnvInputs( extension: Record, agentSlug: string, ): CompanyPortabilityManifest["envInputs"] { const inputs = isPlainRecord(extension.inputs) ? extension.inputs : null; const env = inputs && isPlainRecord(inputs.env) ? inputs.env : null; if (!env) return []; return Object.entries(env).flatMap(([key, value]) => { if (!isPlainRecord(value)) return []; const record = value as EnvInputRecord; return [{ key, description: asString(record.description) ?? null, agentSlug, kind: record.kind === "plain" ? "plain" : "secret", requirement: record.requirement === "required" ? "required" : "optional", defaultValue: typeof record.default === "string" ? record.default : null, portability: record.portability === "system_dependent" ? "system_dependent" : "portable", }]; }); } function readAgentSkillRefs(frontmatter: Record) { const skills = frontmatter.skills; if (!Array.isArray(skills)) return []; return Array.from(new Set( skills .filter((entry): entry is string => typeof entry === "string") .map((entry) => normalizeSkillKey(entry) ?? entry.trim()) .filter(Boolean), )); } function buildManifestFromPackageFiles( files: Record, opts?: { sourceLabel?: { companyId: string; companyName: string } | null }, ): ResolvedSource { const normalizedFiles = normalizeFileMap(files); const companyPath = normalizedFiles["COMPANY.md"] ?? undefined; const resolvedCompanyPath = companyPath !== undefined ? "COMPANY.md" : Object.keys(normalizedFiles).find((entry) => entry.endsWith("/COMPANY.md") || entry === "COMPANY.md"); if (!resolvedCompanyPath) { throw unprocessable("Company package is missing COMPANY.md"); } const companyDoc = parseFrontmatterMarkdown(normalizedFiles[resolvedCompanyPath]!); const companyFrontmatter = companyDoc.frontmatter; const paperclipExtensionPath = findPaperclipExtensionPath(normalizedFiles); const paperclipExtension = paperclipExtensionPath ? parseYamlFile(normalizedFiles[paperclipExtensionPath] ?? "") : {}; const paperclipCompany = isPlainRecord(paperclipExtension.company) ? paperclipExtension.company : {}; const paperclipAgents = isPlainRecord(paperclipExtension.agents) ? paperclipExtension.agents : {}; const paperclipProjects = isPlainRecord(paperclipExtension.projects) ? paperclipExtension.projects : {}; const paperclipTasks = isPlainRecord(paperclipExtension.tasks) ? paperclipExtension.tasks : {}; const companyName = asString(companyFrontmatter.name) ?? opts?.sourceLabel?.companyName ?? "Imported Company"; const companySlug = asString(companyFrontmatter.slug) ?? normalizeAgentUrlKey(companyName) ?? "company"; const includeEntries = readIncludeEntries(companyFrontmatter); const referencedAgentPaths = includeEntries .map((entry) => resolvePortablePath(resolvedCompanyPath, entry.path)) .filter((entry) => entry.endsWith("/AGENTS.md") || entry === "AGENTS.md"); const referencedProjectPaths = includeEntries .map((entry) => resolvePortablePath(resolvedCompanyPath, entry.path)) .filter((entry) => entry.endsWith("/PROJECT.md") || entry === "PROJECT.md"); const referencedTaskPaths = includeEntries .map((entry) => resolvePortablePath(resolvedCompanyPath, entry.path)) .filter((entry) => entry.endsWith("/TASK.md") || entry === "TASK.md"); const referencedSkillPaths = includeEntries .map((entry) => resolvePortablePath(resolvedCompanyPath, entry.path)) .filter((entry) => entry.endsWith("/SKILL.md") || entry === "SKILL.md"); const discoveredAgentPaths = Object.keys(normalizedFiles).filter( (entry) => entry.endsWith("/AGENTS.md") || entry === "AGENTS.md", ); const discoveredProjectPaths = Object.keys(normalizedFiles).filter( (entry) => entry.endsWith("/PROJECT.md") || entry === "PROJECT.md", ); const discoveredTaskPaths = Object.keys(normalizedFiles).filter( (entry) => entry.endsWith("/TASK.md") || entry === "TASK.md", ); const discoveredSkillPaths = Object.keys(normalizedFiles).filter( (entry) => entry.endsWith("/SKILL.md") || entry === "SKILL.md", ); const agentPaths = Array.from(new Set([...referencedAgentPaths, ...discoveredAgentPaths])).sort(); const projectPaths = Array.from(new Set([...referencedProjectPaths, ...discoveredProjectPaths])).sort(); const taskPaths = Array.from(new Set([...referencedTaskPaths, ...discoveredTaskPaths])).sort(); const skillPaths = Array.from(new Set([...referencedSkillPaths, ...discoveredSkillPaths])).sort(); const manifest: CompanyPortabilityManifest = { schemaVersion: 3, generatedAt: new Date().toISOString(), source: opts?.sourceLabel ?? null, includes: { company: true, agents: true, projects: projectPaths.length > 0, issues: taskPaths.length > 0, }, company: { path: resolvedCompanyPath, name: companyName, description: asString(companyFrontmatter.description), brandColor: asString(paperclipCompany.brandColor), requireBoardApprovalForNewAgents: typeof paperclipCompany.requireBoardApprovalForNewAgents === "boolean" ? paperclipCompany.requireBoardApprovalForNewAgents : readCompanyApprovalDefault(companyFrontmatter), }, agents: [], skills: [], projects: [], issues: [], envInputs: [], }; const warnings: string[] = []; for (const agentPath of agentPaths) { const markdownRaw = normalizedFiles[agentPath]; if (typeof markdownRaw !== "string") { warnings.push(`Referenced agent file is missing from package: ${agentPath}`); continue; } const agentDoc = parseFrontmatterMarkdown(markdownRaw); const frontmatter = agentDoc.frontmatter; const fallbackSlug = normalizeAgentUrlKey(path.posix.basename(path.posix.dirname(agentPath))) ?? "agent"; const slug = asString(frontmatter.slug) ?? fallbackSlug; const extension = isPlainRecord(paperclipAgents[slug]) ? paperclipAgents[slug] : {}; const extensionAdapter = isPlainRecord(extension.adapter) ? extension.adapter : null; const extensionRuntime = isPlainRecord(extension.runtime) ? extension.runtime : null; const extensionPermissions = isPlainRecord(extension.permissions) ? extension.permissions : null; const extensionMetadata = isPlainRecord(extension.metadata) ? extension.metadata : null; const adapterConfig = isPlainRecord(extensionAdapter?.config) ? extensionAdapter.config : {}; const runtimeConfig = extensionRuntime ?? {}; const title = asString(frontmatter.title); manifest.agents.push({ slug, name: asString(frontmatter.name) ?? title ?? slug, path: agentPath, skills: readAgentSkillRefs(frontmatter), role: asString(extension.role) ?? "agent", title, icon: asString(extension.icon), capabilities: asString(extension.capabilities), reportsToSlug: asString(frontmatter.reportsTo) ?? asString(extension.reportsTo), adapterType: asString(extensionAdapter?.type) ?? "process", adapterConfig, runtimeConfig, permissions: extensionPermissions ?? {}, budgetMonthlyCents: typeof extension.budgetMonthlyCents === "number" && Number.isFinite(extension.budgetMonthlyCents) ? Math.max(0, Math.floor(extension.budgetMonthlyCents)) : 0, metadata: extensionMetadata, }); manifest.envInputs.push(...readAgentEnvInputs(extension, slug)); if (frontmatter.kind && frontmatter.kind !== "agent") { warnings.push(`Agent markdown ${agentPath} does not declare kind: agent in frontmatter.`); } } for (const skillPath of skillPaths) { const markdownRaw = normalizedFiles[skillPath]; if (typeof markdownRaw !== "string") { warnings.push(`Referenced skill file is missing from package: ${skillPath}`); continue; } const skillDoc = parseFrontmatterMarkdown(markdownRaw); const frontmatter = skillDoc.frontmatter; const skillDir = path.posix.dirname(skillPath); const fallbackSlug = normalizeAgentUrlKey(path.posix.basename(skillDir)) ?? "skill"; const slug = asString(frontmatter.slug) ?? normalizeAgentUrlKey(asString(frontmatter.name) ?? "") ?? fallbackSlug; const inventory = Object.keys(normalizedFiles) .filter((entry) => entry === skillPath || entry.startsWith(`${skillDir}/`)) .map((entry) => ({ path: entry === skillPath ? "SKILL.md" : entry.slice(skillDir.length + 1), kind: entry === skillPath ? "skill" : entry.startsWith(`${skillDir}/references/`) ? "reference" : entry.startsWith(`${skillDir}/scripts/`) ? "script" : entry.startsWith(`${skillDir}/assets/`) ? "asset" : entry.endsWith(".md") ? "markdown" : "other", })); const metadata = isPlainRecord(frontmatter.metadata) ? frontmatter.metadata : null; const sources = metadata && Array.isArray(metadata.sources) ? metadata.sources : []; const primarySource = sources.find((entry) => isPlainRecord(entry)) as Record | undefined; const sourceKind = asString(primarySource?.kind); let sourceType = "catalog"; let sourceLocator: string | null = null; let sourceRef: string | null = null; let normalizedMetadata: Record | null = null; if (sourceKind === "github-dir" || sourceKind === "github-file") { const repo = asString(primarySource?.repo); const repoPath = asString(primarySource?.path); const commit = asString(primarySource?.commit); const trackingRef = asString(primarySource?.trackingRef); const [owner, repoName] = (repo ?? "").split("/"); sourceType = "github"; sourceLocator = asString(primarySource?.url) ?? (repo ? `https://github.com/${repo}${repoPath ? `/tree/${trackingRef ?? commit ?? "main"}/${repoPath}` : ""}` : null); sourceRef = commit; normalizedMetadata = owner && repoName ? { sourceKind: "github", owner, repo: repoName, ref: commit, trackingRef, repoSkillDir: repoPath ?? `skills/${slug}`, } : null; } else if (sourceKind === "url") { sourceType = "url"; sourceLocator = asString(primarySource?.url) ?? asString(primarySource?.rawUrl); normalizedMetadata = { sourceKind: "url", }; } else if (metadata) { normalizedMetadata = { sourceKind: "catalog", }; } const key = deriveManifestSkillKey(frontmatter, slug, normalizedMetadata, sourceType, sourceLocator); manifest.skills.push({ key, slug, name: asString(frontmatter.name) ?? slug, path: skillPath, description: asString(frontmatter.description), sourceType, sourceLocator, sourceRef, trustLevel: null, compatibility: "compatible", metadata: normalizedMetadata, fileInventory: inventory, }); } for (const projectPath of projectPaths) { const markdownRaw = normalizedFiles[projectPath]; if (typeof markdownRaw !== "string") { warnings.push(`Referenced project file is missing from package: ${projectPath}`); continue; } const projectDoc = parseFrontmatterMarkdown(markdownRaw); const frontmatter = projectDoc.frontmatter; const fallbackSlug = deriveProjectUrlKey( asString(frontmatter.name) ?? path.posix.basename(path.posix.dirname(projectPath)) ?? "project", projectPath, ); const slug = asString(frontmatter.slug) ?? fallbackSlug; const extension = isPlainRecord(paperclipProjects[slug]) ? paperclipProjects[slug] : {}; manifest.projects.push({ slug, name: asString(frontmatter.name) ?? slug, path: projectPath, description: asString(frontmatter.description), ownerAgentSlug: asString(frontmatter.owner), leadAgentSlug: asString(extension.leadAgentSlug), targetDate: asString(extension.targetDate), color: asString(extension.color), status: asString(extension.status), executionWorkspacePolicy: isPlainRecord(extension.executionWorkspacePolicy) ? extension.executionWorkspacePolicy : null, metadata: isPlainRecord(extension.metadata) ? extension.metadata : null, }); if (frontmatter.kind && frontmatter.kind !== "project") { warnings.push(`Project markdown ${projectPath} does not declare kind: project in frontmatter.`); } } for (const taskPath of taskPaths) { const markdownRaw = normalizedFiles[taskPath]; if (typeof markdownRaw !== "string") { warnings.push(`Referenced task file is missing from package: ${taskPath}`); continue; } const taskDoc = parseFrontmatterMarkdown(markdownRaw); const frontmatter = taskDoc.frontmatter; const fallbackSlug = normalizeAgentUrlKey(path.posix.basename(path.posix.dirname(taskPath))) ?? "task"; const slug = asString(frontmatter.slug) ?? fallbackSlug; const extension = isPlainRecord(paperclipTasks[slug]) ? paperclipTasks[slug] : {}; const schedule = isPlainRecord(frontmatter.schedule) ? frontmatter.schedule : null; const recurrence = schedule && isPlainRecord(schedule.recurrence) ? schedule.recurrence : isPlainRecord(extension.recurrence) ? extension.recurrence : null; manifest.issues.push({ slug, identifier: asString(extension.identifier), title: asString(frontmatter.name) ?? asString(frontmatter.title) ?? slug, path: taskPath, projectSlug: asString(frontmatter.project), assigneeAgentSlug: asString(frontmatter.assignee), description: taskDoc.body || asString(frontmatter.description), recurrence, status: asString(extension.status), priority: asString(extension.priority), labelIds: Array.isArray(extension.labelIds) ? extension.labelIds.filter((entry): entry is string => typeof entry === "string") : [], billingCode: asString(extension.billingCode), executionWorkspaceSettings: isPlainRecord(extension.executionWorkspaceSettings) ? extension.executionWorkspaceSettings : null, assigneeAdapterOverrides: isPlainRecord(extension.assigneeAdapterOverrides) ? extension.assigneeAdapterOverrides : null, metadata: isPlainRecord(extension.metadata) ? extension.metadata : null, }); if (frontmatter.kind && frontmatter.kind !== "task") { warnings.push(`Task markdown ${taskPath} does not declare kind: task in frontmatter.`); } } manifest.envInputs = dedupeEnvInputs(manifest.envInputs); return { manifest, files: normalizedFiles, warnings, }; } function parseGitHubSourceUrl(rawUrl: string) { const url = new URL(rawUrl); if (url.hostname !== "github.com") { throw unprocessable("GitHub source must use github.com URL"); } const parts = url.pathname.split("/").filter(Boolean); if (parts.length < 2) { throw unprocessable("Invalid GitHub URL"); } const owner = parts[0]!; const repo = parts[1]!.replace(/\.git$/i, ""); let ref = "main"; let basePath = ""; let companyPath = "COMPANY.md"; if (parts[2] === "tree") { ref = parts[3] ?? "main"; basePath = parts.slice(4).join("/"); } else if (parts[2] === "blob") { ref = parts[3] ?? "main"; const blobPath = parts.slice(4).join("/"); if (!blobPath) { throw unprocessable("Invalid GitHub blob URL"); } companyPath = blobPath; basePath = path.posix.dirname(blobPath); if (basePath === ".") basePath = ""; } return { owner, repo, ref, basePath, companyPath }; } function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: string) { const normalizedFilePath = filePath.replace(/^\/+/, ""); return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${normalizedFilePath}`; } export function companyPortabilityService(db: Db) { const companies = companyService(db); const agents = agentService(db); const instructions = agentInstructionsService(); const access = accessService(db); const projects = projectService(db); const issues = issueService(db); const companySkills = companySkillService(db); async function resolveSource(source: CompanyPortabilityPreview["source"]): Promise { if (source.type === "inline") { return buildManifestFromPackageFiles( normalizeFileMap(source.files, source.rootPath), ); } const parsed = parseGitHubSourceUrl(source.url); let ref = parsed.ref; const warnings: string[] = []; const companyRelativePath = parsed.companyPath === "COMPANY.md" ? [parsed.basePath, "COMPANY.md"].filter(Boolean).join("/") : parsed.companyPath; let companyMarkdown: string | null = null; try { companyMarkdown = await fetchOptionalText( resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, companyRelativePath), ); } catch (err) { if (ref === "main") { ref = "master"; warnings.push("GitHub ref main not found; falling back to master."); companyMarkdown = await fetchOptionalText( resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, companyRelativePath), ); } else { throw err; } } if (!companyMarkdown) { throw unprocessable("GitHub company package is missing COMPANY.md"); } const companyPath = parsed.companyPath === "COMPANY.md" ? "COMPANY.md" : normalizePortablePath(path.posix.relative(parsed.basePath || ".", parsed.companyPath)); const files: Record = { [companyPath]: companyMarkdown, }; const tree = await fetchJson<{ tree?: Array<{ path: string; type: string }> }>( `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/git/trees/${ref}?recursive=1`, ).catch(() => ({ tree: [] })); const basePrefix = parsed.basePath ? `${parsed.basePath.replace(/^\/+|\/+$/g, "")}/` : ""; const candidatePaths = (tree.tree ?? []) .filter((entry) => entry.type === "blob") .map((entry) => entry.path) .filter((entry): entry is string => typeof entry === "string") .filter((entry) => { if (basePrefix && !entry.startsWith(basePrefix)) return false; const relative = basePrefix ? entry.slice(basePrefix.length) : entry; return ( relative.endsWith(".md") || relative.startsWith("skills/") || relative === ".paperclip.yaml" || relative === ".paperclip.yml" ); }); for (const repoPath of candidatePaths) { const relativePath = basePrefix ? repoPath.slice(basePrefix.length) : repoPath; if (files[relativePath] !== undefined) continue; files[normalizePortablePath(relativePath)] = await fetchText( resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, repoPath), ); } const companyDoc = parseFrontmatterMarkdown(companyMarkdown); const includeEntries = readIncludeEntries(companyDoc.frontmatter); for (const includeEntry of includeEntries) { const repoPath = [parsed.basePath, includeEntry.path].filter(Boolean).join("/"); const relativePath = normalizePortablePath(includeEntry.path); if (files[relativePath] !== undefined) continue; if (!(repoPath.endsWith(".md") || repoPath.endsWith(".yaml") || repoPath.endsWith(".yml"))) continue; files[relativePath] = await fetchText( resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, repoPath), ); } const resolved = buildManifestFromPackageFiles(files); resolved.warnings.unshift(...warnings); return resolved; } async function exportBundle( companyId: string, input: CompanyPortabilityExport, ): Promise { const include = normalizeInclude({ ...input.include, projects: input.projects && input.projects.length > 0 ? true : input.include?.projects, issues: (input.issues && input.issues.length > 0) || (input.projectIssues && input.projectIssues.length > 0) ? true : input.include?.issues, }); const company = await companies.getById(companyId); if (!company) throw notFound("Company not found"); const files: Record = {}; const warnings: string[] = []; const envInputs: CompanyPortabilityManifest["envInputs"] = []; const rootPath = normalizeAgentUrlKey(company.name) ?? "company-package"; const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : []; const agentRows = allAgentRows.filter((agent) => agent.status !== "terminated"); const companySkillRows = await companySkills.listFull(companyId); if (include.agents) { const skipped = allAgentRows.length - agentRows.length; if (skipped > 0) { warnings.push(`Skipped ${skipped} terminated agent${skipped === 1 ? "" : "s"} from export.`); } } const usedSlugs = new Set(); const idToSlug = new Map(); for (const agent of agentRows) { const baseSlug = toSafeSlug(agent.name, "agent"); const slug = uniqueSlug(baseSlug, usedSlugs); idToSlug.set(agent.id, slug); } const projectsSvc = projectService(db); const issuesSvc = issueService(db); const allProjectsRaw = include.projects || include.issues ? await projectsSvc.list(companyId) : []; const allProjects = allProjectsRaw.filter((project) => !project.archivedAt); if (include.projects) { const skippedArchived = allProjectsRaw.length - allProjects.length; if (skippedArchived > 0) { warnings.push(`Skipped ${skippedArchived} archived project${skippedArchived === 1 ? "" : "s"} from export.`); } } const projectById = new Map(allProjects.map((project) => [project.id, project])); const projectByReference = new Map(); for (const project of allProjects) { projectByReference.set(project.id, project); projectByReference.set(project.urlKey, project); } const selectedProjects = new Map(); const normalizeProjectSelector = (selector: string) => selector.trim().toLowerCase(); for (const selector of input.projects ?? []) { const match = projectByReference.get(selector) ?? projectByReference.get(normalizeProjectSelector(selector)); if (!match) { warnings.push(`Project selector "${selector}" was not found and was skipped.`); continue; } selectedProjects.set(match.id, match); } const selectedIssues = new Map>>(); const resolveIssueBySelector = async (selector: string) => { const trimmed = selector.trim(); if (!trimmed) return null; return trimmed.includes("-") ? issuesSvc.getByIdentifier(trimmed) : issuesSvc.getById(trimmed); }; for (const selector of input.issues ?? []) { const issue = await resolveIssueBySelector(selector); if (!issue || issue.companyId !== companyId) { warnings.push(`Issue selector "${selector}" was not found and was skipped.`); continue; } selectedIssues.set(issue.id, issue); if (issue.projectId) { const parentProject = projectById.get(issue.projectId); if (parentProject) selectedProjects.set(parentProject.id, parentProject); } } for (const selector of input.projectIssues ?? []) { const match = projectByReference.get(selector) ?? projectByReference.get(normalizeProjectSelector(selector)); if (!match) { warnings.push(`Project-issues selector "${selector}" was not found and was skipped.`); continue; } selectedProjects.set(match.id, match); const projectIssues = await issuesSvc.list(companyId, { projectId: match.id }); for (const issue of projectIssues) { selectedIssues.set(issue.id, issue); } } if (include.projects && selectedProjects.size === 0) { for (const project of allProjects) { selectedProjects.set(project.id, project); } } if (include.issues && selectedIssues.size === 0) { const allIssues = await issuesSvc.list(companyId); for (const issue of allIssues) { selectedIssues.set(issue.id, issue); if (issue.projectId) { const parentProject = projectById.get(issue.projectId); if (parentProject) selectedProjects.set(parentProject.id, parentProject); } } } const selectedProjectRows = Array.from(selectedProjects.values()) .sort((left, right) => left.name.localeCompare(right.name)); const selectedIssueRows = Array.from(selectedIssues.values()) .filter((issue): issue is NonNullable => issue != null) .sort((left, right) => (left.identifier ?? left.title).localeCompare(right.identifier ?? right.title)); const taskSlugByIssueId = new Map(); const usedTaskSlugs = new Set(); for (const issue of selectedIssueRows) { const baseSlug = normalizeAgentUrlKey(issue.identifier ?? issue.title) ?? "task"; taskSlugByIssueId.set(issue.id, uniqueSlug(baseSlug, usedTaskSlugs)); } const projectSlugById = new Map(); const usedProjectSlugs = new Set(); for (const project of selectedProjectRows) { const baseSlug = deriveProjectUrlKey(project.name, project.name); projectSlugById.set(project.id, uniqueSlug(baseSlug, usedProjectSlugs)); } const companyPath = "COMPANY.md"; const companyBodySections: string[] = []; if (include.agents) { const companyAgentSummaries = agentRows.map((agent) => ({ slug: idToSlug.get(agent.id) ?? "agent", name: agent.name, })); companyBodySections.push(renderCompanyAgentsSection(companyAgentSummaries)); } if (selectedProjectRows.length > 0) { companyBodySections.push( ["# Projects", "", ...selectedProjectRows.map((project) => `- ${projectSlugById.get(project.id) ?? project.id} - ${project.name}`)].join("\n"), ); } files[companyPath] = buildMarkdown( { name: company.name, description: company.description ?? null, schema: "agentcompanies/v1", slug: rootPath, }, companyBodySections.join("\n\n").trim(), ); const paperclipAgentsOut: Record> = {}; const paperclipProjectsOut: Record> = {}; const paperclipTasksOut: Record> = {}; const skillExportDirs = buildSkillExportDirMap(companySkillRows, company.issuePrefix); for (const skill of companySkillRows) { const packageDir = skillExportDirs.get(skill.key) ?? `skills/${normalizeSkillSlug(skill.slug) ?? "skill"}`; if (shouldReferenceSkillOnExport(skill, Boolean(input.expandReferencedSkills))) { files[`${packageDir}/SKILL.md`] = await buildReferencedSkillMarkdown(skill); continue; } for (const inventoryEntry of skill.fileInventory) { const fileDetail = await companySkills.readFile(companyId, skill.id, inventoryEntry.path).catch(() => null); if (!fileDetail) continue; const filePath = `${packageDir}/${inventoryEntry.path}`; files[filePath] = inventoryEntry.path === "SKILL.md" ? await withSkillSourceMetadata(skill, fileDetail.content) : fileDetail.content; } } if (include.agents) { for (const agent of agentRows) { const slug = idToSlug.get(agent.id)!; const exportedInstructions = await instructions.exportFiles(agent); warnings.push(...exportedInstructions.warnings); const envInputsStart = envInputs.length; const exportedEnvInputs = extractPortableEnvInputs( slug, (agent.adapterConfig as Record).env, warnings, ); envInputs.push(...exportedEnvInputs); const adapterDefaultRules = ADAPTER_DEFAULT_RULES_BY_TYPE[agent.adapterType] ?? []; const portableAdapterConfig = pruneDefaultLikeValue( normalizePortableConfig(agent.adapterConfig), { dropFalseBooleans: true, defaultRules: adapterDefaultRules, }, ) as Record; const portableRuntimeConfig = pruneDefaultLikeValue( normalizePortableConfig(agent.runtimeConfig), { dropFalseBooleans: true, defaultRules: RUNTIME_DEFAULT_RULES, }, ) as Record; const portablePermissions = pruneDefaultLikeValue(agent.permissions ?? {}, { dropFalseBooleans: true }) as Record; const agentEnvInputs = dedupeEnvInputs( envInputs .slice(envInputsStart) .filter((inputValue) => inputValue.agentSlug === slug), ); const reportsToSlug = agent.reportsTo ? (idToSlug.get(agent.reportsTo) ?? null) : null; const desiredSkills = readPaperclipSkillSyncPreference( (agent.adapterConfig as Record) ?? {}, ).desiredSkills; const commandValue = asString(portableAdapterConfig.command); if (commandValue && isAbsoluteCommand(commandValue)) { warnings.push(`Agent ${slug} command ${commandValue} was omitted from export because it is system-dependent.`); delete portableAdapterConfig.command; } for (const [relativePath, content] of Object.entries(exportedInstructions.files)) { const targetPath = `agents/${slug}/${relativePath}`; if (relativePath === exportedInstructions.entryFile) { files[targetPath] = buildMarkdown( stripEmptyValues({ name: agent.name, title: agent.title ?? null, reportsTo: reportsToSlug, skills: desiredSkills.length > 0 ? desiredSkills : undefined, }) as Record, content, ); } else { files[targetPath] = content; } } const extension = stripEmptyValues({ role: agent.role !== "agent" ? agent.role : undefined, icon: agent.icon ?? null, capabilities: agent.capabilities ?? null, adapter: { type: agent.adapterType, config: portableAdapterConfig, }, runtime: portableRuntimeConfig, permissions: portablePermissions, budgetMonthlyCents: (agent.budgetMonthlyCents ?? 0) > 0 ? agent.budgetMonthlyCents : undefined, metadata: (agent.metadata as Record | null) ?? null, }); if (isPlainRecord(extension) && agentEnvInputs.length > 0) { extension.inputs = { env: buildEnvInputMap(agentEnvInputs), }; } paperclipAgentsOut[slug] = isPlainRecord(extension) ? extension : {}; } } for (const project of selectedProjectRows) { const slug = projectSlugById.get(project.id)!; const projectPath = `projects/${slug}/PROJECT.md`; files[projectPath] = buildMarkdown( { name: project.name, description: project.description ?? null, owner: project.leadAgentId ? (idToSlug.get(project.leadAgentId) ?? null) : null, }, project.description ?? "", ); const extension = stripEmptyValues({ leadAgentSlug: project.leadAgentId ? (idToSlug.get(project.leadAgentId) ?? null) : null, targetDate: project.targetDate ?? null, color: project.color ?? null, status: project.status, executionWorkspacePolicy: project.executionWorkspacePolicy ?? undefined, }); paperclipProjectsOut[slug] = isPlainRecord(extension) ? extension : {}; } for (const issue of selectedIssueRows) { const taskSlug = taskSlugByIssueId.get(issue.id)!; const projectSlug = issue.projectId ? (projectSlugById.get(issue.projectId) ?? null) : null; // All tasks go in top-level tasks/ folder, never nested under projects/ const taskPath = `tasks/${taskSlug}/TASK.md`; const assigneeSlug = issue.assigneeAgentId ? (idToSlug.get(issue.assigneeAgentId) ?? null) : null; files[taskPath] = buildMarkdown( { name: issue.title, project: projectSlug, assignee: assigneeSlug, }, issue.description ?? "", ); const extension = stripEmptyValues({ identifier: issue.identifier, status: issue.status, priority: issue.priority, labelIds: issue.labelIds ?? undefined, billingCode: issue.billingCode ?? null, executionWorkspaceSettings: issue.executionWorkspaceSettings ?? undefined, assigneeAdapterOverrides: issue.assigneeAdapterOverrides ?? undefined, }); paperclipTasksOut[taskSlug] = isPlainRecord(extension) ? extension : {}; } const paperclipExtensionPath = ".paperclip.yaml"; const paperclipAgents = Object.fromEntries( Object.entries(paperclipAgentsOut).filter(([, value]) => isPlainRecord(value) && Object.keys(value).length > 0), ); const paperclipProjects = Object.fromEntries( Object.entries(paperclipProjectsOut).filter(([, value]) => isPlainRecord(value) && Object.keys(value).length > 0), ); const paperclipTasks = Object.fromEntries( Object.entries(paperclipTasksOut).filter(([, value]) => isPlainRecord(value) && Object.keys(value).length > 0), ); files[paperclipExtensionPath] = buildYamlFile( { schema: "paperclip/v1", company: stripEmptyValues({ brandColor: company.brandColor ?? null, requireBoardApprovalForNewAgents: company.requireBoardApprovalForNewAgents ? undefined : false, }), agents: Object.keys(paperclipAgents).length > 0 ? paperclipAgents : undefined, projects: Object.keys(paperclipProjects).length > 0 ? paperclipProjects : undefined, tasks: Object.keys(paperclipTasks).length > 0 ? paperclipTasks : undefined, }, { preserveEmptyStrings: true }, ); const resolved = buildManifestFromPackageFiles(files, { sourceLabel: { companyId: company.id, companyName: company.name, }, }); resolved.manifest.includes = include; resolved.manifest.envInputs = dedupeEnvInputs(envInputs); resolved.warnings.unshift(...warnings); // Generate README.md with Mermaid org chart files["README.md"] = generateReadme(resolved.manifest, { companyName: company.name, companyDescription: company.description ?? null, }); return { rootPath, manifest: resolved.manifest, files, warnings: resolved.warnings, paperclipExtensionPath, }; } async function buildPreview(input: CompanyPortabilityPreview): Promise { const requestedInclude = normalizeInclude(input.include); const source = applySelectedFilesToSource(await resolveSource(input.source), input.selectedFiles); const manifest = source.manifest; const include: CompanyPortabilityInclude = { company: requestedInclude.company && manifest.company !== null, agents: requestedInclude.agents && manifest.agents.length > 0, projects: requestedInclude.projects && manifest.projects.length > 0, issues: requestedInclude.issues && manifest.issues.length > 0, }; const collisionStrategy = input.collisionStrategy ?? DEFAULT_COLLISION_STRATEGY; const warnings = [...source.warnings]; const errors: string[] = []; if (include.company && !manifest.company) { errors.push("Manifest does not include company metadata."); } const selectedSlugs = include.agents ? ( input.agents && input.agents !== "all" ? Array.from(new Set(input.agents)) : manifest.agents.map((agent) => agent.slug) ) : []; const selectedAgents = include.agents ? manifest.agents.filter((agent) => selectedSlugs.includes(agent.slug)) : []; const selectedMissing = selectedSlugs.filter((slug) => !manifest.agents.some((agent) => agent.slug === slug)); for (const missing of selectedMissing) { errors.push(`Selected agent slug not found in manifest: ${missing}`); } if (include.agents && selectedAgents.length === 0) { warnings.push("No agents selected for import."); } const availableSkillKeys = new Set(source.manifest.skills.map((skill) => skill.key)); const availableSkillSlugs = new Map(); for (const skill of source.manifest.skills) { const existing = availableSkillSlugs.get(skill.slug) ?? []; existing.push(skill); availableSkillSlugs.set(skill.slug, existing); } for (const agent of selectedAgents) { const filePath = ensureMarkdownPath(agent.path); const markdown = source.files[filePath]; if (typeof markdown !== "string") { errors.push(`Missing markdown file for agent ${agent.slug}: ${filePath}`); continue; } const parsed = parseFrontmatterMarkdown(markdown); if (parsed.frontmatter.kind && parsed.frontmatter.kind !== "agent") { warnings.push(`Agent markdown ${filePath} does not declare kind: agent in frontmatter.`); } for (const skillRef of agent.skills) { const slugMatches = availableSkillSlugs.get(skillRef) ?? []; if (!availableSkillKeys.has(skillRef) && slugMatches.length !== 1) { warnings.push(`Agent ${agent.slug} references skill ${skillRef}, but that skill is not present in the package.`); } } } if (include.projects) { for (const project of manifest.projects) { const markdown = source.files[ensureMarkdownPath(project.path)]; if (typeof markdown !== "string") { errors.push(`Missing markdown file for project ${project.slug}: ${project.path}`); continue; } const parsed = parseFrontmatterMarkdown(markdown); if (parsed.frontmatter.kind && parsed.frontmatter.kind !== "project") { warnings.push(`Project markdown ${project.path} does not declare kind: project in frontmatter.`); } } } if (include.issues) { for (const issue of manifest.issues) { const markdown = source.files[ensureMarkdownPath(issue.path)]; if (typeof markdown !== "string") { errors.push(`Missing markdown file for task ${issue.slug}: ${issue.path}`); continue; } const parsed = parseFrontmatterMarkdown(markdown); if (parsed.frontmatter.kind && parsed.frontmatter.kind !== "task") { warnings.push(`Task markdown ${issue.path} does not declare kind: task in frontmatter.`); } if (issue.recurrence) { warnings.push(`Task ${issue.slug} has recurrence metadata; Paperclip will import it as a one-time issue for now.`); } } } for (const envInput of manifest.envInputs) { if (envInput.portability === "system_dependent") { warnings.push(`Environment input ${envInput.key}${envInput.agentSlug ? ` for ${envInput.agentSlug}` : ""} is system-dependent and may need manual adjustment after import.`); } } let targetCompanyId: string | null = null; let targetCompanyName: string | null = null; if (input.target.mode === "existing_company") { const targetCompany = await companies.getById(input.target.companyId); if (!targetCompany) throw notFound("Target company not found"); targetCompanyId = targetCompany.id; targetCompanyName = targetCompany.name; } const agentPlans: CompanyPortabilityPreviewAgentPlan[] = []; const existingSlugToAgent = new Map(); const existingSlugs = new Set(); const projectPlans: CompanyPortabilityPreviewResult["plan"]["projectPlans"] = []; const issuePlans: CompanyPortabilityPreviewResult["plan"]["issuePlans"] = []; const existingProjectSlugToProject = new Map(); const existingProjectSlugs = new Set(); if (input.target.mode === "existing_company") { const existingAgents = await agents.list(input.target.companyId); for (const existing of existingAgents) { const slug = normalizeAgentUrlKey(existing.name) ?? existing.id; if (!existingSlugToAgent.has(slug)) existingSlugToAgent.set(slug, existing); existingSlugs.add(slug); } const existingProjects = await projects.list(input.target.companyId); for (const existing of existingProjects) { if (!existingProjectSlugToProject.has(existing.urlKey)) { existingProjectSlugToProject.set(existing.urlKey, { id: existing.id, name: existing.name }); } existingProjectSlugs.add(existing.urlKey); } } for (const manifestAgent of selectedAgents) { const existing = existingSlugToAgent.get(manifestAgent.slug) ?? null; if (!existing) { agentPlans.push({ slug: manifestAgent.slug, action: "create", plannedName: manifestAgent.name, existingAgentId: null, reason: null, }); continue; } if (collisionStrategy === "replace") { agentPlans.push({ slug: manifestAgent.slug, action: "update", plannedName: existing.name, existingAgentId: existing.id, reason: "Existing slug matched; replace strategy.", }); continue; } if (collisionStrategy === "skip") { agentPlans.push({ slug: manifestAgent.slug, action: "skip", plannedName: existing.name, existingAgentId: existing.id, reason: "Existing slug matched; skip strategy.", }); continue; } const renamed = uniqueNameBySlug(manifestAgent.name, existingSlugs); existingSlugs.add(normalizeAgentUrlKey(renamed) ?? manifestAgent.slug); agentPlans.push({ slug: manifestAgent.slug, action: "create", plannedName: renamed, existingAgentId: existing.id, reason: "Existing slug matched; rename strategy.", }); } if (include.projects) { for (const manifestProject of manifest.projects) { const existing = existingProjectSlugToProject.get(manifestProject.slug) ?? null; if (!existing) { projectPlans.push({ slug: manifestProject.slug, action: "create", plannedName: manifestProject.name, existingProjectId: null, reason: null, }); continue; } if (collisionStrategy === "replace") { projectPlans.push({ slug: manifestProject.slug, action: "update", plannedName: existing.name, existingProjectId: existing.id, reason: "Existing slug matched; replace strategy.", }); continue; } if (collisionStrategy === "skip") { projectPlans.push({ slug: manifestProject.slug, action: "skip", plannedName: existing.name, existingProjectId: existing.id, reason: "Existing slug matched; skip strategy.", }); continue; } const renamed = uniqueProjectName(manifestProject.name, existingProjectSlugs); existingProjectSlugs.add(deriveProjectUrlKey(renamed, renamed)); projectPlans.push({ slug: manifestProject.slug, action: "create", plannedName: renamed, existingProjectId: existing.id, reason: "Existing slug matched; rename strategy.", }); } } // Apply user-specified name overrides (keyed by slug) if (input.nameOverrides) { for (const ap of agentPlans) { const override = input.nameOverrides[ap.slug]; if (override) { ap.plannedName = override; } } for (const pp of projectPlans) { const override = input.nameOverrides[pp.slug]; if (override) { pp.plannedName = override; } } for (const ip of issuePlans) { const override = input.nameOverrides[ip.slug]; if (override) { ip.plannedTitle = override; } } } // Warn about agents that will be overwritten/updated for (const ap of agentPlans) { if (ap.action === "update") { warnings.push(`Existing agent "${ap.plannedName}" (${ap.slug}) will be overwritten by import.`); } } // Warn about projects that will be overwritten/updated for (const pp of projectPlans) { if (pp.action === "update") { warnings.push(`Existing project "${pp.plannedName}" (${pp.slug}) will be overwritten by import.`); } } if (include.issues) { for (const manifestIssue of manifest.issues) { issuePlans.push({ slug: manifestIssue.slug, action: "create", plannedTitle: manifestIssue.title, reason: manifestIssue.recurrence ? "Recurrence will not be activated on import." : null, }); } } const preview: CompanyPortabilityPreviewResult = { include, targetCompanyId, targetCompanyName, collisionStrategy, selectedAgentSlugs: selectedAgents.map((agent) => agent.slug), plan: { companyAction: input.target.mode === "new_company" ? "create" : include.company ? "update" : "none", agentPlans, projectPlans, issuePlans, }, manifest, files: source.files, envInputs: manifest.envInputs ?? [], warnings, errors, }; return { preview, source, include, collisionStrategy, selectedAgents, }; } async function previewImport(input: CompanyPortabilityPreview): Promise { const plan = await buildPreview(input); return plan.preview; } async function importBundle( input: CompanyPortabilityImport, actorUserId: string | null | undefined, ): Promise { const plan = await buildPreview(input); if (plan.preview.errors.length > 0) { throw unprocessable(`Import preview has errors: ${plan.preview.errors.join("; ")}`); } const sourceManifest = plan.source.manifest; const warnings = [...plan.preview.warnings]; const include = plan.include; let targetCompany: { id: string; name: string } | null = null; let companyAction: "created" | "updated" | "unchanged" = "unchanged"; if (input.target.mode === "new_company") { const companyName = asString(input.target.newCompanyName) ?? sourceManifest.company?.name ?? sourceManifest.source?.companyName ?? "Imported Company"; const created = await companies.create({ name: companyName, description: include.company ? (sourceManifest.company?.description ?? null) : null, brandColor: include.company ? (sourceManifest.company?.brandColor ?? null) : null, requireBoardApprovalForNewAgents: include.company ? (sourceManifest.company?.requireBoardApprovalForNewAgents ?? true) : true, }); await access.ensureMembership(created.id, "user", actorUserId ?? "board", "owner", "active"); targetCompany = created; companyAction = "created"; } else { targetCompany = await companies.getById(input.target.companyId); if (!targetCompany) throw notFound("Target company not found"); if (include.company && sourceManifest.company) { const updated = await companies.update(targetCompany.id, { name: sourceManifest.company.name, description: sourceManifest.company.description, brandColor: sourceManifest.company.brandColor, requireBoardApprovalForNewAgents: sourceManifest.company.requireBoardApprovalForNewAgents, }); targetCompany = updated ?? targetCompany; companyAction = "updated"; } } if (!targetCompany) throw notFound("Target company not found"); const resultAgents: CompanyPortabilityImportResult["agents"] = []; const importedSlugToAgentId = new Map(); const existingSlugToAgentId = new Map(); const existingAgents = await agents.list(targetCompany.id); for (const existing of existingAgents) { existingSlugToAgentId.set(normalizeAgentUrlKey(existing.name) ?? existing.id, existing.id); } const importedSlugToProjectId = new Map(); const existingProjectSlugToId = new Map(); const existingProjects = await projects.list(targetCompany.id); for (const existing of existingProjects) { existingProjectSlugToId.set(existing.urlKey, existing.id); } await companySkills.importPackageFiles(targetCompany.id, plan.source.files); if (include.agents) { for (const planAgent of plan.preview.plan.agentPlans) { const manifestAgent = plan.selectedAgents.find((agent) => agent.slug === planAgent.slug); if (!manifestAgent) continue; if (planAgent.action === "skip") { resultAgents.push({ slug: planAgent.slug, id: planAgent.existingAgentId, action: "skipped", name: planAgent.plannedName, reason: planAgent.reason, }); continue; } const bundlePrefix = `agents/${manifestAgent.slug}/`; const bundleFiles = Object.fromEntries( Object.entries(plan.source.files) .filter(([filePath]) => filePath.startsWith(bundlePrefix)) .map(([filePath, content]) => [normalizePortablePath(filePath.slice(bundlePrefix.length)), content]), ); const markdownRaw = bundleFiles["AGENTS.md"] ?? plan.source.files[manifestAgent.path]; const fallbackPromptTemplate = asString((manifestAgent.adapterConfig as Record).promptTemplate) || ""; if (!markdownRaw && fallbackPromptTemplate) { bundleFiles["AGENTS.md"] = fallbackPromptTemplate; } if (!markdownRaw && !fallbackPromptTemplate) { warnings.push(`Missing AGENTS markdown for ${manifestAgent.slug}; imported with an empty managed bundle.`); } // Apply adapter overrides from request if present const adapterOverride = input.adapterOverrides?.[planAgent.slug]; const effectiveAdapterType = adapterOverride?.adapterType ?? manifestAgent.adapterType; const baseAdapterConfig = adapterOverride?.adapterConfig ? { ...adapterOverride.adapterConfig } : { ...manifestAgent.adapterConfig } as Record; const desiredSkills = manifestAgent.skills ?? []; const adapterConfigWithSkills = writePaperclipSkillSyncPreference( baseAdapterConfig, desiredSkills, ); delete adapterConfigWithSkills.promptTemplate; delete adapterConfigWithSkills.bootstrapPromptTemplate; delete adapterConfigWithSkills.instructionsFilePath; delete adapterConfigWithSkills.instructionsBundleMode; delete adapterConfigWithSkills.instructionsRootPath; delete adapterConfigWithSkills.instructionsEntryFile; const patch = { name: planAgent.plannedName, role: manifestAgent.role, title: manifestAgent.title, icon: manifestAgent.icon, capabilities: manifestAgent.capabilities, reportsTo: null, adapterType: effectiveAdapterType, adapterConfig: adapterConfigWithSkills, runtimeConfig: manifestAgent.runtimeConfig, budgetMonthlyCents: manifestAgent.budgetMonthlyCents, permissions: manifestAgent.permissions, metadata: manifestAgent.metadata, }; if (planAgent.action === "update" && planAgent.existingAgentId) { let updated = await agents.update(planAgent.existingAgentId, patch); if (!updated) { warnings.push(`Skipped update for missing agent ${planAgent.existingAgentId}.`); resultAgents.push({ slug: planAgent.slug, id: null, action: "skipped", name: planAgent.plannedName, reason: "Existing target agent not found.", }); continue; } try { const materialized = await instructions.materializeManagedBundle(updated, bundleFiles, { clearLegacyPromptTemplate: true, replaceExisting: true, }); updated = await agents.update(updated.id, { adapterConfig: materialized.adapterConfig }) ?? updated; } catch (err) { warnings.push(`Failed to materialize instructions bundle for ${manifestAgent.slug}: ${err instanceof Error ? err.message : String(err)}`); } importedSlugToAgentId.set(planAgent.slug, updated.id); existingSlugToAgentId.set(normalizeAgentUrlKey(updated.name) ?? updated.id, updated.id); resultAgents.push({ slug: planAgent.slug, id: updated.id, action: "updated", name: updated.name, reason: planAgent.reason, }); continue; } let created = await agents.create(targetCompany.id, patch); try { const materialized = await instructions.materializeManagedBundle(created, bundleFiles, { clearLegacyPromptTemplate: true, replaceExisting: true, }); created = await agents.update(created.id, { adapterConfig: materialized.adapterConfig }) ?? created; } catch (err) { warnings.push(`Failed to materialize instructions bundle for ${manifestAgent.slug}: ${err instanceof Error ? err.message : String(err)}`); } importedSlugToAgentId.set(planAgent.slug, created.id); existingSlugToAgentId.set(normalizeAgentUrlKey(created.name) ?? created.id, created.id); resultAgents.push({ slug: planAgent.slug, id: created.id, action: "created", name: created.name, reason: planAgent.reason, }); } // Apply reporting links once all imported agent ids are available. for (const manifestAgent of plan.selectedAgents) { const agentId = importedSlugToAgentId.get(manifestAgent.slug); if (!agentId) continue; const managerSlug = manifestAgent.reportsToSlug; if (!managerSlug) continue; const managerId = importedSlugToAgentId.get(managerSlug) ?? existingSlugToAgentId.get(managerSlug) ?? null; if (!managerId || managerId === agentId) continue; try { await agents.update(agentId, { reportsTo: managerId }); } catch { warnings.push(`Could not assign manager ${managerSlug} for imported agent ${manifestAgent.slug}.`); } } } if (include.projects) { for (const planProject of plan.preview.plan.projectPlans) { const manifestProject = sourceManifest.projects.find((project) => project.slug === planProject.slug); if (!manifestProject) continue; if (planProject.action === "skip") continue; const projectLeadAgentId = manifestProject.leadAgentSlug ? importedSlugToAgentId.get(manifestProject.leadAgentSlug) ?? existingSlugToAgentId.get(manifestProject.leadAgentSlug) ?? null : null; const projectPatch = { name: planProject.plannedName, description: manifestProject.description, leadAgentId: projectLeadAgentId, targetDate: manifestProject.targetDate, color: manifestProject.color, status: manifestProject.status && PROJECT_STATUSES.includes(manifestProject.status as any) ? manifestProject.status as typeof PROJECT_STATUSES[number] : "backlog", executionWorkspacePolicy: manifestProject.executionWorkspacePolicy, }; if (planProject.action === "update" && planProject.existingProjectId) { const updated = await projects.update(planProject.existingProjectId, projectPatch); if (!updated) { warnings.push(`Skipped update for missing project ${planProject.existingProjectId}.`); continue; } importedSlugToProjectId.set(planProject.slug, updated.id); existingProjectSlugToId.set(updated.urlKey, updated.id); continue; } const created = await projects.create(targetCompany.id, projectPatch); importedSlugToProjectId.set(planProject.slug, created.id); existingProjectSlugToId.set(created.urlKey, created.id); } } if (include.issues) { for (const manifestIssue of sourceManifest.issues) { const markdownRaw = plan.source.files[manifestIssue.path]; const parsed = markdownRaw ? parseFrontmatterMarkdown(markdownRaw) : null; const description = parsed?.body || manifestIssue.description || null; const assigneeAgentId = manifestIssue.assigneeAgentSlug ? importedSlugToAgentId.get(manifestIssue.assigneeAgentSlug) ?? existingSlugToAgentId.get(manifestIssue.assigneeAgentSlug) ?? null : null; const projectId = manifestIssue.projectSlug ? importedSlugToProjectId.get(manifestIssue.projectSlug) ?? existingProjectSlugToId.get(manifestIssue.projectSlug) ?? null : null; await issues.create(targetCompany.id, { projectId, title: manifestIssue.title, description, assigneeAgentId, status: manifestIssue.status && ISSUE_STATUSES.includes(manifestIssue.status as any) ? manifestIssue.status as typeof ISSUE_STATUSES[number] : "backlog", priority: manifestIssue.priority && ISSUE_PRIORITIES.includes(manifestIssue.priority as any) ? manifestIssue.priority as typeof ISSUE_PRIORITIES[number] : "medium", billingCode: manifestIssue.billingCode, assigneeAdapterOverrides: manifestIssue.assigneeAdapterOverrides, executionWorkspaceSettings: manifestIssue.executionWorkspaceSettings, labelIds: [], }); if (manifestIssue.recurrence) { warnings.push(`Imported task ${manifestIssue.slug} as a one-time issue; recurrence metadata was not activated.`); } } } return { company: { id: targetCompany.id, name: targetCompany.name, action: companyAction, }, agents: resultAgents, envInputs: sourceManifest.envInputs ?? [], warnings, }; } return { exportBundle, previewImport, importBundle, }; }