2026-03-02 09:06:58 -06:00
import { promises as fs } from "node:fs" ;
2026-03-15 06:13:50 -05:00
import { execFile } from "node:child_process" ;
2026-03-02 09:06:58 -06:00
import path from "node:path" ;
2026-03-15 06:13:50 -05:00
import { promisify } from "node:util" ;
2026-03-03 08:45:26 -06:00
import type { Db } from "@paperclipai/db" ;
2026-03-02 09:06:58 -06:00
import type {
CompanyPortabilityAgentManifestEntry ,
CompanyPortabilityCollisionStrategy ,
2026-03-14 09:46:16 -05:00
CompanyPortabilityEnvInput ,
2026-03-02 09:06:58 -06:00
CompanyPortabilityExport ,
CompanyPortabilityExportResult ,
CompanyPortabilityImport ,
CompanyPortabilityImportResult ,
CompanyPortabilityInclude ,
CompanyPortabilityManifest ,
CompanyPortabilityPreview ,
CompanyPortabilityPreviewAgentPlan ,
CompanyPortabilityPreviewResult ,
2026-03-14 09:46:16 -05:00
CompanyPortabilityProjectManifestEntry ,
CompanyPortabilityIssueManifestEntry ,
2026-03-14 18:59:26 -05:00
CompanyPortabilitySkillManifestEntry ,
CompanySkill ,
2026-03-14 09:46:16 -05:00
} from "@paperclipai/shared" ;
import {
ISSUE_PRIORITIES ,
ISSUE_STATUSES ,
PROJECT_STATUSES ,
deriveProjectUrlKey ,
normalizeAgentUrlKey ,
2026-03-03 08:45:26 -06:00
} from "@paperclipai/shared" ;
2026-03-14 18:59:26 -05:00
import {
readPaperclipSkillSyncPreference ,
writePaperclipSkillSyncPreference ,
} from "@paperclipai/adapter-utils/server-utils" ;
2026-03-02 09:06:58 -06:00
import { notFound , unprocessable } from "../errors.js" ;
import { accessService } from "./access.js" ;
import { agentService } from "./agents.js" ;
2026-03-14 18:59:26 -05:00
import { companySkillService } from "./company-skills.js" ;
2026-03-02 09:06:58 -06:00
import { companyService } from "./companies.js" ;
2026-03-14 09:46:16 -05:00
import { issueService } from "./issues.js" ;
import { projectService } from "./projects.js" ;
2026-03-02 09:06:58 -06:00
const DEFAULT_INCLUDE : CompanyPortabilityInclude = {
company : true ,
agents : true ,
2026-03-14 09:46:16 -05:00
projects : false ,
issues : false ,
2026-03-02 09:06:58 -06:00
} ;
const DEFAULT_COLLISION_STRATEGY : CompanyPortabilityCollisionStrategy = "rename" ;
2026-03-15 06:13:50 -05:00
const execFileAsync = promisify ( execFile ) ;
let bundledSkillsCommitPromise : Promise < string | null > | null = null ;
2026-03-02 09:06:58 -06:00
2026-03-14 09:46:16 -05:00
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" )
) ;
}
2026-03-02 09:06:58 -06:00
type ResolvedSource = {
manifest : CompanyPortabilityManifest ;
files : Record < string , string > ;
warnings : string [ ] ;
} ;
type MarkdownDoc = {
frontmatter : Record < string , unknown > ;
body : string ;
} ;
2026-03-13 22:29:30 -05:00
type CompanyPackageIncludeEntry = {
path : string ;
} ;
2026-03-14 09:46:16 -05:00
type PaperclipExtensionDoc = {
schema? : string ;
company? : Record < string , unknown > | null ;
agents? : Record < string , Record < string , unknown > > | null ;
projects? : Record < string , Record < string , unknown > > | null ;
tasks? : Record < string , Record < string , unknown > > | null ;
} ;
type ProjectLike = {
id : string ;
name : string ;
description : string | null ;
leadAgentId : string | null ;
targetDate : string | null ;
color : string | null ;
status : string ;
executionWorkspacePolicy : Record < string , unknown > | null ;
metadata? : Record < string , unknown > | 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 < string , unknown > | null ;
assigneeAdapterOverrides : Record < string , unknown > | null ;
} ;
2026-03-02 09:06:58 -06:00
type ImportPlanInternal = {
preview : CompanyPortabilityPreviewResult ;
source : ResolvedSource ;
include : CompanyPortabilityInclude ;
collisionStrategy : CompanyPortabilityCollisionStrategy ;
selectedAgents : CompanyPortabilityAgentManifestEntry [ ] ;
} ;
type AgentLike = {
id : string ;
name : string ;
adapterConfig : Record < string , unknown > ;
} ;
2026-03-14 09:46:16 -05:00
type EnvInputRecord = {
kind : "secret" | "plain" ;
requirement : "required" | "optional" ;
default ? : string | null ;
description? : string | null ;
portability ? : "portable" | "system_dependent" ;
} ;
2026-03-02 10:31:48 -06:00
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 < string , Array < { path : string [ ] ; value : unknown } > > = {
codex_local : [
{ path : [ "timeoutSec" ] , value : 0 } ,
{ path : [ "graceSec" ] , value : 15 } ,
] ,
2026-03-08 16:43:34 +05:30
gemini_local : [
{ path : [ "timeoutSec" ] , value : 0 } ,
{ path : [ "graceSec" ] , value : 15 } ,
] ,
2026-03-04 16:48:54 -06:00
opencode_local : [
{ path : [ "timeoutSec" ] , value : 0 } ,
{ path : [ "graceSec" ] , value : 15 } ,
] ,
2026-03-05 06:31:22 -06:00
cursor : [
{ path : [ "timeoutSec" ] , value : 0 } ,
{ path : [ "graceSec" ] , value : 15 } ,
] ,
2026-03-02 10:31:48 -06:00
claude_local : [
{ path : [ "timeoutSec" ] , value : 0 } ,
{ path : [ "graceSec" ] , value : 15 } ,
2026-03-12 08:42:12 -05:00
{ path : [ "maxTurnsPerRun" ] , value : 300 } ,
2026-03-02 10:31:48 -06:00
] ,
2026-03-07 08:59:34 -06:00
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" ] } ,
] ,
2026-03-02 10:31:48 -06:00
} ;
function isPlainRecord ( value : unknown ) : value is Record < string , unknown > {
return typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
}
2026-03-02 09:06:58 -06:00
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 < string > ) {
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 < string > ) {
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 ;
}
}
2026-03-14 09:46:16 -05:00
function uniqueProjectName ( baseName : string , existingProjectSlugs : Set < string > ) {
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 ;
}
}
2026-03-02 09:06:58 -06:00
function normalizeInclude ( input? : Partial < CompanyPortabilityInclude > ) : CompanyPortabilityInclude {
return {
company : input?.company ? ? DEFAULT_INCLUDE . company ,
agents : input?.agents ? ? DEFAULT_INCLUDE . agents ,
2026-03-14 09:46:16 -05:00
projects : input?.projects ? ? DEFAULT_INCLUDE . projects ,
issues : input?.issues ? ? DEFAULT_INCLUDE . issues ,
2026-03-02 09:06:58 -06:00
} ;
}
2026-03-13 22:29:30 -05:00
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 < string , string > ,
rootPath? : string | null ,
) : Record < string , string > {
const normalizedRoot = rootPath ? normalizePortablePath ( rootPath ) : null ;
const out : Record < string , string > = { } ;
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 ;
}
2026-03-14 09:46:16 -05:00
function findPaperclipExtensionPath ( files : Record < string , string > ) {
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 ;
}
2026-03-02 09:06:58 -06:00
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 ;
}
2026-03-14 09:46:16 -05:00
function normalizePortableConfig (
value : unknown ,
) : Record < string , unknown > {
if ( typeof value !== "object" || value === null || Array . isArray ( value ) ) return { } ;
const input = value as Record < string , unknown > ;
const next : Record < string , unknown > = { } ;
for ( const [ key , entry ] of Object . entries ( input ) ) {
2026-03-16 08:55:37 -05:00
if (
key === "cwd" ||
key === "instructionsFilePath" ||
key === "promptTemplate" ||
key === "paperclipSkillSync"
) continue ;
2026-03-14 09:46:16 -05:00
if ( key === "env" ) continue ;
next [ key ] = entry ;
}
return next ;
}
function isAbsoluteCommand ( value : string ) {
return path . isAbsolute ( value ) || /^[A-Za-z]:[\\/]/ . test ( value ) ;
}
function extractPortableEnvInputs (
2026-03-02 09:06:58 -06:00
agentSlug : string ,
envValue : unknown ,
2026-03-14 09:46:16 -05:00
warnings : string [ ] ,
) : CompanyPortabilityEnvInput [ ] {
if ( ! isPlainRecord ( envValue ) ) return [ ] ;
2026-03-02 09:06:58 -06:00
const env = envValue as Record < string , unknown > ;
2026-03-14 09:46:16 -05:00
const inputs : CompanyPortabilityEnvInput [ ] = [ ] ;
2026-03-02 09:06:58 -06:00
for ( const [ key , binding ] of Object . entries ( env ) ) {
2026-03-14 09:46:16 -05:00
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 ( {
2026-03-02 09:06:58 -06:00
key ,
2026-03-14 09:46:16 -05:00
description : ` Provide ${ key } for agent ${ agentSlug } ` ,
2026-03-02 09:06:58 -06:00
agentSlug ,
2026-03-14 09:46:16 -05:00
kind : "secret" ,
requirement : "optional" ,
defaultValue : "" ,
portability : "portable" ,
2026-03-02 09:06:58 -06:00
} ) ;
continue ;
}
2026-03-14 09:46:16 -05:00
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 ,
} ) ;
2026-03-02 09:06:58 -06:00
continue ;
}
2026-03-14 09:46:16 -05:00
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 ,
} ) ;
}
2026-03-02 09:06:58 -06:00
}
2026-03-14 09:46:16 -05:00
return inputs ;
2026-03-02 09:06:58 -06:00
}
2026-03-02 10:31:48 -06:00
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 < string , unknown > = { } ;
for ( const [ key , entry ] of Object . entries ( value ) ) {
const next = pruneDefaultLikeValue ( entry , {
. . . opts ,
path : [ . . . pathSegments , key ] ,
} ) ;
if ( next === undefined ) continue ;
out [ key ] = next ;
2026-03-02 09:06:58 -06:00
}
2026-03-02 10:31:48 -06:00
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 ;
}
2026-03-14 09:46:16 -05:00
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 < string , unknown > = { } ;
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" ,
2026-03-14 18:59:26 -05:00
"skills" ,
2026-03-14 09:46:16 -05:00
"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 < string , number > (
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 < string , unknown > ) {
return Object . entries ( value ) . sort ( ( [ leftKey ] , [ rightKey ] ) = > compareYamlKeys ( leftKey , rightKey ) ) ;
}
2026-03-02 10:31:48 -06:00
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 ) ) ;
2026-03-02 09:06:58 -06:00
}
2026-03-02 10:31:48 -06:00
return lines ;
}
if ( isPlainRecord ( value ) ) {
2026-03-14 09:46:16 -05:00
const entries = orderedYamlEntries ( value ) ;
2026-03-02 10:31:48 -06:00
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 < string , unknown > ) {
const lines : string [ ] = [ "---" ] ;
2026-03-14 09:46:16 -05:00
for ( const [ key , value ] of orderedYamlEntries ( frontmatter ) ) {
2026-03-15 15:54:26 -05:00
// Skip null/undefined values — don't export empty fields
if ( value === null || value === undefined ) continue ;
2026-03-02 10:31:48 -06:00
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 ) } ` ) ;
2026-03-02 09:06:58 -06:00
continue ;
}
2026-03-02 10:31:48 -06:00
lines . push ( ` ${ key } : ` ) ;
lines . push ( . . . renderYamlBlock ( value , 1 ) ) ;
2026-03-02 09:06:58 -06:00
}
lines . push ( "---" ) ;
return ` ${ lines . join ( "\n" ) } \ n ` ;
}
function buildMarkdown ( frontmatter : Record < string , unknown > , body : string ) {
const cleanBody = body . replace ( /\r\n/g , "\n" ) . trim ( ) ;
if ( ! cleanBody ) {
return ` ${ renderFrontmatter ( frontmatter ) } \ n ` ;
}
return ` ${ renderFrontmatter ( frontmatter ) } \ n ${ cleanBody } \ n ` ;
}
2026-03-16 10:14:09 -05:00
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 < string > ,
) {
const parsed = parseFrontmatterMarkdown ( markdown ) ;
const includeEntries = readIncludeEntries ( parsed . frontmatter ) ;
const filteredIncludes = includeEntries . filter ( ( entry ) = >
selectedFiles . has ( resolvePortablePath ( companyPath , entry . path ) ) ,
) ;
const nextFrontmatter : Record < string , unknown > = { . . . 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 < string , string > = { } ;
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 ;
}
2026-03-15 06:13:50 -05:00
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 ) {
2026-03-14 18:59:26 -05:00
const metadata = isPlainRecord ( skill . metadata ) ? skill.metadata : null ;
if ( asString ( metadata ? . sourceKind ) === "paperclip_bundled" ) {
2026-03-15 06:13:50 -05:00
const commit = await resolveBundledSkillsCommit ( ) ;
2026-03-14 18:59:26 -05:00
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" ;
}
2026-03-15 06:13:50 -05:00
async function buildReferencedSkillMarkdown ( skill : CompanySkill ) {
const sourceEntry = await buildSkillSourceEntry ( skill ) ;
2026-03-14 18:59:26 -05:00
const frontmatter : Record < string , unknown > = {
name : skill.name ,
description : skill.description ? ? null ,
} ;
if ( sourceEntry ) {
frontmatter . metadata = {
sources : [ sourceEntry ] ,
} ;
}
return buildMarkdown ( frontmatter , "" ) ;
}
2026-03-15 06:13:50 -05:00
async function withSkillSourceMetadata ( skill : CompanySkill , markdown : string ) {
const sourceEntry = await buildSkillSourceEntry ( skill ) ;
2026-03-14 18:59:26 -05:00
if ( ! sourceEntry ) return markdown ;
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 ) )
: [ ] ;
metadata . sources = [ . . . existingSources , sourceEntry ] ;
const frontmatter = {
. . . parsed . frontmatter ,
metadata ,
} ;
return buildMarkdown ( frontmatter , parsed . body ) ;
}
2026-03-02 10:31:48 -06:00
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" ) ;
}
2026-03-13 22:29:30 -05:00
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 < string , unknown > = {
[ 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 < string , unknown > = { } ;
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 < string , unknown > {
const prepared = prepareYamlLines ( raw ) ;
if ( prepared . length === 0 ) return { } ;
const parsed = parseYamlBlock ( prepared , 0 , prepared [ 0 ] ! . indent ) ;
return isPlainRecord ( parsed . value ) ? parsed . value : { } ;
}
2026-03-14 09:46:16 -05:00
function parseYamlFile ( raw : string ) : Record < string , unknown > {
return parseYamlFrontmatter ( raw ) ;
}
function buildYamlFile ( value : Record < string , unknown > , opts ? : { preserveEmptyStrings? : boolean } ) {
const cleaned = stripEmptyValues ( value , opts ) ;
if ( ! isPlainRecord ( cleaned ) ) return "{}\n" ;
return renderYamlBlock ( cleaned , 0 ) . join ( "\n" ) + "\n" ;
}
2026-03-02 09:06:58 -06:00
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 ( ) ;
2026-03-13 22:29:30 -05:00
return {
frontmatter : parseYamlFrontmatter ( frontmatterRaw ) ,
body ,
} ;
2026-03-02 09:06:58 -06:00
}
2026-03-13 22:29:30 -05:00
async function fetchText ( url : string ) {
2026-03-02 09:06:58 -06:00
const response = await fetch ( url ) ;
if ( ! response . ok ) {
throw unprocessable ( ` Failed to fetch ${ url } : ${ response . status } ` ) ;
}
2026-03-13 22:29:30 -05:00
return response . text ( ) ;
2026-03-02 09:06:58 -06:00
}
2026-03-13 22:29:30 -05:00
async function fetchOptionalText ( url : string ) {
2026-03-02 09:06:58 -06:00
const response = await fetch ( url ) ;
2026-03-13 22:29:30 -05:00
if ( response . status === 404 ) return null ;
2026-03-02 09:06:58 -06:00
if ( ! response . ok ) {
throw unprocessable ( ` Failed to fetch ${ url } : ${ response . status } ` ) ;
}
return response . text ( ) ;
}
2026-03-14 09:46:16 -05:00
async function fetchJson < T > ( url : string ) : Promise < T > {
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 < T > ;
}
function dedupeEnvInputs ( values : CompanyPortabilityManifest [ "envInputs" ] ) {
2026-03-02 09:06:58 -06:00
const seen = new Set < string > ( ) ;
2026-03-14 09:46:16 -05:00
const out : CompanyPortabilityManifest [ "envInputs" ] = [ ] ;
2026-03-02 09:06:58 -06:00
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 ;
}
2026-03-14 09:46:16 -05:00
function buildEnvInputMap ( inputs : CompanyPortabilityEnvInput [ ] ) {
const env : Record < string , Record < string , unknown > > = { } ;
for ( const input of inputs ) {
const entry : Record < string , unknown > = {
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 ;
2026-03-13 22:29:30 -05:00
}
2026-03-14 09:46:16 -05:00
function readCompanyApprovalDefault ( _frontmatter : Record < string , unknown > ) {
2026-03-13 22:29:30 -05:00
return true ;
}
function readIncludeEntries ( frontmatter : Record < string , unknown > ) : 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 [ ] ;
} ) ;
}
2026-03-14 09:46:16 -05:00
function readAgentEnvInputs (
extension : Record < string , unknown > ,
2026-03-13 22:29:30 -05:00
agentSlug : string ,
2026-03-14 09:46:16 -05:00
) : 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" ,
} ] ;
2026-03-13 22:29:30 -05:00
} ) ;
}
2026-03-14 18:59:26 -05:00
function readAgentSkillRefs ( frontmatter : Record < string , unknown > ) {
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 ) = > normalizeAgentUrlKey ( entry ) ? ? entry . trim ( ) )
. filter ( Boolean ) ,
) ) ;
}
2026-03-13 22:29:30 -05:00
function buildManifestFromPackageFiles (
files : Record < string , string > ,
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 ;
2026-03-14 09:46:16 -05:00
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 : { } ;
2026-03-13 22:29:30 -05:00
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" ) ;
2026-03-14 09:46:16 -05:00
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" ) ;
2026-03-14 18:59:26 -05:00
const referencedSkillPaths = includeEntries
. map ( ( entry ) = > resolvePortablePath ( resolvedCompanyPath , entry . path ) )
. filter ( ( entry ) = > entry . endsWith ( "/SKILL.md" ) || entry === "SKILL.md" ) ;
2026-03-13 22:29:30 -05:00
const discoveredAgentPaths = Object . keys ( normalizedFiles ) . filter (
( entry ) = > entry . endsWith ( "/AGENTS.md" ) || entry === "AGENTS.md" ,
) ;
2026-03-14 09:46:16 -05:00
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" ,
) ;
2026-03-14 18:59:26 -05:00
const discoveredSkillPaths = Object . keys ( normalizedFiles ) . filter (
( entry ) = > entry . endsWith ( "/SKILL.md" ) || entry === "SKILL.md" ,
) ;
2026-03-13 22:29:30 -05:00
const agentPaths = Array . from ( new Set ( [ . . . referencedAgentPaths , . . . discoveredAgentPaths ] ) ) . sort ( ) ;
2026-03-14 09:46:16 -05:00
const projectPaths = Array . from ( new Set ( [ . . . referencedProjectPaths , . . . discoveredProjectPaths ] ) ) . sort ( ) ;
const taskPaths = Array . from ( new Set ( [ . . . referencedTaskPaths , . . . discoveredTaskPaths ] ) ) . sort ( ) ;
2026-03-14 18:59:26 -05:00
const skillPaths = Array . from ( new Set ( [ . . . referencedSkillPaths , . . . discoveredSkillPaths ] ) ) . sort ( ) ;
2026-03-13 22:29:30 -05:00
const manifest : CompanyPortabilityManifest = {
2026-03-14 09:46:16 -05:00
schemaVersion : 3 ,
2026-03-13 22:29:30 -05:00
generatedAt : new Date ( ) . toISOString ( ) ,
source : opts?.sourceLabel ? ? null ,
includes : {
company : true ,
agents : true ,
2026-03-14 09:46:16 -05:00
projects : projectPaths.length > 0 ,
issues : taskPaths.length > 0 ,
2026-03-13 22:29:30 -05:00
} ,
company : {
path : resolvedCompanyPath ,
name : companyName ,
description : asString ( companyFrontmatter . description ) ,
2026-03-14 09:46:16 -05:00
brandColor : asString ( paperclipCompany . brandColor ) ,
requireBoardApprovalForNewAgents :
typeof paperclipCompany . requireBoardApprovalForNewAgents === "boolean"
? paperclipCompany . requireBoardApprovalForNewAgents
: readCompanyApprovalDefault ( companyFrontmatter ) ,
2026-03-13 22:29:30 -05:00
} ,
agents : [ ] ,
2026-03-14 18:59:26 -05:00
skills : [ ] ,
2026-03-14 09:46:16 -05:00
projects : [ ] ,
issues : [ ] ,
envInputs : [ ] ,
2026-03-13 22:29:30 -05:00
} ;
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 ;
2026-03-14 09:46:16 -05:00
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 ? ? { } ;
2026-03-13 22:29:30 -05:00
const title = asString ( frontmatter . title ) ;
manifest . agents . push ( {
slug ,
name : asString ( frontmatter . name ) ? ? title ? ? slug ,
path : agentPath ,
2026-03-14 18:59:26 -05:00
skills : readAgentSkillRefs ( frontmatter ) ,
2026-03-14 09:46:16 -05:00
role : asString ( extension . role ) ? ? "agent" ,
2026-03-13 22:29:30 -05:00
title ,
2026-03-14 09:46:16 -05:00
icon : asString ( extension . icon ) ,
capabilities : asString ( extension . capabilities ) ,
reportsToSlug : asString ( frontmatter . reportsTo ) ? ? asString ( extension . reportsTo ) ,
adapterType : asString ( extensionAdapter ? . type ) ? ? "process" ,
2026-03-13 22:29:30 -05:00
adapterConfig ,
runtimeConfig ,
2026-03-14 09:46:16 -05:00
permissions : extensionPermissions ? ? { } ,
2026-03-13 22:29:30 -05:00
budgetMonthlyCents :
2026-03-14 09:46:16 -05:00
typeof extension . budgetMonthlyCents === "number" && Number . isFinite ( extension . budgetMonthlyCents )
? Math . max ( 0 , Math . floor ( extension . budgetMonthlyCents ) )
2026-03-13 22:29:30 -05:00
: 0 ,
2026-03-14 09:46:16 -05:00
metadata : extensionMetadata ,
2026-03-13 22:29:30 -05:00
} ) ;
2026-03-14 09:46:16 -05:00
manifest . envInputs . push ( . . . readAgentEnvInputs ( extension , slug ) ) ;
2026-03-13 22:29:30 -05:00
2026-03-14 09:46:16 -05:00
if ( frontmatter . kind && frontmatter . kind !== "agent" ) {
2026-03-13 22:29:30 -05:00
warnings . push ( ` Agent markdown ${ agentPath } does not declare kind: agent in frontmatter. ` ) ;
}
}
2026-03-14 18:59:26 -05:00
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 < string , unknown > | undefined ;
const sourceKind = asString ( primarySource ? . kind ) ;
let sourceType = "catalog" ;
let sourceLocator : string | null = null ;
let sourceRef : string | null = null ;
let normalizedMetadata : Record < string , unknown > | 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" ,
} ;
}
manifest . skills . push ( {
slug ,
name : asString ( frontmatter . name ) ? ? slug ,
path : skillPath ,
description : asString ( frontmatter . description ) ,
sourceType ,
sourceLocator ,
sourceRef ,
trustLevel : null ,
compatibility : "compatible" ,
metadata : normalizedMetadata ,
fileInventory : inventory ,
} ) ;
}
2026-03-14 09:46:16 -05:00
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 ) ;
2026-03-13 22:29:30 -05:00
return {
manifest ,
files : normalizedFiles ,
warnings ,
} ;
}
function parseGitHubSourceUrl ( rawUrl : string ) {
2026-03-02 09:06:58 -06:00
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 = "" ;
2026-03-13 22:29:30 -05:00
let companyPath = "COMPANY.md" ;
2026-03-02 09:06:58 -06:00
if ( parts [ 2 ] === "tree" ) {
ref = parts [ 3 ] ? ? "main" ;
basePath = parts . slice ( 4 ) . join ( "/" ) ;
2026-03-13 22:29:30 -05:00
} 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 = "" ;
2026-03-02 09:06:58 -06:00
}
2026-03-13 22:29:30 -05:00
return { owner , repo , ref , basePath , companyPath } ;
2026-03-02 09:06:58 -06:00
}
function resolveRawGitHubUrl ( owner : string , repo : string , ref : string , filePath : string ) {
const normalizedFilePath = filePath . replace ( /^\/+/ , "" ) ;
return ` https://raw.githubusercontent.com/ ${ owner } / ${ repo } / ${ ref } / ${ normalizedFilePath } ` ;
}
async function readAgentInstructions ( agent : AgentLike ) : Promise < { body : string ; warning : string | null } > {
const config = agent . adapterConfig as Record < string , unknown > ;
const instructionsFilePath = asString ( config . instructionsFilePath ) ;
2026-03-02 10:31:48 -06:00
if ( instructionsFilePath ) {
const workspaceCwd = asString ( process . env . PAPERCLIP_WORKSPACE_CWD ) ;
const candidates = new Set < string > ( ) ;
if ( path . isAbsolute ( instructionsFilePath ) ) {
candidates . add ( instructionsFilePath ) ;
} else {
if ( workspaceCwd ) candidates . add ( path . resolve ( workspaceCwd , instructionsFilePath ) ) ;
candidates . add ( path . resolve ( process . cwd ( ) , instructionsFilePath ) ) ;
}
for ( const candidate of candidates ) {
try {
const stat = await fs . stat ( candidate ) ;
if ( ! stat . isFile ( ) || stat . size > 1024 * 1024 ) continue ;
2026-03-02 09:06:58 -06:00
const body = await Promise . race ( [
2026-03-02 10:31:48 -06:00
fs . readFile ( candidate , "utf8" ) ,
2026-03-02 09:06:58 -06:00
new Promise < string > ( ( _ , reject ) = > {
setTimeout ( ( ) = > reject ( new Error ( "timed out reading instructions file" ) ) , 1500 ) ;
} ) ,
] ) ;
return { body , warning : null } ;
2026-03-02 10:31:48 -06:00
} catch {
// try next candidate
2026-03-02 09:06:58 -06:00
}
}
}
const promptTemplate = asString ( config . promptTemplate ) ;
if ( promptTemplate ) {
2026-03-02 10:31:48 -06:00
const warning = instructionsFilePath
? ` Agent ${ agent . name } instructionsFilePath was not readable; fell back to promptTemplate. `
: null ;
return {
body : promptTemplate ,
warning ,
} ;
2026-03-02 09:06:58 -06:00
}
return {
body : "_No AGENTS instructions were resolved from current agent config._" ,
warning : ` Agent ${ agent . name } has no resolvable instructionsFilePath/promptTemplate; exported placeholder AGENTS.md. ` ,
} ;
}
export function companyPortabilityService ( db : Db ) {
const companies = companyService ( db ) ;
const agents = agentService ( db ) ;
const access = accessService ( db ) ;
2026-03-14 09:46:16 -05:00
const projects = projectService ( db ) ;
const issues = issueService ( db ) ;
2026-03-14 18:59:26 -05:00
const companySkills = companySkillService ( db ) ;
2026-03-02 09:06:58 -06:00
async function resolveSource ( source : CompanyPortabilityPreview [ "source" ] ) : Promise < ResolvedSource > {
if ( source . type === "inline" ) {
2026-03-13 22:29:30 -05:00
return buildManifestFromPackageFiles (
normalizeFileMap ( source . files , source . rootPath ) ,
) ;
2026-03-02 09:06:58 -06:00
}
2026-03-13 22:29:30 -05:00
const parsed = parseGitHubSourceUrl ( source . url ) ;
2026-03-02 09:06:58 -06:00
let ref = parsed . ref ;
const warnings : string [ ] = [ ] ;
2026-03-13 22:29:30 -05:00
const companyRelativePath = parsed . companyPath === "COMPANY.md"
? [ parsed . basePath , "COMPANY.md" ] . filter ( Boolean ) . join ( "/" )
: parsed . companyPath ;
let companyMarkdown : string | null = null ;
2026-03-02 09:06:58 -06:00
try {
2026-03-13 22:29:30 -05:00
companyMarkdown = await fetchOptionalText (
resolveRawGitHubUrl ( parsed . owner , parsed . repo , ref , companyRelativePath ) ,
2026-03-02 09:06:58 -06:00
) ;
} catch ( err ) {
if ( ref === "main" ) {
ref = "master" ;
warnings . push ( "GitHub ref main not found; falling back to master." ) ;
2026-03-13 22:29:30 -05:00
companyMarkdown = await fetchOptionalText (
resolveRawGitHubUrl ( parsed . owner , parsed . repo , ref , companyRelativePath ) ,
2026-03-02 09:06:58 -06:00
) ;
} else {
throw err ;
}
}
2026-03-13 22:29:30 -05:00
if ( ! companyMarkdown ) {
throw unprocessable ( "GitHub company package is missing COMPANY.md" ) ;
2026-03-02 09:06:58 -06:00
}
2026-03-13 22:29:30 -05:00
const companyPath = parsed . companyPath === "COMPANY.md"
? "COMPANY.md"
: normalizePortablePath ( path . posix . relative ( parsed . basePath || "." , parsed . companyPath ) ) ;
const files : Record < string , string > = {
[ companyPath ] : companyMarkdown ,
} ;
2026-03-14 09:46:16 -05:00
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" ) ||
2026-03-14 18:59:26 -05:00
relative . startsWith ( "skills/" ) ||
2026-03-14 09:46:16 -05:00
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 ) ,
) ;
}
2026-03-13 22:29:30 -05:00
const companyDoc = parseFrontmatterMarkdown ( companyMarkdown ) ;
const includeEntries = readIncludeEntries ( companyDoc . frontmatter ) ;
for ( const includeEntry of includeEntries ) {
const repoPath = [ parsed . basePath , includeEntry . path ] . filter ( Boolean ) . join ( "/" ) ;
2026-03-14 09:46:16 -05:00
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 (
2026-03-13 22:29:30 -05:00
resolveRawGitHubUrl ( parsed . owner , parsed . repo , ref , repoPath ) ,
2026-03-02 09:06:58 -06:00
) ;
}
2026-03-13 22:29:30 -05:00
const resolved = buildManifestFromPackageFiles ( files ) ;
resolved . warnings . unshift ( . . . warnings ) ;
return resolved ;
2026-03-02 09:06:58 -06:00
}
async function exportBundle (
companyId : string ,
input : CompanyPortabilityExport ,
) : Promise < CompanyPortabilityExportResult > {
2026-03-14 09:46:16 -05:00
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 ,
} ) ;
2026-03-02 09:06:58 -06:00
const company = await companies . getById ( companyId ) ;
if ( ! company ) throw notFound ( "Company not found" ) ;
const files : Record < string , string > = { } ;
const warnings : string [ ] = [ ] ;
2026-03-14 09:46:16 -05:00
const envInputs : CompanyPortabilityManifest [ "envInputs" ] = [ ] ;
2026-03-13 22:29:30 -05:00
const rootPath = normalizeAgentUrlKey ( company . name ) ? ? "company-package" ;
2026-03-02 09:06:58 -06:00
2026-03-04 22:28:22 +00:00
const allAgentRows = include . agents ? await agents . list ( companyId , { includeTerminated : true } ) : [ ] ;
2026-03-02 10:31:48 -06:00
const agentRows = allAgentRows . filter ( ( agent ) = > agent . status !== "terminated" ) ;
2026-03-16 17:45:28 -05:00
const companySkillRows = await companySkills . listFull ( companyId ) ;
2026-03-02 10:31:48 -06:00
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 < string > ( ) ;
const idToSlug = new Map < string , string > ( ) ;
for ( const agent of agentRows ) {
const baseSlug = toSafeSlug ( agent . name , "agent" ) ;
const slug = uniqueSlug ( baseSlug , usedSlugs ) ;
idToSlug . set ( agent . id , slug ) ;
}
2026-03-14 09:46:16 -05:00
const projectsSvc = projectService ( db ) ;
const issuesSvc = issueService ( db ) ;
const allProjects = include . projects || include . issues ? await projectsSvc . list ( companyId ) : [ ] ;
const projectById = new Map ( allProjects . map ( ( project ) = > [ project . id , project ] ) ) ;
const projectByReference = new Map < string , typeof allProjects [ number ] > ( ) ;
for ( const project of allProjects ) {
projectByReference . set ( project . id , project ) ;
projectByReference . set ( project . urlKey , project ) ;
}
const selectedProjects = new Map < string , typeof allProjects [ number ] > ( ) ;
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 < string , Awaited < ReturnType < typeof issuesSvc.getById > > > ( ) ;
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 < typeof issue > = > issue != null )
. sort ( ( left , right ) = > ( left . identifier ? ? left . title ) . localeCompare ( right . identifier ? ? right . title ) ) ;
const taskSlugByIssueId = new Map < string , string > ( ) ;
const usedTaskSlugs = new Set < string > ( ) ;
for ( const issue of selectedIssueRows ) {
const baseSlug = normalizeAgentUrlKey ( issue . identifier ? ? issue . title ) ? ? "task" ;
taskSlugByIssueId . set ( issue . id , uniqueSlug ( baseSlug , usedTaskSlugs ) ) ;
}
const projectSlugById = new Map < string , string > ( ) ;
const usedProjectSlugs = new Set < string > ( ) ;
for ( const project of selectedProjectRows ) {
2026-03-14 18:59:26 -05:00
const baseSlug = deriveProjectUrlKey ( project . name , project . name ) ;
2026-03-14 09:46:16 -05:00
projectSlugById . set ( project . id , uniqueSlug ( baseSlug , usedProjectSlugs ) ) ;
}
const companyPath = "COMPANY.md" ;
const companyBodySections : string [ ] = [ ] ;
if ( include . agents ) {
2026-03-02 10:31:48 -06:00
const companyAgentSummaries = agentRows . map ( ( agent ) = > ( {
slug : idToSlug.get ( agent . id ) ? ? "agent" ,
name : agent.name ,
} ) ) ;
2026-03-14 09:46:16 -05:00
companyBodySections . push ( renderCompanyAgentsSection ( companyAgentSummaries ) ) ;
}
if ( selectedProjectRows . length > 0 ) {
companyBodySections . push (
[ "# Projects" , "" , . . . selectedProjectRows . map ( ( project ) = > ` - ${ projectSlugById . get ( project . id ) ? ? project . id } - ${ project . name } ` ) ] . join ( "\n" ) ,
2026-03-02 09:06:58 -06:00
) ;
}
2026-03-14 09:46:16 -05:00
files [ companyPath ] = buildMarkdown (
{
name : company.name ,
description : company.description ? ? null ,
schema : "agentcompanies/v1" ,
slug : rootPath ,
} ,
companyBodySections . join ( "\n\n" ) . trim ( ) ,
) ;
const paperclipAgentsOut : Record < string , Record < string , unknown > > = { } ;
const paperclipProjectsOut : Record < string , Record < string , unknown > > = { } ;
const paperclipTasksOut : Record < string , Record < string , unknown > > = { } ;
2026-03-02 09:06:58 -06:00
2026-03-14 18:59:26 -05:00
for ( const skill of companySkillRows ) {
if ( shouldReferenceSkillOnExport ( skill , Boolean ( input . expandReferencedSkills ) ) ) {
2026-03-15 06:13:50 -05:00
files [ ` skills/ ${ skill . slug } /SKILL.md ` ] = await buildReferencedSkillMarkdown ( skill ) ;
2026-03-14 18:59:26 -05:00
continue ;
}
for ( const inventoryEntry of skill . fileInventory ) {
const fileDetail = await companySkills . readFile ( companyId , skill . id , inventoryEntry . path ) . catch ( ( ) = > null ) ;
if ( ! fileDetail ) continue ;
const filePath = ` skills/ ${ skill . slug } / ${ inventoryEntry . path } ` ;
files [ filePath ] = inventoryEntry . path === "SKILL.md"
2026-03-15 06:13:50 -05:00
? await withSkillSourceMetadata ( skill , fileDetail . content )
2026-03-14 18:59:26 -05:00
: fileDetail . content ;
}
}
2026-03-02 09:06:58 -06:00
if ( include . agents ) {
for ( const agent of agentRows ) {
const slug = idToSlug . get ( agent . id ) ! ;
const instructions = await readAgentInstructions ( agent ) ;
if ( instructions . warning ) warnings . push ( instructions . warning ) ;
const agentPath = ` agents/ ${ slug } /AGENTS.md ` ;
2026-03-14 09:46:16 -05:00
const envInputsStart = envInputs . length ;
const exportedEnvInputs = extractPortableEnvInputs (
slug ,
( agent . adapterConfig as Record < string , unknown > ) . env ,
warnings ,
) ;
envInputs . push ( . . . exportedEnvInputs ) ;
2026-03-02 10:31:48 -06:00
const adapterDefaultRules = ADAPTER_DEFAULT_RULES_BY_TYPE [ agent . adapterType ] ? ? [ ] ;
const portableAdapterConfig = pruneDefaultLikeValue (
2026-03-14 09:46:16 -05:00
normalizePortableConfig ( agent . adapterConfig ) ,
2026-03-02 10:31:48 -06:00
{
dropFalseBooleans : true ,
defaultRules : adapterDefaultRules ,
} ,
) as Record < string , unknown > ;
const portableRuntimeConfig = pruneDefaultLikeValue (
2026-03-14 09:46:16 -05:00
normalizePortableConfig ( agent . runtimeConfig ) ,
2026-03-02 10:31:48 -06:00
{
dropFalseBooleans : true ,
defaultRules : RUNTIME_DEFAULT_RULES ,
} ,
) as Record < string , unknown > ;
const portablePermissions = pruneDefaultLikeValue ( agent . permissions ? ? { } , { dropFalseBooleans : true } ) as Record < string , unknown > ;
2026-03-14 09:46:16 -05:00
const agentEnvInputs = dedupeEnvInputs (
envInputs
. slice ( envInputsStart )
. filter ( ( inputValue ) = > inputValue . agentSlug === slug ) ,
2026-03-02 10:31:48 -06:00
) ;
const reportsToSlug = agent . reportsTo ? ( idToSlug . get ( agent . reportsTo ) ? ? null ) : null ;
2026-03-14 18:59:26 -05:00
const desiredSkills = readPaperclipSkillSyncPreference (
( agent . adapterConfig as Record < string , unknown > ) ? ? { } ,
) . desiredSkills ;
2026-03-02 10:31:48 -06:00
2026-03-14 09:46:16 -05:00
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 ;
}
2026-03-02 09:06:58 -06:00
files [ agentPath ] = buildMarkdown (
2026-03-14 18:59:26 -05:00
stripEmptyValues ( {
2026-03-02 09:06:58 -06:00
name : agent.name ,
2026-03-13 22:29:30 -05:00
title : agent.title ? ? null ,
2026-03-02 10:31:48 -06:00
reportsTo : reportsToSlug ,
2026-03-14 18:59:26 -05:00
skills : desiredSkills.length > 0 ? desiredSkills : undefined ,
} ) as Record < string , unknown > ,
2026-03-02 09:06:58 -06:00
instructions . body ,
) ;
2026-03-14 09:46:16 -05:00
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 < string , unknown > | null ) ? ? null ,
} ) ;
if ( isPlainRecord ( extension ) && agentEnvInputs . length > 0 ) {
extension . inputs = {
env : buildEnvInputMap ( agentEnvInputs ) ,
} ;
}
paperclipAgentsOut [ slug ] = isPlainRecord ( extension ) ? extension : { } ;
2026-03-02 09:06:58 -06:00
}
}
2026-03-14 09:46:16 -05:00
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 ;
2026-03-15 16:39:11 -05:00
// All tasks go in top-level tasks/ folder, never nested under projects/
const taskPath = ` tasks/ ${ taskSlug } /TASK.md ` ;
2026-03-14 09:46:16 -05:00
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 } ,
) ;
2026-03-13 22:29:30 -05:00
const resolved = buildManifestFromPackageFiles ( files , {
sourceLabel : {
companyId : company.id ,
companyName : company.name ,
} ,
} ) ;
resolved . manifest . includes = include ;
2026-03-14 09:46:16 -05:00
resolved . manifest . envInputs = dedupeEnvInputs ( envInputs ) ;
2026-03-13 22:29:30 -05:00
resolved . warnings . unshift ( . . . warnings ) ;
2026-03-02 09:06:58 -06:00
return {
2026-03-13 22:29:30 -05:00
rootPath ,
manifest : resolved.manifest ,
2026-03-02 09:06:58 -06:00
files ,
2026-03-13 22:29:30 -05:00
warnings : resolved.warnings ,
2026-03-14 09:46:16 -05:00
paperclipExtensionPath ,
2026-03-02 09:06:58 -06:00
} ;
}
async function buildPreview ( input : CompanyPortabilityPreview ) : Promise < ImportPlanInternal > {
2026-03-16 10:14:09 -05:00
const requestedInclude = normalizeInclude ( input . include ) ;
const source = applySelectedFilesToSource ( await resolveSource ( input . source ) , input . selectedFiles ) ;
2026-03-02 09:06:58 -06:00
const manifest = source . manifest ;
2026-03-16 10:14:09 -05:00
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 ,
} ;
2026-03-02 09:06:58 -06:00
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." ) ;
}
2026-03-13 22:29:30 -05:00
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 ) )
: [ ] ;
2026-03-02 09:06:58 -06:00
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." ) ;
}
2026-03-14 18:59:26 -05:00
const availableSkillSlugs = new Set ( source . manifest . skills . map ( ( skill ) = > skill . slug ) ) ;
2026-03-02 09:06:58 -06:00
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 ) ;
2026-03-14 09:46:16 -05:00
if ( parsed . frontmatter . kind && parsed . frontmatter . kind !== "agent" ) {
2026-03-02 09:06:58 -06:00
warnings . push ( ` Agent markdown ${ filePath } does not declare kind: agent in frontmatter. ` ) ;
}
2026-03-14 18:59:26 -05:00
for ( const skillSlug of agent . skills ) {
if ( ! availableSkillSlugs . has ( skillSlug ) ) {
warnings . push ( ` Agent ${ agent . slug } references skill ${ skillSlug } , but that skill is not present in the package. ` ) ;
}
}
2026-03-02 09:06:58 -06:00
}
2026-03-14 09:46:16 -05:00
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. ` ) ;
}
}
2026-03-02 09:06:58 -06:00
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 < string , { id : string ; name : string } > ( ) ;
const existingSlugs = new Set < string > ( ) ;
2026-03-14 09:46:16 -05:00
const projectPlans : CompanyPortabilityPreviewResult [ "plan" ] [ "projectPlans" ] = [ ] ;
const issuePlans : CompanyPortabilityPreviewResult [ "plan" ] [ "issuePlans" ] = [ ] ;
const existingProjectSlugToProject = new Map < string , { id : string ; name : string } > ( ) ;
const existingProjectSlugs = new Set < string > ( ) ;
2026-03-02 09:06:58 -06:00
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 ) ;
}
2026-03-14 09:46:16 -05:00
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 ) ;
}
2026-03-02 09:06:58 -06:00
}
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." ,
} ) ;
}
2026-03-14 09:46:16 -05:00
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." ,
} ) ;
}
}
2026-03-16 09:21:48 -05:00
// 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 ;
}
}
}
2026-03-16 08:59:43 -05:00
// 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. ` ) ;
}
}
2026-03-14 09:46:16 -05:00
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 ,
} ) ;
}
}
2026-03-02 09:06:58 -06:00
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 ,
2026-03-14 09:46:16 -05:00
projectPlans ,
issuePlans ,
2026-03-02 09:06:58 -06:00
} ,
2026-03-15 15:00:32 -05:00
manifest ,
files : source.files ,
2026-03-14 09:46:16 -05:00
envInputs : manifest.envInputs ? ? [ ] ,
2026-03-02 09:06:58 -06:00
warnings ,
errors ,
} ;
return {
preview ,
source ,
include ,
collisionStrategy ,
selectedAgents ,
} ;
}
async function previewImport ( input : CompanyPortabilityPreview ) : Promise < CompanyPortabilityPreviewResult > {
const plan = await buildPreview ( input ) ;
return plan . preview ;
}
async function importBundle (
input : CompanyPortabilityImport ,
actorUserId : string | null | undefined ,
) : Promise < CompanyPortabilityImportResult > {
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 < string , string > ( ) ;
const existingSlugToAgentId = new Map < string , string > ( ) ;
const existingAgents = await agents . list ( targetCompany . id ) ;
for ( const existing of existingAgents ) {
existingSlugToAgentId . set ( normalizeAgentUrlKey ( existing . name ) ? ? existing . id , existing . id ) ;
}
2026-03-14 09:46:16 -05:00
const importedSlugToProjectId = new Map < string , string > ( ) ;
const existingProjectSlugToId = new Map < string , string > ( ) ;
const existingProjects = await projects . list ( targetCompany . id ) ;
for ( const existing of existingProjects ) {
existingProjectSlugToId . set ( existing . urlKey , existing . id ) ;
}
2026-03-02 09:06:58 -06:00
2026-03-14 18:59:26 -05:00
await companySkills . importPackageFiles ( targetCompany . id , plan . source . files ) ;
2026-03-02 09:06:58 -06:00
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 markdownRaw = plan . source . files [ manifestAgent . path ] ;
if ( ! markdownRaw ) {
warnings . push ( ` Missing AGENTS markdown for ${ manifestAgent . slug } ; imported without prompt template. ` ) ;
}
const markdown = markdownRaw ? parseFrontmatterMarkdown ( markdownRaw ) : { frontmatter : { } , body : "" } ;
2026-03-16 10:28:44 -05:00
const promptTemplate = markdown . body || asString ( ( manifestAgent . adapterConfig as Record < string , unknown > ) . promptTemplate ) || "" ;
// 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 , promptTemplate }
: { . . . manifestAgent . adapterConfig , promptTemplate } as Record < string , unknown > ;
2026-03-14 18:59:26 -05:00
const desiredSkills = manifestAgent . skills ? ? [ ] ;
const adapterConfigWithSkills = writePaperclipSkillSyncPreference (
2026-03-16 10:28:44 -05:00
baseAdapterConfig ,
2026-03-14 18:59:26 -05:00
desiredSkills ,
) ;
2026-03-16 12:17:28 -05:00
delete adapterConfigWithSkills . instructionsFilePath ;
2026-03-02 09:06:58 -06:00
const patch = {
name : planAgent.plannedName ,
role : manifestAgent.role ,
title : manifestAgent.title ,
icon : manifestAgent.icon ,
capabilities : manifestAgent.capabilities ,
reportsTo : null ,
2026-03-16 10:28:44 -05:00
adapterType : effectiveAdapterType ,
2026-03-14 18:59:26 -05:00
adapterConfig : adapterConfigWithSkills ,
2026-03-02 09:06:58 -06:00
runtimeConfig : manifestAgent.runtimeConfig ,
budgetMonthlyCents : manifestAgent.budgetMonthlyCents ,
permissions : manifestAgent.permissions ,
metadata : manifestAgent.metadata ,
} ;
if ( planAgent . action === "update" && planAgent . existingAgentId ) {
const 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 ;
}
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 ;
}
const created = await agents . create ( targetCompany . id , patch ) ;
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 } . ` ) ;
}
}
}
2026-03-14 09:46:16 -05:00
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. ` ) ;
}
}
}
2026-03-02 09:06:58 -06:00
return {
company : {
id : targetCompany.id ,
name : targetCompany.name ,
action : companyAction ,
} ,
agents : resultAgents ,
2026-03-14 09:46:16 -05:00
envInputs : sourceManifest.envInputs ? ? [ ] ,
2026-03-02 09:06:58 -06:00
warnings ,
} ;
}
return {
exportBundle ,
previewImport ,
importBundle ,
} ;
}