2026-03-18 16:23:19 -05:00
import { createHash } from "node:crypto" ;
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 ,
2026-03-19 07:15:36 -05:00
CompanyPortabilityFileEntry ,
2026-03-18 21:54:10 -05:00
CompanyPortabilityExportPreviewResult ,
2026-03-02 09:06:58 -06:00
CompanyPortabilityExportResult ,
CompanyPortabilityImport ,
CompanyPortabilityImportResult ,
CompanyPortabilityInclude ,
CompanyPortabilityManifest ,
CompanyPortabilityPreview ,
CompanyPortabilityPreviewAgentPlan ,
CompanyPortabilityPreviewResult ,
2026-03-14 09:46:16 -05:00
CompanyPortabilityProjectManifestEntry ,
2026-03-23 11:14:01 -05:00
CompanyPortabilityProjectWorkspaceManifestEntry ,
CompanyPortabilityIssueRoutineManifestEntry ,
CompanyPortabilityIssueRoutineTriggerManifestEntry ,
2026-03-14 09:46:16 -05:00
CompanyPortabilityIssueManifestEntry ,
2026-03-23 16:49:46 -05:00
CompanyPortabilitySidebarOrder ,
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 ,
2026-03-23 11:14:01 -05:00
ROUTINE_CATCH_UP_POLICIES ,
ROUTINE_CONCURRENCY_POLICIES ,
ROUTINE_STATUSES ,
ROUTINE_TRIGGER_KINDS ,
ROUTINE_TRIGGER_SIGNING_MODES ,
2026-03-14 09:46:16 -05:00
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" ;
2026-04-01 21:05:48 +00:00
import { ghFetch , gitHubApiBase , resolveRawGitHubUrl } from "./github-fetch.js" ;
2026-03-19 07:15:36 -05:00
import type { StorageService } from "../storage/types.js" ;
2026-03-02 09:06:58 -06:00
import { accessService } from "./access.js" ;
import { agentService } from "./agents.js" ;
2026-03-17 13:42:00 -05:00
import { agentInstructionsService } from "./agent-instructions.js" ;
2026-03-19 07:15:36 -05:00
import { assetService } from "./assets.js" ;
2026-03-17 09:09:37 -05:00
import { generateReadme } from "./company-export-readme.js" ;
2026-03-20 05:51:33 -05:00
import { renderOrgChartPng , type OrgNode } from "../routes/org-chart-svg.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-23 11:14:01 -05:00
import { validateCron } from "./cron.js" ;
2026-03-14 09:46:16 -05:00
import { issueService } from "./issues.js" ;
import { projectService } from "./projects.js" ;
2026-03-23 11:14:01 -05:00
import { routineService } from "./routines.js" ;
2026-03-02 09:06:58 -06:00
2026-03-20 05:51:33 -05:00
/** Build OrgNode tree from manifest agent list (slug + reportsToSlug). */
function buildOrgTreeFromManifest ( agents : CompanyPortabilityManifest [ "agents" ] ) : OrgNode [ ] {
const ROLE_LABELS : Record < string , string > = {
ceo : "Chief Executive" , cto : "Technology" , cmo : "Marketing" ,
cfo : "Finance" , coo : "Operations" , vp : "VP" , manager : "Manager" ,
engineer : "Engineer" , agent : "Agent" ,
} ;
const bySlug = new Map ( agents . map ( ( a ) = > [ a . slug , a ] ) ) ;
const childrenOf = new Map < string | null , typeof agents > ( ) ;
for ( const a of agents ) {
const parent = a . reportsToSlug ? ? null ;
const list = childrenOf . get ( parent ) ? ? [ ] ;
list . push ( a ) ;
childrenOf . set ( parent , list ) ;
}
const build = ( parentSlug : string | null ) : OrgNode [ ] = > {
const members = childrenOf . get ( parentSlug ) ? ? [ ] ;
return members . map ( ( m ) = > ( {
id : m.slug ,
name : m.name ,
role : ROLE_LABELS [ m . role ] ? ? m . role ,
status : "active" ,
reports : build ( m . slug ) ,
} ) ) ;
} ;
// Find roots: agents whose reportsToSlug is null or points to a non-existent slug
const roots = agents . filter ( ( a ) = > ! a . reportsToSlug || ! bySlug . has ( a . reportsToSlug ) ) ;
const rootSlugs = new Set ( roots . map ( ( r ) = > r . slug ) ) ;
// Start from null parent, but also include orphans
const tree = build ( null ) ;
for ( const root of roots ) {
if ( root . reportsToSlug && ! bySlug . has ( root . reportsToSlug ) ) {
// Orphan root (parent slug doesn't exist)
tree . push ( {
id : root.slug ,
name : root.name ,
role : ROLE_LABELS [ root . role ] ? ? root . role ,
status : "active" ,
reports : build ( root . slug ) ,
} ) ;
}
}
return tree ;
}
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-20 06:20:30 -05:00
skills : 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-18 21:54:10 -05:00
function resolveImportMode ( options? : ImportBehaviorOptions ) : ImportMode {
return options ? . mode ? ? "board_full" ;
}
function resolveSkillConflictStrategy ( mode : ImportMode , collisionStrategy : CompanyPortabilityCollisionStrategy ) {
if ( mode === "board_full" ) return "replace" as const ;
return collisionStrategy === "skip" ? "skip" as const : "rename" as const ;
}
function classifyPortableFileKind ( pathValue : string ) : CompanyPortabilityExportPreviewResult [ "fileInventory" ] [ number ] [ "kind" ] {
const normalized = normalizePortablePath ( pathValue ) ;
if ( normalized === "COMPANY.md" ) return "company" ;
if ( normalized === ".paperclip.yaml" || normalized === ".paperclip.yml" ) return "extension" ;
if ( normalized === "README.md" ) return "readme" ;
if ( normalized . startsWith ( "agents/" ) ) return "agent" ;
if ( normalized . startsWith ( "skills/" ) ) return "skill" ;
if ( normalized . startsWith ( "projects/" ) ) return "project" ;
if ( normalized . startsWith ( "tasks/" ) ) return "issue" ;
return "other" ;
}
2026-03-16 18:27:20 -05:00
function normalizeSkillSlug ( value : string | null | undefined ) {
return value ? normalizeAgentUrlKey ( value ) ? ? null : null ;
}
function normalizeSkillKey ( value : string | null | undefined ) {
if ( ! value ) return null ;
const segments = value
. split ( "/" )
. map ( ( segment ) = > normalizeSkillSlug ( segment ) )
. filter ( ( segment ) : segment is string = > Boolean ( segment ) ) ;
return segments . length > 0 ? segments . join ( "/" ) : null ;
}
function readSkillKey ( frontmatter : Record < string , unknown > ) {
const metadata = isPlainRecord ( frontmatter . metadata ) ? frontmatter.metadata : null ;
const paperclip = isPlainRecord ( metadata ? . paperclip ) ? metadata ? . paperclip as Record < string , unknown > : null ;
return normalizeSkillKey (
asString ( frontmatter . key )
? ? asString ( frontmatter . skillKey )
? ? asString ( metadata ? . skillKey )
? ? asString ( metadata ? . canonicalKey )
? ? asString ( metadata ? . paperclipSkillKey )
? ? asString ( paperclip ? . skillKey )
? ? asString ( paperclip ? . key ) ,
) ;
}
function deriveManifestSkillKey (
frontmatter : Record < string , unknown > ,
fallbackSlug : string ,
metadata : Record < string , unknown > | null ,
sourceType : string ,
sourceLocator : string | null ,
) {
const explicit = readSkillKey ( frontmatter ) ;
if ( explicit ) return explicit ;
const slug = normalizeSkillSlug ( asString ( frontmatter . slug ) ? ? fallbackSlug ) ? ? "skill" ;
const sourceKind = asString ( metadata ? . sourceKind ) ;
const owner = normalizeSkillSlug ( asString ( metadata ? . owner ) ) ;
const repo = normalizeSkillSlug ( asString ( metadata ? . repo ) ) ;
2026-03-19 14:15:35 -05:00
if ( ( sourceType === "github" || sourceType === "skills_sh" || sourceKind === "github" || sourceKind === "skills_sh" ) && owner && repo ) {
2026-03-16 18:27:20 -05:00
return ` ${ owner } / ${ repo } / ${ slug } ` ;
}
if ( sourceKind === "paperclip_bundled" ) {
return ` paperclipai/paperclip/ ${ slug } ` ;
}
if ( sourceType === "url" || sourceKind === "url" ) {
try {
const host = normalizeSkillSlug ( sourceLocator ? new URL ( sourceLocator ) . host : null ) ? ? "url" ;
return ` url/ ${ host } / ${ slug } ` ;
} catch {
return ` url/unknown/ ${ slug } ` ;
}
}
return slug ;
}
2026-03-18 16:23:19 -05:00
function hashSkillValue ( value : string ) {
return createHash ( "sha256" ) . update ( value ) . digest ( "hex" ) . slice ( 0 , 8 ) ;
}
2026-03-18 16:54:25 -05:00
function normalizeExportPathSegment ( value : string | null | undefined , preserveCase = false ) {
if ( ! value ) return null ;
const trimmed = value . trim ( ) ;
if ( ! trimmed ) return null ;
const normalized = trimmed
. replace ( /[^A-Za-z0-9._-]+/g , "-" )
. replace ( /-+/g , "-" )
. replace ( /^-+|-+$/g , "" ) ;
if ( ! normalized ) return null ;
return preserveCase ? normalized : normalized.toLowerCase ( ) ;
}
2026-03-18 16:23:19 -05:00
function readSkillSourceKind ( skill : CompanySkill ) {
const metadata = isPlainRecord ( skill . metadata ) ? skill.metadata : null ;
return asString ( metadata ? . sourceKind ) ;
}
2026-03-18 16:54:25 -05:00
function deriveLocalExportNamespace ( skill : CompanySkill , slug : string ) {
const metadata = isPlainRecord ( skill . metadata ) ? skill.metadata : null ;
const candidates = [
asString ( metadata ? . projectName ) ,
asString ( metadata ? . workspaceName ) ,
] ;
if ( skill . sourceLocator ) {
const basename = path . basename ( skill . sourceLocator ) ;
candidates . push ( basename . toLowerCase ( ) === "skill.md" ? path . basename ( path . dirname ( skill . sourceLocator ) ) : basename ) ;
}
for ( const value of candidates ) {
const normalized = normalizeSkillSlug ( value ) ;
if ( normalized && normalized !== slug ) return normalized ;
}
return null ;
}
function derivePrimarySkillExportDir (
skill : CompanySkill ,
slug : string ,
companyIssuePrefix : string | null | undefined ,
) {
const normalizedKey = normalizeSkillKey ( skill . key ) ;
const keySegments = normalizedKey ? . split ( "/" ) ? ? [ ] ;
const primaryNamespace = keySegments [ 0 ] ? ? null ;
if ( primaryNamespace === "company" ) {
const companySegment = normalizeExportPathSegment ( companyIssuePrefix , true )
? ? normalizeExportPathSegment ( keySegments [ 1 ] , true )
? ? "company" ;
return ` skills/company/ ${ companySegment } / ${ slug } ` ;
}
if ( primaryNamespace === "local" ) {
const localNamespace = deriveLocalExportNamespace ( skill , slug ) ;
return localNamespace
? ` skills/local/ ${ localNamespace } / ${ slug } `
: ` skills/local/ ${ slug } ` ;
}
if ( primaryNamespace === "url" ) {
let derivedHost : string | null = keySegments [ 1 ] ? ? null ;
if ( ! derivedHost ) {
try {
derivedHost = normalizeSkillSlug ( skill . sourceLocator ? new URL ( skill . sourceLocator ) . host : null ) ;
} catch {
derivedHost = null ;
}
}
const host = derivedHost ? ? "url" ;
return ` skills/url/ ${ host } / ${ slug } ` ;
}
if ( keySegments . length > 1 ) {
return ` skills/ ${ keySegments . join ( "/" ) } ` ;
}
return ` skills/ ${ slug } ` ;
}
function appendSkillExportDirSuffix ( packageDir : string , suffix : string ) {
const lastSeparator = packageDir . lastIndexOf ( "/" ) ;
if ( lastSeparator < 0 ) return ` ${ packageDir } -- ${ suffix } ` ;
return ` ${ packageDir . slice ( 0 , lastSeparator + 1 ) } ${ packageDir . slice ( lastSeparator + 1 ) } -- ${ suffix } ` ;
}
function deriveSkillExportDirCandidates (
skill : CompanySkill ,
slug : string ,
companyIssuePrefix : string | null | undefined ,
) {
const primaryDir = derivePrimarySkillExportDir ( skill , slug , companyIssuePrefix ) ;
2026-03-18 16:23:19 -05:00
const metadata = isPlainRecord ( skill . metadata ) ? skill.metadata : null ;
const sourceKind = readSkillSourceKind ( skill ) ;
const suffixes = new Set < string > ( ) ;
2026-03-18 16:54:25 -05:00
const pushSuffix = ( value : string | null | undefined , preserveCase = false ) = > {
const normalized = normalizeExportPathSegment ( value , preserveCase ) ;
2026-03-18 16:23:19 -05:00
if ( normalized && normalized !== slug ) {
suffixes . add ( normalized ) ;
}
} ;
if ( sourceKind === "paperclip_bundled" ) {
pushSuffix ( "paperclip" ) ;
}
2026-03-19 14:15:35 -05:00
if ( skill . sourceType === "github" || skill . sourceType === "skills_sh" ) {
2026-03-18 16:23:19 -05:00
pushSuffix ( asString ( metadata ? . repo ) ) ;
pushSuffix ( asString ( metadata ? . owner ) ) ;
2026-03-19 14:15:35 -05:00
pushSuffix ( skill . sourceType === "skills_sh" ? "skills_sh" : "github" ) ;
2026-03-18 16:23:19 -05:00
} else if ( skill . sourceType === "url" ) {
try {
pushSuffix ( skill . sourceLocator ? new URL ( skill . sourceLocator ) . host : null ) ;
} catch {
// Ignore URL parse failures and fall through to generic suffixes.
}
pushSuffix ( "url" ) ;
} else if ( skill . sourceType === "local_path" ) {
pushSuffix ( asString ( metadata ? . projectName ) ) ;
pushSuffix ( asString ( metadata ? . workspaceName ) ) ;
2026-03-18 16:54:25 -05:00
pushSuffix ( deriveLocalExportNamespace ( skill , slug ) ) ;
2026-03-18 16:23:19 -05:00
if ( sourceKind === "managed_local" ) pushSuffix ( "company" ) ;
if ( sourceKind === "project_scan" ) pushSuffix ( "project" ) ;
pushSuffix ( "local" ) ;
} else {
pushSuffix ( sourceKind ) ;
pushSuffix ( "skill" ) ;
}
2026-03-18 16:54:25 -05:00
return [ primaryDir , . . . Array . from ( suffixes , ( suffix ) = > appendSkillExportDirSuffix ( primaryDir , suffix ) ) ] ;
2026-03-18 16:23:19 -05:00
}
2026-03-18 16:54:25 -05:00
function buildSkillExportDirMap ( skills : CompanySkill [ ] , companyIssuePrefix : string | null | undefined ) {
2026-03-18 16:23:19 -05:00
const usedDirs = new Set < string > ( ) ;
const keyToDir = new Map < string , string > ( ) ;
const orderedSkills = [ . . . skills ] . sort ( ( left , right ) = > left . key . localeCompare ( right . key ) ) ;
for ( const skill of orderedSkills ) {
const slug = normalizeSkillSlug ( skill . slug ) ? ? "skill" ;
2026-03-18 16:54:25 -05:00
const candidates = deriveSkillExportDirCandidates ( skill , slug , companyIssuePrefix ) ;
2026-03-18 16:23:19 -05:00
let packageDir = candidates . find ( ( candidate ) = > ! usedDirs . has ( candidate ) ) ? ? null ;
if ( ! packageDir ) {
2026-03-18 16:54:25 -05:00
packageDir = appendSkillExportDirSuffix ( candidates [ 0 ] ? ? ` skills/ ${ slug } ` , hashSkillValue ( skill . key ) ) ;
2026-03-18 16:23:19 -05:00
while ( usedDirs . has ( packageDir ) ) {
2026-03-18 16:54:25 -05:00
packageDir = appendSkillExportDirSuffix (
candidates [ 0 ] ? ? ` skills/ ${ slug } ` ,
hashSkillValue ( ` ${ skill . key } : ${ packageDir } ` ) ,
) ;
2026-03-18 16:23:19 -05:00
}
}
usedDirs . add ( packageDir ) ;
keyToDir . set ( skill . key , packageDir ) ;
}
return keyToDir ;
2026-03-16 18:27:20 -05: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" ) ||
2026-03-20 08:55:10 -05:00
normalized . includes ( "apikey" ) ||
2026-03-14 09:46:16 -05:00
normalized . includes ( "api_key" ) ||
normalized . includes ( "api-key" ) ||
normalized . includes ( "access_token" ) ||
normalized . includes ( "access-token" ) ||
2026-03-20 08:55:10 -05:00
normalized . includes ( "auth" ) ||
2026-03-14 09:46:16 -05:00
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" ) ||
2026-03-20 08:55:10 -05:00
normalized . includes ( "privatekey" ) ||
2026-03-14 09:46:16 -05:00
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 ;
2026-03-19 07:15:36 -05:00
files : Record < string , CompanyPortabilityFileEntry > ;
2026-03-02 09:06:58 -06:00
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 ;
2026-03-23 11:14:01 -05:00
routines? : Record < string , Record < string , unknown > > | null ;
2026-03-14 09:46:16 -05:00
} ;
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 ;
2026-03-23 11:14:01 -05:00
workspaces? : Array < {
id : string ;
name : string ;
sourceType : string ;
cwd : string | null ;
repoUrl : string | null ;
repoRef : string | null ;
defaultRef : string | null ;
visibility : string ;
setupCommand : string | null ;
cleanupCommand : string | null ;
metadata? : Record < string , unknown > | null ;
isPrimary : boolean ;
} > ;
2026-03-14 09:46:16 -05:00
metadata? : Record < string , unknown > | null ;
} ;
type IssueLike = {
id : string ;
identifier : string | null ;
title : string ;
description : string | null ;
projectId : string | null ;
2026-03-23 11:14:01 -05:00
projectWorkspaceId : string | null ;
2026-03-14 09:46:16 -05:00
assigneeAgentId : string | null ;
status : string ;
priority : string ;
labelIds? : string [ ] ;
billingCode : string | null ;
executionWorkspaceSettings : Record < string , unknown > | null ;
assigneeAdapterOverrides : Record < string , unknown > | null ;
} ;
2026-03-23 11:14:01 -05:00
type RoutineLike = NonNullable < Awaited < ReturnType < ReturnType < typeof routineService > [ "getDetail" ] > > > ;
2026-03-02 09:06:58 -06:00
type ImportPlanInternal = {
preview : CompanyPortabilityPreviewResult ;
source : ResolvedSource ;
include : CompanyPortabilityInclude ;
collisionStrategy : CompanyPortabilityCollisionStrategy ;
selectedAgents : CompanyPortabilityAgentManifestEntry [ ] ;
} ;
2026-03-18 21:54:10 -05:00
type ImportMode = "board_full" | "agent_safe" ;
type ImportBehaviorOptions = {
mode? : ImportMode ;
sourceCompanyId? : string | null ;
} ;
2026-03-02 09:06:58 -06:00
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-19 07:15:36 -05:00
const COMPANY_LOGO_CONTENT_TYPE_EXTENSIONS : Record < string , string > = {
"image/gif" : ".gif" ,
"image/jpeg" : ".jpg" ,
"image/png" : ".png" ,
"image/svg+xml" : ".svg" ,
"image/webp" : ".webp" ,
} ;
const COMPANY_LOGO_FILE_NAME = "company-logo" ;
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 ;
}
2026-03-23 11:14:01 -05:00
function asBoolean ( value : unknown ) : boolean | null {
return typeof value === "boolean" ? value : null ;
}
function asInteger ( value : unknown ) : number | null {
return typeof value === "number" && Number . isInteger ( value ) ? value : null ;
}
function normalizeRoutineTriggerExtension ( value : unknown ) : CompanyPortabilityIssueRoutineTriggerManifestEntry | null {
if ( ! isPlainRecord ( value ) ) return null ;
const kind = asString ( value . kind ) ;
if ( ! kind ) return null ;
return {
kind ,
label : asString ( value . label ) ,
enabled : asBoolean ( value . enabled ) ? ? true ,
cronExpression : asString ( value . cronExpression ) ,
timezone : asString ( value . timezone ) ,
signingMode : asString ( value . signingMode ) ,
replayWindowSec : asInteger ( value . replayWindowSec ) ,
} ;
}
function normalizeRoutineExtension ( value : unknown ) : CompanyPortabilityIssueRoutineManifestEntry | null {
if ( ! isPlainRecord ( value ) ) return null ;
const triggers = Array . isArray ( value . triggers )
? value . triggers
. map ( ( entry ) = > normalizeRoutineTriggerExtension ( entry ) )
. filter ( ( entry ) : entry is CompanyPortabilityIssueRoutineTriggerManifestEntry = > entry !== null )
: [ ] ;
const routine = {
concurrencyPolicy : asString ( value . concurrencyPolicy ) ,
catchUpPolicy : asString ( value . catchUpPolicy ) ,
triggers ,
} ;
return stripEmptyValues ( routine ) ? routine : null ;
}
function buildRoutineManifestFromLiveRoutine ( routine : RoutineLike ) : CompanyPortabilityIssueRoutineManifestEntry {
return {
concurrencyPolicy : routine.concurrencyPolicy ,
catchUpPolicy : routine.catchUpPolicy ,
triggers : routine.triggers.map ( ( trigger ) = > ( {
kind : trigger.kind ,
label : trigger.label ? ? null ,
enabled : Boolean ( trigger . enabled ) ,
cronExpression : trigger.kind === "schedule" ? trigger . cronExpression ? ? null : null ,
timezone : trigger.kind === "schedule" ? trigger . timezone ? ? null : null ,
signingMode : trigger.kind === "webhook" ? trigger . signingMode ? ? null : null ,
replayWindowSec : trigger.kind === "webhook" ? trigger . replayWindowSec ? ? null : null ,
} ) ) ,
} ;
}
function containsAbsolutePathFragment ( value : string ) {
return /(^|\s)(\/[^/\s]|[A-Za-z]:[\\/])/ . test ( value ) ;
}
function containsSystemDependentPathValue ( value : unknown ) : boolean {
if ( typeof value === "string" ) {
return path . isAbsolute ( value ) || /^[A-Za-z]:[\\/]/ . test ( value ) || containsAbsolutePathFragment ( value ) ;
}
if ( Array . isArray ( value ) ) {
return value . some ( ( entry ) = > containsSystemDependentPathValue ( entry ) ) ;
}
if ( isPlainRecord ( value ) ) {
return Object . values ( value ) . some ( ( entry ) = > containsSystemDependentPathValue ( entry ) ) ;
}
return false ;
}
function clonePortableRecord ( value : unknown ) {
if ( ! isPlainRecord ( value ) ) return null ;
return structuredClone ( value ) as Record < string , unknown > ;
}
2026-03-23 16:30:28 -05:00
function disableImportedTimerHeartbeat ( runtimeConfig : unknown ) {
const next = clonePortableRecord ( runtimeConfig ) ? ? { } ;
const heartbeat = isPlainRecord ( next . heartbeat ) ? { . . . next . heartbeat } : { } ;
heartbeat . enabled = false ;
next . heartbeat = heartbeat ;
return next ;
}
2026-03-23 11:14:01 -05:00
function normalizePortableProjectWorkspaceExtension (
workspaceKey : string ,
value : unknown ,
) : CompanyPortabilityProjectWorkspaceManifestEntry | null {
if ( ! isPlainRecord ( value ) ) return null ;
const normalizedKey = normalizeAgentUrlKey ( workspaceKey ) ? ? workspaceKey . trim ( ) ;
if ( ! normalizedKey ) return null ;
return {
key : normalizedKey ,
name : asString ( value . name ) ? ? normalizedKey ,
sourceType : asString ( value . sourceType ) ,
repoUrl : asString ( value . repoUrl ) ,
repoRef : asString ( value . repoRef ) ,
defaultRef : asString ( value . defaultRef ) ,
visibility : asString ( value . visibility ) ,
setupCommand : asString ( value . setupCommand ) ,
cleanupCommand : asString ( value . cleanupCommand ) ,
metadata : isPlainRecord ( value . metadata ) ? value.metadata : null ,
isPrimary : asBoolean ( value . isPrimary ) ? ? false ,
} ;
}
function derivePortableProjectWorkspaceKey (
workspace : NonNullable < ProjectLike [ "workspaces" ] > [ number ] ,
usedKeys : Set < string > ,
) {
const baseKey =
normalizeAgentUrlKey ( workspace . name )
? ? normalizeAgentUrlKey ( asString ( workspace . repoUrl ) ? . split ( "/" ) . pop ( ) ? . replace ( /\.git$/i , "" ) ? ? "" )
? ? "workspace" ;
return uniqueSlug ( baseKey , usedKeys ) ;
}
function exportPortableProjectExecutionWorkspacePolicy (
projectSlug : string ,
policy : unknown ,
workspaceKeyById : Map < string , string > ,
warnings : string [ ] ,
) {
const next = clonePortableRecord ( policy ) ;
if ( ! next ) return null ;
const defaultWorkspaceId = asString ( next . defaultProjectWorkspaceId ) ;
if ( defaultWorkspaceId ) {
const defaultWorkspaceKey = workspaceKeyById . get ( defaultWorkspaceId ) ;
if ( defaultWorkspaceKey ) {
next . defaultProjectWorkspaceKey = defaultWorkspaceKey ;
} else {
warnings . push ( ` Project ${ projectSlug } default workspace ${ defaultWorkspaceId } was omitted from export because that workspace is not portable. ` ) ;
}
delete next . defaultProjectWorkspaceId ;
}
const cleaned = stripEmptyValues ( next ) ;
return isPlainRecord ( cleaned ) ? cleaned : null ;
}
function importPortableProjectExecutionWorkspacePolicy (
projectSlug : string ,
policy : Record < string , unknown > | null | undefined ,
workspaceIdByKey : Map < string , string > ,
warnings : string [ ] ,
) {
const next = clonePortableRecord ( policy ) ;
if ( ! next ) return null ;
const defaultWorkspaceKey = asString ( next . defaultProjectWorkspaceKey ) ;
if ( defaultWorkspaceKey ) {
const defaultWorkspaceId = workspaceIdByKey . get ( defaultWorkspaceKey ) ;
if ( defaultWorkspaceId ) {
next . defaultProjectWorkspaceId = defaultWorkspaceId ;
} else {
warnings . push ( ` Project ${ projectSlug } references missing workspace key ${ defaultWorkspaceKey } ; imported execution workspace policy without a default workspace. ` ) ;
}
}
delete next . defaultProjectWorkspaceKey ;
const cleaned = stripEmptyValues ( next ) ;
return isPlainRecord ( cleaned ) ? cleaned : null ;
}
function stripPortableProjectExecutionWorkspaceRefs ( policy : Record < string , unknown > | null | undefined ) {
const next = clonePortableRecord ( policy ) ;
if ( ! next ) return null ;
delete next . defaultProjectWorkspaceId ;
delete next . defaultProjectWorkspaceKey ;
const cleaned = stripEmptyValues ( next ) ;
return isPlainRecord ( cleaned ) ? cleaned : null ;
}
2026-03-23 12:35:08 -05:00
async function readGitOutput ( cwd : string , args : string [ ] ) {
const { stdout } = await execFileAsync ( "git" , [ "-C" , cwd , . . . args ] , { cwd } ) ;
const trimmed = stdout . trim ( ) ;
return trimmed . length > 0 ? trimmed : null ;
}
async function inferPortableWorkspaceGitMetadata ( workspace : NonNullable < ProjectLike [ "workspaces" ] > [ number ] ) {
const cwd = asString ( workspace . cwd ) ;
if ( ! cwd ) {
return {
repoUrl : null ,
repoRef : null ,
defaultRef : null ,
} ;
}
let repoUrl : string | null = null ;
try {
repoUrl = await readGitOutput ( cwd , [ "remote" , "get-url" , "origin" ] ) ;
} catch {
try {
const firstRemote = await readGitOutput ( cwd , [ "remote" ] ) ;
const remoteName = firstRemote ? . split ( "\n" ) . map ( ( entry ) = > entry . trim ( ) ) . find ( Boolean ) ? ? null ;
if ( remoteName ) {
repoUrl = await readGitOutput ( cwd , [ "remote" , "get-url" , remoteName ] ) ;
}
} catch {
repoUrl = null ;
}
}
let repoRef : string | null = null ;
try {
repoRef = await readGitOutput ( cwd , [ "branch" , "--show-current" ] ) ;
} catch {
repoRef = null ;
}
let defaultRef : string | null = null ;
try {
const remoteHead = await readGitOutput ( cwd , [ "symbolic-ref" , "--quiet" , "--short" , "refs/remotes/origin/HEAD" ] ) ;
defaultRef = remoteHead ? . startsWith ( "origin/" ) ? remoteHead . slice ( "origin/" . length ) : remoteHead ;
} catch {
defaultRef = null ;
}
return {
repoUrl ,
repoRef ,
defaultRef ,
} ;
}
async function buildPortableProjectWorkspaces (
2026-03-23 11:14:01 -05:00
projectSlug : string ,
workspaces : ProjectLike [ "workspaces" ] | undefined ,
warnings : string [ ] ,
) {
const exportedWorkspaces : Record < string , Record < string , unknown > > = { } ;
const manifestWorkspaces : CompanyPortabilityProjectWorkspaceManifestEntry [ ] = [ ] ;
const workspaceKeyById = new Map < string , string > ( ) ;
2026-03-23 12:35:08 -05:00
const workspaceKeyBySignature = new Map < string , string > ( ) ;
const manifestWorkspaceByKey = new Map < string , CompanyPortabilityProjectWorkspaceManifestEntry > ( ) ;
2026-03-23 11:14:01 -05:00
const usedKeys = new Set < string > ( ) ;
for ( const workspace of workspaces ? ? [ ] ) {
2026-03-23 12:35:08 -05:00
const inferredGitMetadata =
! asString ( workspace . repoUrl ) || ! asString ( workspace . repoRef ) || ! asString ( workspace . defaultRef )
? await inferPortableWorkspaceGitMetadata ( workspace )
: { repoUrl : null , repoRef : null , defaultRef : null } ;
const repoUrl = asString ( workspace . repoUrl ) ? ? inferredGitMetadata . repoUrl ;
2026-03-23 11:14:01 -05:00
if ( ! repoUrl ) {
warnings . push ( ` Project ${ projectSlug } workspace ${ workspace . name } was omitted from export because it does not have a portable repoUrl. ` ) ;
continue ;
}
2026-03-23 12:35:08 -05:00
const repoRef = asString ( workspace . repoRef ) ? ? inferredGitMetadata . repoRef ;
const defaultRef = asString ( workspace . defaultRef ) ? ? inferredGitMetadata . defaultRef ? ? repoRef ;
const workspaceSignature = JSON . stringify ( {
name : workspace.name ,
repoUrl ,
repoRef ,
defaultRef ,
} ) ;
const existingWorkspaceKey = workspaceKeyBySignature . get ( workspaceSignature ) ;
if ( existingWorkspaceKey ) {
workspaceKeyById . set ( workspace . id , existingWorkspaceKey ) ;
const existingManifestWorkspace = manifestWorkspaceByKey . get ( existingWorkspaceKey ) ;
if ( existingManifestWorkspace && workspace . isPrimary ) {
existingManifestWorkspace . isPrimary = true ;
const existingExtensionWorkspace = exportedWorkspaces [ existingWorkspaceKey ] ;
if ( isPlainRecord ( existingExtensionWorkspace ) ) existingExtensionWorkspace . isPrimary = true ;
}
continue ;
}
2026-03-23 11:14:01 -05:00
const workspaceKey = derivePortableProjectWorkspaceKey ( workspace , usedKeys ) ;
workspaceKeyById . set ( workspace . id , workspaceKey ) ;
2026-03-23 12:35:08 -05:00
workspaceKeyBySignature . set ( workspaceSignature , workspaceKey ) ;
2026-03-23 11:14:01 -05:00
let setupCommand = asString ( workspace . setupCommand ) ;
if ( setupCommand && containsAbsolutePathFragment ( setupCommand ) ) {
warnings . push ( ` Project ${ projectSlug } workspace ${ workspaceKey } setupCommand was omitted from export because it is system-dependent. ` ) ;
setupCommand = null ;
}
let cleanupCommand = asString ( workspace . cleanupCommand ) ;
if ( cleanupCommand && containsAbsolutePathFragment ( cleanupCommand ) ) {
warnings . push ( ` Project ${ projectSlug } workspace ${ workspaceKey } cleanupCommand was omitted from export because it is system-dependent. ` ) ;
cleanupCommand = null ;
}
const metadata = isPlainRecord ( workspace . metadata ) && ! containsSystemDependentPathValue ( workspace . metadata )
? workspace . metadata
: null ;
if ( isPlainRecord ( workspace . metadata ) && metadata == null ) {
warnings . push ( ` Project ${ projectSlug } workspace ${ workspaceKey } metadata was omitted from export because it contains system-dependent paths. ` ) ;
}
const portableWorkspace = stripEmptyValues ( {
name : workspace.name ,
sourceType : workspace.sourceType ,
repoUrl ,
2026-03-23 12:35:08 -05:00
repoRef ,
defaultRef ,
2026-03-23 11:14:01 -05:00
visibility : asString ( workspace . visibility ) ,
setupCommand ,
cleanupCommand ,
metadata ,
isPrimary : workspace.isPrimary ? true : undefined ,
} ) ;
if ( ! isPlainRecord ( portableWorkspace ) ) continue ;
exportedWorkspaces [ workspaceKey ] = portableWorkspace ;
2026-03-23 12:35:08 -05:00
const manifestWorkspace = {
2026-03-23 11:14:01 -05:00
key : workspaceKey ,
name : workspace.name ,
sourceType : asString ( workspace . sourceType ) ,
repoUrl ,
2026-03-23 12:35:08 -05:00
repoRef ,
defaultRef ,
2026-03-23 11:14:01 -05:00
visibility : asString ( workspace . visibility ) ,
setupCommand ,
cleanupCommand ,
metadata ,
isPrimary : workspace.isPrimary ,
2026-03-23 12:35:08 -05:00
} ;
manifestWorkspaces . push ( manifestWorkspace ) ;
manifestWorkspaceByKey . set ( workspaceKey , manifestWorkspace ) ;
2026-03-23 11:14:01 -05:00
}
return {
extension : Object.keys ( exportedWorkspaces ) . length > 0 ? exportedWorkspaces : undefined ,
manifest : manifestWorkspaces ,
workspaceKeyById ,
} ;
}
const WEEKDAY_TO_CRON : Record < string , string > = {
sunday : "0" ,
monday : "1" ,
tuesday : "2" ,
wednesday : "3" ,
thursday : "4" ,
friday : "5" ,
saturday : "6" ,
} ;
function readZonedDateParts ( startsAt : string , timeZone : string ) {
try {
const date = new Date ( startsAt ) ;
if ( Number . isNaN ( date . getTime ( ) ) ) return null ;
const formatter = new Intl . DateTimeFormat ( "en-US" , {
timeZone ,
hour12 : false ,
weekday : "long" ,
month : "numeric" ,
day : "numeric" ,
hour : "numeric" ,
minute : "numeric" ,
} ) ;
const parts = Object . fromEntries (
formatter
. formatToParts ( date )
. filter ( ( entry ) = > entry . type !== "literal" )
. map ( ( entry ) = > [ entry . type , entry . value ] ) ,
) as Record < string , string > ;
const weekday = WEEKDAY_TO_CRON [ parts . weekday ? . toLowerCase ( ) ? ? "" ] ;
const month = Number ( parts . month ) ;
const day = Number ( parts . day ) ;
const hour = Number ( parts . hour ) ;
const minute = Number ( parts . minute ) ;
if ( ! weekday || ! Number . isFinite ( month ) || ! Number . isFinite ( day ) || ! Number . isFinite ( hour ) || ! Number . isFinite ( minute ) ) {
return null ;
}
return { weekday , month , day , hour , minute } ;
} catch {
return null ;
}
}
function normalizeCronList ( values : string [ ] ) {
return Array . from ( new Set ( values ) ) . sort ( ( left , right ) = > Number ( left ) - Number ( right ) ) . join ( "," ) ;
}
function buildLegacyRoutineTriggerFromRecurrence (
issue : Pick < CompanyPortabilityIssueManifestEntry , "slug" | "legacyRecurrence" > ,
scheduleValue : unknown ,
) {
const warnings : string [ ] = [ ] ;
const errors : string [ ] = [ ] ;
if ( ! issue . legacyRecurrence || ! isPlainRecord ( issue . legacyRecurrence ) ) {
return { trigger : null , warnings , errors } ;
}
const schedule = isPlainRecord ( scheduleValue ) ? scheduleValue : null ;
const frequency = asString ( issue . legacyRecurrence . frequency ) ;
const interval = asInteger ( issue . legacyRecurrence . interval ) ? ? 1 ;
if ( ! frequency ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy recurrence without frequency; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
if ( interval < 1 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy recurrence with an invalid interval; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
const timezone = asString ( schedule ? . timezone ) ? ? "UTC" ;
const startsAt = asString ( schedule ? . startsAt ) ;
const zonedStartsAt = startsAt ? readZonedDateParts ( startsAt , timezone ) : null ;
if ( startsAt && ! zonedStartsAt ) {
errors . push ( ` Recurring task ${ issue . slug } has an invalid legacy startsAt/timezone combination; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
const time = isPlainRecord ( issue . legacyRecurrence . time ) ? issue.legacyRecurrence.time : null ;
const hour = asInteger ( time ? . hour ) ? ? zonedStartsAt ? . hour ? ? 0 ;
const minute = asInteger ( time ? . minute ) ? ? zonedStartsAt ? . minute ? ? 0 ;
if ( hour < 0 || hour > 23 || minute < 0 || minute > 59 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy recurrence with an invalid time; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
if ( issue . legacyRecurrence . until != null || issue . legacyRecurrence . count != null ) {
warnings . push ( ` Recurring task ${ issue . slug } uses legacy recurrence end bounds; Paperclip will import the routine trigger without those limits. ` ) ;
}
let cronExpression : string | null = null ;
if ( frequency === "hourly" ) {
const hourField = interval === 1
? "*"
: zonedStartsAt
? ` ${ zonedStartsAt . hour } -23/ ${ interval } `
: ` */ ${ interval } ` ;
cronExpression = ` ${ minute } ${ hourField } * * * ` ;
} else if ( frequency === "daily" ) {
if ( Array . isArray ( issue . legacyRecurrence . weekdays ) || Array . isArray ( issue . legacyRecurrence . monthDays ) || Array . isArray ( issue . legacyRecurrence . months ) ) {
errors . push ( ` Recurring task ${ issue . slug } uses unsupported legacy daily recurrence constraints; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
const dayField = interval === 1 ? "*" : ` */ ${ interval } ` ;
cronExpression = ` ${ minute } ${ hour } ${ dayField } * * ` ;
} else if ( frequency === "weekly" ) {
if ( interval !== 1 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy weekly recurrence with interval > 1; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
const weekdays = Array . isArray ( issue . legacyRecurrence . weekdays )
? issue . legacyRecurrence . weekdays
. map ( ( entry ) = > asString ( entry ) )
. filter ( ( entry ) : entry is string = > Boolean ( entry ) )
: [ ] ;
const cronWeekdays = weekdays
. map ( ( entry ) = > WEEKDAY_TO_CRON [ entry . toLowerCase ( ) ] )
. filter ( ( entry ) : entry is string = > Boolean ( entry ) ) ;
if ( cronWeekdays . length === 0 && zonedStartsAt ? . weekday ) {
cronWeekdays . push ( zonedStartsAt . weekday ) ;
}
if ( cronWeekdays . length === 0 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy weekly recurrence without weekdays; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
cronExpression = ` ${ minute } ${ hour } * * ${ normalizeCronList ( cronWeekdays ) } ` ;
} else if ( frequency === "monthly" ) {
if ( interval !== 1 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy monthly recurrence with interval > 1; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
if ( Array . isArray ( issue . legacyRecurrence . ordinalWeekdays ) && issue . legacyRecurrence . ordinalWeekdays . length > 0 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy ordinal monthly recurrence; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
const monthDays = Array . isArray ( issue . legacyRecurrence . monthDays )
? issue . legacyRecurrence . monthDays
. map ( ( entry ) = > asInteger ( entry ) )
. filter ( ( entry ) : entry is number = > entry != null && entry >= 1 && entry <= 31 )
: [ ] ;
if ( monthDays . length === 0 && zonedStartsAt ? . day ) {
monthDays . push ( zonedStartsAt . day ) ;
}
if ( monthDays . length === 0 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy monthly recurrence without monthDays; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
const months = Array . isArray ( issue . legacyRecurrence . months )
? issue . legacyRecurrence . months
. map ( ( entry ) = > asInteger ( entry ) )
. filter ( ( entry ) : entry is number = > entry != null && entry >= 1 && entry <= 12 )
: [ ] ;
const monthField = months . length > 0 ? normalizeCronList ( months . map ( String ) ) : "*" ;
cronExpression = ` ${ minute } ${ hour } ${ normalizeCronList ( monthDays . map ( String ) ) } ${ monthField } * ` ;
} else if ( frequency === "yearly" ) {
if ( interval !== 1 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy yearly recurrence with interval > 1; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
const months = Array . isArray ( issue . legacyRecurrence . months )
? issue . legacyRecurrence . months
. map ( ( entry ) = > asInteger ( entry ) )
. filter ( ( entry ) : entry is number = > entry != null && entry >= 1 && entry <= 12 )
: [ ] ;
if ( months . length === 0 && zonedStartsAt ? . month ) {
months . push ( zonedStartsAt . month ) ;
}
const monthDays = Array . isArray ( issue . legacyRecurrence . monthDays )
? issue . legacyRecurrence . monthDays
. map ( ( entry ) = > asInteger ( entry ) )
. filter ( ( entry ) : entry is number = > entry != null && entry >= 1 && entry <= 31 )
: [ ] ;
if ( monthDays . length === 0 && zonedStartsAt ? . day ) {
monthDays . push ( zonedStartsAt . day ) ;
}
if ( months . length === 0 || monthDays . length === 0 ) {
errors . push ( ` Recurring task ${ issue . slug } uses legacy yearly recurrence without month/monthDay anchors; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
cronExpression = ` ${ minute } ${ hour } ${ normalizeCronList ( monthDays . map ( String ) ) } ${ normalizeCronList ( months . map ( String ) ) } * ` ;
} else {
errors . push ( ` Recurring task ${ issue . slug } uses unsupported legacy recurrence frequency " ${ frequency } "; add .paperclip.yaml routines. ${ issue . slug } .triggers. ` ) ;
return { trigger : null , warnings , errors } ;
}
return {
trigger : {
kind : "schedule" ,
label : "Migrated legacy recurrence" ,
enabled : true ,
cronExpression ,
timezone ,
signingMode : null ,
replayWindowSec : null ,
} satisfies CompanyPortabilityIssueRoutineTriggerManifestEntry ,
warnings ,
errors ,
} ;
}
function resolvePortableRoutineDefinition (
issue : Pick < CompanyPortabilityIssueManifestEntry , "slug" | "recurring" | "routine" | "legacyRecurrence" > ,
scheduleValue : unknown ,
) {
const warnings : string [ ] = [ ] ;
const errors : string [ ] = [ ] ;
if ( ! issue . recurring ) {
return { routine : null , warnings , errors } ;
}
const routine = issue . routine
? {
concurrencyPolicy : issue.routine.concurrencyPolicy ,
catchUpPolicy : issue.routine.catchUpPolicy ,
triggers : [ . . . issue . routine . triggers ] ,
}
: {
concurrencyPolicy : null ,
catchUpPolicy : null ,
triggers : [ ] as CompanyPortabilityIssueRoutineTriggerManifestEntry [ ] ,
} ;
if ( routine . concurrencyPolicy && ! ROUTINE_CONCURRENCY_POLICIES . includes ( routine . concurrencyPolicy as any ) ) {
errors . push ( ` Recurring task ${ issue . slug } uses unsupported routine concurrencyPolicy " ${ routine . concurrencyPolicy } ". ` ) ;
}
if ( routine . catchUpPolicy && ! ROUTINE_CATCH_UP_POLICIES . includes ( routine . catchUpPolicy as any ) ) {
errors . push ( ` Recurring task ${ issue . slug } uses unsupported routine catchUpPolicy " ${ routine . catchUpPolicy } ". ` ) ;
}
for ( const trigger of routine . triggers ) {
if ( ! ROUTINE_TRIGGER_KINDS . includes ( trigger . kind as any ) ) {
errors . push ( ` Recurring task ${ issue . slug } uses unsupported trigger kind " ${ trigger . kind } ". ` ) ;
continue ;
}
if ( trigger . kind === "schedule" ) {
if ( ! trigger . cronExpression || ! trigger . timezone ) {
errors . push ( ` Recurring task ${ issue . slug } has a schedule trigger missing cronExpression/timezone. ` ) ;
continue ;
}
const cronError = validateCron ( trigger . cronExpression ) ;
if ( cronError ) {
errors . push ( ` Recurring task ${ issue . slug } has an invalid schedule trigger: ${ cronError } ` ) ;
}
continue ;
}
if ( trigger . kind === "webhook" && trigger . signingMode && ! ROUTINE_TRIGGER_SIGNING_MODES . includes ( trigger . signingMode as any ) ) {
errors . push ( ` Recurring task ${ issue . slug } uses unsupported webhook signingMode " ${ trigger . signingMode } ". ` ) ;
}
}
if ( routine . triggers . length === 0 && issue . legacyRecurrence ) {
const migrated = buildLegacyRoutineTriggerFromRecurrence ( issue , scheduleValue ) ;
warnings . push ( . . . migrated . warnings ) ;
errors . push ( . . . migrated . errors ) ;
if ( migrated . trigger ) {
routine . triggers . push ( migrated . trigger ) ;
}
}
return { routine , warnings , errors } ;
}
2026-03-02 09:06:58 -06:00
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-20 06:20:30 -05:00
skills : input?.skills ? ? DEFAULT_INCLUDE . skills ,
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 , "/" ) ) ) ;
}
2026-03-19 07:15:36 -05:00
function isPortableBinaryFile (
value : CompanyPortabilityFileEntry ,
) : value is Extract < CompanyPortabilityFileEntry , { encoding : "base64" } > {
return typeof value === "object" && value !== null && value . encoding === "base64" && typeof value . data === "string" ;
}
function readPortableTextFile (
files : Record < string , CompanyPortabilityFileEntry > ,
filePath : string ,
) {
const value = files [ filePath ] ;
return typeof value === "string" ? value : null ;
}
function inferContentTypeFromPath ( filePath : string ) {
const extension = path . posix . extname ( filePath ) . toLowerCase ( ) ;
switch ( extension ) {
case ".gif" :
return "image/gif" ;
case ".jpeg" :
case ".jpg" :
return "image/jpeg" ;
case ".png" :
return "image/png" ;
case ".svg" :
return "image/svg+xml" ;
case ".webp" :
return "image/webp" ;
default :
return null ;
}
}
function resolveCompanyLogoExtension ( contentType : string | null | undefined , originalFilename : string | null | undefined ) {
const fromContentType = contentType ? COMPANY_LOGO_CONTENT_TYPE_EXTENSIONS [ contentType . toLowerCase ( ) ] : null ;
if ( fromContentType ) return fromContentType ;
const extension = originalFilename ? path . extname ( originalFilename ) . toLowerCase ( ) : "" ;
return extension || ".png" ;
}
function portableBinaryFileToBuffer ( entry : Extract < CompanyPortabilityFileEntry , { encoding : "base64" } > ) {
return Buffer . from ( entry . data , "base64" ) ;
}
function portableFileToBuffer ( entry : CompanyPortabilityFileEntry , filePath : string ) {
if ( typeof entry === "string" ) {
return Buffer . from ( entry , "utf8" ) ;
}
if ( isPortableBinaryFile ( entry ) ) {
return portableBinaryFileToBuffer ( entry ) ;
}
throw unprocessable ( ` Unsupported file entry encoding for ${ filePath } ` ) ;
}
function bufferToPortableBinaryFile ( buffer : Buffer , contentType : string | null ) : CompanyPortabilityFileEntry {
return {
encoding : "base64" ,
data : buffer.toString ( "base64" ) ,
contentType ,
} ;
}
async function streamToBuffer ( stream : NodeJS.ReadableStream ) {
const chunks : Buffer [ ] = [ ] ;
for await ( const chunk of stream ) {
chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer.from ( chunk ) ) ;
}
return Buffer . concat ( chunks ) ;
}
2026-03-13 22:29:30 -05:00
function normalizeFileMap (
2026-03-19 07:15:36 -05:00
files : Record < string , CompanyPortabilityFileEntry > ,
2026-03-13 22:29:30 -05:00
rootPath? : string | null ,
2026-03-19 07:15:36 -05:00
) : Record < string , CompanyPortabilityFileEntry > {
2026-03-13 22:29:30 -05:00
const normalizedRoot = rootPath ? normalizePortablePath ( rootPath ) : null ;
2026-03-19 07:15:36 -05:00
const out : Record < string , CompanyPortabilityFileEntry > = { } ;
2026-03-13 22:29:30 -05:00
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-19 07:24:04 -05:00
function pickTextFiles ( files : Record < string , CompanyPortabilityFileEntry > ) {
const out : Record < string , string > = { } ;
for ( const [ filePath , content ] of Object . entries ( files ) ) {
if ( typeof content === "string" ) {
out [ filePath ] = content ;
}
}
return out ;
}
2026-03-18 21:54:10 -05:00
function collectSelectedExportSlugs ( selectedFiles : Set < string > ) {
const agents = new Set < string > ( ) ;
const projects = new Set < string > ( ) ;
const tasks = new Set < string > ( ) ;
for ( const filePath of selectedFiles ) {
const agentMatch = filePath . match ( /^agents\/([^/]+)\// ) ;
if ( agentMatch ) agents . add ( agentMatch [ 1 ] ! ) ;
const projectMatch = filePath . match ( /^projects\/([^/]+)\// ) ;
if ( projectMatch ) projects . add ( projectMatch [ 1 ] ! ) ;
const taskMatch = filePath . match ( /^tasks\/([^/]+)\// ) ;
if ( taskMatch ) tasks . add ( taskMatch [ 1 ] ! ) ;
}
2026-03-23 11:14:01 -05:00
return { agents , projects , tasks , routines : new Set ( tasks ) } ;
2026-03-18 21:54:10 -05:00
}
2026-03-23 16:49:46 -05:00
function normalizePortableSlugList ( value : unknown ) {
if ( ! Array . isArray ( value ) ) return [ ] ;
const seen = new Set < string > ( ) ;
const normalized : string [ ] = [ ] ;
for ( const entry of value ) {
if ( typeof entry !== "string" ) continue ;
const trimmed = entry . trim ( ) ;
if ( ! trimmed || seen . has ( trimmed ) ) continue ;
seen . add ( trimmed ) ;
normalized . push ( trimmed ) ;
}
return normalized ;
}
function normalizePortableSidebarOrder ( value : unknown ) : CompanyPortabilitySidebarOrder | null {
if ( ! isPlainRecord ( value ) ) return null ;
const sidebar = {
agents : normalizePortableSlugList ( value . agents ) ,
projects : normalizePortableSlugList ( value . projects ) ,
2026-03-18 21:54:10 -05:00
} ;
2026-03-23 16:49:46 -05:00
return sidebar . agents . length > 0 || sidebar . projects . length > 0 ? sidebar : null ;
}
2026-03-18 21:54:10 -05:00
2026-03-23 16:49:46 -05:00
function sortAgentsBySidebarOrder < T extends { id : string ; name : string ; reportsTo : string | null } > ( agents : T [ ] ) {
if ( agents . length === 0 ) return [ ] ;
2026-03-18 21:54:10 -05:00
2026-03-23 16:49:46 -05:00
const byId = new Map ( agents . map ( ( agent ) = > [ agent . id , agent ] ) ) ;
const childrenOf = new Map < string | null , T [ ] > ( ) ;
for ( const agent of agents ) {
const parentId = agent . reportsTo && byId . has ( agent . reportsTo ) ? agent.reportsTo : null ;
const siblings = childrenOf . get ( parentId ) ? ? [ ] ;
siblings . push ( agent ) ;
childrenOf . set ( parentId , siblings ) ;
}
2026-03-18 21:54:10 -05:00
2026-03-23 16:49:46 -05:00
for ( const siblings of childrenOf . values ( ) ) {
siblings . sort ( ( left , right ) = > left . name . localeCompare ( right . name ) ) ;
}
2026-03-18 21:54:10 -05:00
2026-03-23 16:49:46 -05:00
const sorted : T [ ] = [ ] ;
const queue = [ . . . ( childrenOf . get ( null ) ? ? [ ] ) ] ;
while ( queue . length > 0 ) {
const agent = queue . shift ( ) ;
if ( ! agent ) continue ;
sorted . push ( agent ) ;
const children = childrenOf . get ( agent . id ) ;
if ( children ) queue . push ( . . . children ) ;
}
2026-03-18 21:54:10 -05:00
2026-03-23 16:49:46 -05:00
return sorted ;
}
function filterPortableExtensionYaml ( yaml : string , selectedFiles : Set < string > ) {
const selected = collectSelectedExportSlugs ( selectedFiles ) ;
const parsed = parseYamlFile ( yaml ) ;
for ( const section of [ "agents" , "projects" , "tasks" , "routines" ] as const ) {
const sectionValue = parsed [ section ] ;
if ( ! isPlainRecord ( sectionValue ) ) continue ;
const sectionSlugs = selected [ section ] ;
const filteredEntries = Object . fromEntries (
Object . entries ( sectionValue ) . filter ( ( [ slug ] ) = > sectionSlugs . has ( slug ) ) ,
) ;
if ( Object . keys ( filteredEntries ) . length > 0 ) {
parsed [ section ] = filteredEntries ;
} else {
delete parsed [ section ] ;
2026-03-18 21:54:10 -05:00
}
2026-03-23 16:49:46 -05:00
}
2026-03-18 21:54:10 -05:00
2026-03-23 16:49:46 -05:00
const companySection = parsed . company ;
if ( isPlainRecord ( companySection ) ) {
const logoPath = asString ( companySection . logoPath ) ? ? asString ( companySection . logo ) ;
if ( logoPath && ! selectedFiles . has ( logoPath ) ) {
delete companySection . logoPath ;
delete companySection . logo ;
}
2026-03-18 21:54:10 -05:00
}
2026-03-23 16:49:46 -05:00
const sidebarOrder = normalizePortableSidebarOrder ( parsed . sidebar ) ;
if ( sidebarOrder ) {
const filteredSidebar = stripEmptyValues ( {
agents : sidebarOrder.agents.filter ( ( slug ) = > selected . agents . has ( slug ) ) ,
projects : sidebarOrder.projects.filter ( ( slug ) = > selected . projects . has ( slug ) ) ,
} ) ;
if ( isPlainRecord ( filteredSidebar ) ) {
parsed . sidebar = filteredSidebar ;
} else {
delete parsed . sidebar ;
}
} else {
delete parsed . sidebar ;
2026-03-19 07:24:04 -05:00
}
2026-03-23 16:49:46 -05:00
return buildYamlFile ( parsed , { preserveEmptyStrings : true } ) ;
2026-03-18 21:54:10 -05:00
}
function filterExportFiles (
2026-03-19 07:15:36 -05:00
files : Record < string , CompanyPortabilityFileEntry > ,
2026-03-18 21:54:10 -05:00
selectedFilesInput : string [ ] | undefined ,
paperclipExtensionPath : string ,
) {
if ( ! selectedFilesInput || selectedFilesInput . length === 0 ) {
return files ;
}
const selectedFiles = new Set (
selectedFilesInput
. map ( ( entry ) = > normalizePortablePath ( entry ) )
. filter ( ( entry ) = > entry . length > 0 ) ,
) ;
2026-03-19 07:15:36 -05:00
const filtered : Record < string , CompanyPortabilityFileEntry > = { } ;
2026-03-18 21:54:10 -05:00
for ( const [ filePath , content ] of Object . entries ( files ) ) {
if ( ! selectedFiles . has ( filePath ) ) continue ;
filtered [ filePath ] = content ;
}
2026-03-19 07:15:36 -05:00
const extensionEntry = filtered [ paperclipExtensionPath ] ;
if ( selectedFiles . has ( paperclipExtensionPath ) && typeof extensionEntry === "string" ) {
filtered [ paperclipExtensionPath ] = filterPortableExtensionYaml ( extensionEntry , selectedFiles ) ;
2026-03-18 21:54:10 -05:00
}
return filtered ;
}
2026-03-19 07:15:36 -05:00
function findPaperclipExtensionPath ( files : Record < string , CompanyPortabilityFileEntry > ) {
2026-03-14 09:46:16 -05:00
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" ||
2026-03-17 13:42:00 -05:00
key === "instructionsBundleMode" ||
key === "instructionsRootPath" ||
key === "instructionsEntryFile" ||
2026-03-16 08:55:37 -05:00
key === "promptTemplate" ||
2026-03-26 07:23:44 -05:00
key === "bootstrapPromptTemplate" || // deprecated — kept for backward compat
2026-03-16 08:55:37 -05:00
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 ) ;
2026-03-20 13:40:53 -05:00
const isSensitive = isSensitiveEnvKey ( key ) ;
2026-03-14 09:46:16 -05:00
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 ,
2026-03-20 13:40:53 -05:00
kind : isSensitive ? "secret" : "plain" ,
2026-03-14 09:46:16 -05:00
requirement : "optional" ,
2026-03-20 13:40:53 -05:00
defaultValue : isSensitive ? "" : defaultValue ? ? "" ,
2026-03-14 09:46:16 -05:00
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" ,
2026-03-19 07:24:04 -05:00
"logoPath" ,
2026-03-14 09:46:16 -05:00
"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" ) ;
}
2026-03-19 07:24:04 -05:00
const effectiveFiles : Record < string , CompanyPortabilityFileEntry > = { } ;
2026-03-16 10:14:09 -05:00
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 ,
2026-03-20 06:20:30 -05:00
skills : filtered.manifest.skills.length > 0 ,
2026-03-16 10:14:09 -05:00
} ;
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 } ` ,
} ;
}
2026-03-19 14:15:35 -05:00
if ( skill . sourceType === "github" || skill . sourceType === "skills_sh" ) {
2026-03-14 18:59:26 -05:00
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 ;
2026-03-19 14:15:35 -05:00
return skill . sourceType === "github" || skill . sourceType === "skills_sh" || skill . sourceType === "url" ;
2026-03-14 18:59:26 -05:00
}
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 > = {
2026-03-16 18:27:20 -05:00
key : skill.key ,
slug : skill.slug ,
2026-03-14 18:59:26 -05:00
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
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 ) )
: [ ] ;
2026-03-16 18:27:20 -05:00
if ( sourceEntry ) {
metadata . sources = [ . . . existingSources , sourceEntry ] ;
}
metadata . skillKey = skill . key ;
metadata . paperclipSkillKey = skill . key ;
metadata . paperclip = {
. . . ( isPlainRecord ( metadata . paperclip ) ? metadata . paperclip : { } ) ,
skillKey : skill.key ,
slug : skill.slug ,
} ;
2026-03-14 18:59:26 -05:00
const frontmatter = {
. . . parsed . frontmatter ,
2026-03-16 18:27:20 -05:00
key : skill.key ,
slug : skill.slug ,
2026-03-14 18:59:26 -05:00
metadata ,
} ;
return buildMarkdown ( frontmatter , parsed . body ) ;
}
2026-03-02 10:31:48 -06:00
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-04-01 20:42:48 +00:00
const response = await ghFetch ( url ) ;
2026-03-02 09:06:58 -06:00
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-04-01 20:42:48 +00:00
const response = await ghFetch ( 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-19 07:15:36 -05:00
async function fetchBinary ( url : string ) {
2026-04-01 20:42:48 +00:00
const response = await ghFetch ( url ) ;
2026-03-19 07:15:36 -05:00
if ( ! response . ok ) {
throw unprocessable ( ` Failed to fetch ${ url } : ${ response . status } ` ) ;
}
return Buffer . from ( await response . arrayBuffer ( ) ) ;
}
2026-03-14 09:46:16 -05:00
async function fetchJson < T > ( url : string ) : Promise < T > {
2026-04-01 20:42:48 +00:00
const response = await ghFetch ( url , {
2026-03-14 09:46:16 -05:00
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" )
2026-03-16 18:27:20 -05:00
. map ( ( entry ) = > normalizeSkillKey ( entry ) ? ? entry . trim ( ) )
2026-03-14 18:59:26 -05:00
. filter ( Boolean ) ,
) ) ;
}
2026-03-13 22:29:30 -05:00
function buildManifestFromPackageFiles (
2026-03-19 07:15:36 -05:00
files : Record < string , CompanyPortabilityFileEntry > ,
2026-03-13 22:29:30 -05:00
opts ? : { sourceLabel ? : { companyId : string ; companyName : string } | null } ,
) : ResolvedSource {
const normalizedFiles = normalizeFileMap ( files ) ;
2026-03-19 07:15:36 -05:00
const companyPath = typeof normalizedFiles [ "COMPANY.md" ] === "string"
? normalizedFiles [ "COMPANY.md" ]
: undefined ;
2026-03-13 22:29:30 -05:00
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" ) ;
}
2026-03-19 07:15:36 -05:00
const companyMarkdown = readPortableTextFile ( normalizedFiles , resolvedCompanyPath ) ;
if ( typeof companyMarkdown !== "string" ) {
throw unprocessable ( ` Company package file is not readable as text: ${ resolvedCompanyPath } ` ) ;
}
const companyDoc = parseFrontmatterMarkdown ( companyMarkdown ) ;
2026-03-13 22:29:30 -05:00
const companyFrontmatter = companyDoc . frontmatter ;
2026-03-14 09:46:16 -05:00
const paperclipExtensionPath = findPaperclipExtensionPath ( normalizedFiles ) ;
const paperclipExtension = paperclipExtensionPath
2026-03-19 07:15:36 -05:00
? parseYamlFile ( readPortableTextFile ( normalizedFiles , paperclipExtensionPath ) ? ? "" )
2026-03-14 09:46:16 -05:00
: { } ;
const paperclipCompany = isPlainRecord ( paperclipExtension . company ) ? paperclipExtension . company : { } ;
2026-03-23 16:49:46 -05:00
const paperclipSidebar = normalizePortableSidebarOrder ( paperclipExtension . sidebar ) ;
2026-03-14 09:46:16 -05:00
const paperclipAgents = isPlainRecord ( paperclipExtension . agents ) ? paperclipExtension . agents : { } ;
const paperclipProjects = isPlainRecord ( paperclipExtension . projects ) ? paperclipExtension . projects : { } ;
const paperclipTasks = isPlainRecord ( paperclipExtension . tasks ) ? paperclipExtension . tasks : { } ;
2026-03-23 11:14:01 -05:00
const paperclipRoutines = isPlainRecord ( paperclipExtension . routines ) ? paperclipExtension . routines : { } ;
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-23 11:14:01 -05:00
schemaVersion : 4 ,
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-20 06:20:30 -05:00
skills : skillPaths.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 ) ,
2026-03-19 07:15:36 -05:00
logoPath : asString ( paperclipCompany . logoPath ) ? ? asString ( paperclipCompany . logo ) ,
2026-03-14 09:46:16 -05:00
requireBoardApprovalForNewAgents :
typeof paperclipCompany . requireBoardApprovalForNewAgents === "boolean"
? paperclipCompany . requireBoardApprovalForNewAgents
: readCompanyApprovalDefault ( companyFrontmatter ) ,
2026-03-13 22:29:30 -05:00
} ,
2026-03-23 16:49:46 -05:00
sidebar : paperclipSidebar ,
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 [ ] = [ ] ;
2026-03-19 07:15:36 -05:00
if ( manifest . company ? . logoPath && ! normalizedFiles [ manifest . company . logoPath ] ) {
warnings . push ( ` Referenced company logo file is missing from package: ${ manifest . company . logoPath } ` ) ;
}
2026-03-13 22:29:30 -05:00
for ( const agentPath of agentPaths ) {
2026-03-19 07:15:36 -05:00
const markdownRaw = readPortableTextFile ( normalizedFiles , agentPath ) ;
2026-03-13 22:29:30 -05:00
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 ) {
2026-03-19 07:15:36 -05:00
const markdownRaw = readPortableTextFile ( normalizedFiles , skillPath ) ;
2026-03-14 18:59:26 -05:00
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 ) ;
2026-04-01 20:42:48 +00:00
const sourceHostname = asString ( primarySource ? . hostname ) || "github.com" ;
2026-03-14 18:59:26 -05:00
const [ owner , repoName ] = ( repo ? ? "" ) . split ( "/" ) ;
sourceType = "github" ;
sourceLocator = asString ( primarySource ? . url )
2026-04-01 20:42:48 +00:00
? ? ( repo ? ` https:// ${ sourceHostname } / ${ repo } ${ repoPath ? ` /tree/ ${ trackingRef ? ? commit ? ? "main" } / ${ repoPath } ` : "" } ` : null ) ;
2026-03-14 18:59:26 -05:00
sourceRef = commit ;
normalizedMetadata = owner && repoName
? {
sourceKind : "github" ,
2026-04-01 20:42:48 +00:00
. . . ( sourceHostname !== "github.com" ? { hostname : sourceHostname } : { } ) ,
2026-03-14 18:59:26 -05:00
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" ,
} ;
}
2026-03-16 18:27:20 -05:00
const key = deriveManifestSkillKey ( frontmatter , slug , normalizedMetadata , sourceType , sourceLocator ) ;
2026-03-14 18:59:26 -05:00
manifest . skills . push ( {
2026-03-16 18:27:20 -05:00
key ,
2026-03-14 18:59:26 -05:00
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 ) {
2026-03-19 07:15:36 -05:00
const markdownRaw = readPortableTextFile ( normalizedFiles , projectPath ) ;
2026-03-14 09:46:16 -05:00
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 ] : { } ;
2026-03-23 11:14:01 -05:00
const workspaceExtensions = isPlainRecord ( extension . workspaces ) ? extension . workspaces : { } ;
const workspaces = Object . entries ( workspaceExtensions )
. map ( ( [ workspaceKey , entry ] ) = > normalizePortableProjectWorkspaceExtension ( workspaceKey , entry ) )
. filter ( ( entry ) : entry is CompanyPortabilityProjectWorkspaceManifestEntry = > entry !== null ) ;
2026-03-14 09:46:16 -05:00
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 ,
2026-03-23 11:14:01 -05:00
workspaces ,
2026-03-14 09:46:16 -05:00
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 ) {
2026-03-19 07:15:36 -05:00
const markdownRaw = readPortableTextFile ( normalizedFiles , taskPath ) ;
2026-03-14 09:46:16 -05:00
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 ] : { } ;
2026-03-23 11:14:01 -05:00
const routineExtension = normalizeRoutineExtension ( paperclipRoutines [ slug ] ) ;
const routineExtensionRaw = isPlainRecord ( paperclipRoutines [ slug ] ) ? paperclipRoutines [ slug ] : { } ;
2026-03-14 09:46:16 -05:00
const schedule = isPlainRecord ( frontmatter . schedule ) ? frontmatter.schedule : null ;
2026-03-23 11:14:01 -05:00
const legacyRecurrence = schedule && isPlainRecord ( schedule . recurrence )
2026-03-14 09:46:16 -05:00
? schedule . recurrence
: isPlainRecord ( extension . recurrence )
? extension . recurrence
: null ;
2026-03-23 11:14:01 -05:00
const recurring =
asBoolean ( frontmatter . recurring ) === true
|| routineExtension !== null
|| legacyRecurrence !== null ;
2026-03-14 09:46:16 -05:00
manifest . issues . push ( {
slug ,
identifier : asString ( extension . identifier ) ,
title : asString ( frontmatter . name ) ? ? asString ( frontmatter . title ) ? ? slug ,
path : taskPath ,
projectSlug : asString ( frontmatter . project ) ,
2026-03-23 11:14:01 -05:00
projectWorkspaceKey : asString ( extension . projectWorkspaceKey ) ,
2026-03-14 09:46:16 -05:00
assigneeAgentSlug : asString ( frontmatter . assignee ) ,
description : taskDoc.body || asString ( frontmatter . description ) ,
2026-03-23 11:14:01 -05:00
recurring ,
routine : routineExtension ,
legacyRecurrence ,
status : asString ( extension . status ) ? ? asString ( routineExtensionRaw . status ) ,
priority : asString ( extension . priority ) ? ? asString ( routineExtensionRaw . priority ) ,
2026-03-14 09:46:16 -05:00
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 ,
} ;
}
2026-03-23 06:47:32 -05:00
function normalizeGitHubSourcePath ( value : string | null | undefined ) {
if ( ! value ) return "" ;
return value . trim ( ) . replace ( /\\/g , "/" ) . replace ( /^\/+|\/+$/g , "" ) ;
}
export function parseGitHubSourceUrl ( rawUrl : string ) {
2026-03-02 09:06:58 -06:00
const url = new URL ( rawUrl ) ;
2026-04-01 21:27:10 +00:00
if ( url . protocol !== "https:" ) {
throw unprocessable ( "GitHub source URL must use HTTPS" ) ;
}
2026-04-01 20:42:48 +00:00
const hostname = url . hostname ;
2026-03-02 09:06:58 -06:00
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 , "" ) ;
2026-03-23 06:47:32 -05:00
const queryRef = url . searchParams . get ( "ref" ) ? . trim ( ) ;
const queryPath = normalizeGitHubSourcePath ( url . searchParams . get ( "path" ) ) ;
const queryCompanyPath = normalizeGitHubSourcePath ( url . searchParams . get ( "companyPath" ) ) ;
if ( queryRef || queryPath || queryCompanyPath ) {
const companyPath = queryCompanyPath || [ queryPath , "COMPANY.md" ] . filter ( Boolean ) . join ( "/" ) || "COMPANY.md" ;
let basePath = queryPath ;
if ( ! basePath && companyPath !== "COMPANY.md" ) {
basePath = path . posix . dirname ( companyPath ) ;
if ( basePath === "." ) basePath = "" ;
}
return {
2026-04-01 20:42:48 +00:00
hostname ,
2026-03-23 06:47:32 -05:00
owner ,
repo ,
ref : queryRef || "main" ,
basePath ,
companyPath ,
} ;
}
2026-03-02 09:06:58 -06:00
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-04-01 20:42:48 +00:00
return { hostname , owner , repo , ref , basePath , companyPath } ;
}
2026-03-02 09:06:58 -06:00
2026-03-19 07:15:36 -05:00
export function companyPortabilityService ( db : Db , storage? : StorageService ) {
2026-03-02 09:06:58 -06:00
const companies = companyService ( db ) ;
const agents = agentService ( db ) ;
2026-03-19 07:15:36 -05:00
const assetRecords = assetService ( db ) ;
2026-03-17 13:42:00 -05:00
const instructions = agentInstructionsService ( ) ;
2026-03-02 09:06:58 -06:00
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 (
2026-04-01 20:42:48 +00:00
resolveRawGitHubUrl ( parsed . hostname , 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 (
2026-04-01 20:42:48 +00:00
resolveRawGitHubUrl ( parsed . hostname , 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 ) ) ;
2026-03-19 07:15:36 -05:00
const files : Record < string , CompanyPortabilityFileEntry > = {
2026-03-13 22:29:30 -05:00
[ companyPath ] : companyMarkdown ,
} ;
2026-04-01 20:42:48 +00:00
const apiBase = gitHubApiBase ( parsed . hostname ) ;
2026-03-14 09:46:16 -05:00
const tree = await fetchJson < { tree? : Array < { path : string ; type : string } > } > (
2026-04-01 20:42:48 +00:00
` ${ apiBase } /repos/ ${ parsed . owner } / ${ parsed . repo } /git/trees/ ${ ref } ?recursive=1 ` ,
2026-03-14 09:46:16 -05:00
) . 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 (
2026-04-01 20:42:48 +00:00
resolveRawGitHubUrl ( parsed . hostname , parsed . owner , parsed . repo , ref , repoPath ) ,
2026-03-14 09:46:16 -05:00
) ;
}
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-04-01 20:42:48 +00:00
resolveRawGitHubUrl ( parsed . hostname , 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 ) ;
2026-03-19 07:15:36 -05:00
const companyLogoPath = resolved . manifest . company ? . logoPath ;
if ( companyLogoPath && ! resolved . files [ companyLogoPath ] ) {
const repoPath = [ parsed . basePath , companyLogoPath ] . filter ( Boolean ) . join ( "/" ) ;
try {
const binary = await fetchBinary (
2026-04-01 20:42:48 +00:00
resolveRawGitHubUrl ( parsed . hostname , parsed . owner , parsed . repo , ref , repoPath ) ,
2026-03-19 07:15:36 -05:00
) ;
resolved . files [ companyLogoPath ] = bufferToPortableBinaryFile ( binary , inferContentTypeFromPath ( companyLogoPath ) ) ;
} catch ( err ) {
warnings . push ( ` Failed to fetch company logo ${ companyLogoPath } from GitHub: ${ err instanceof Error ? err.message : String ( err ) } ` ) ;
}
}
2026-03-13 22:29:30 -05:00
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 ,
2026-03-18 21:54:10 -05:00
agents : input.agents && input . agents . length > 0 ? true : input . include ? . agents ,
2026-03-14 09:46:16 -05:00
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-20 06:20:30 -05:00
skills : input.skills && input . skills . length > 0 ? true : input . include ? . skills ,
2026-03-14 09:46:16 -05:00
} ) ;
2026-03-02 09:06:58 -06:00
const company = await companies . getById ( companyId ) ;
if ( ! company ) throw notFound ( "Company not found" ) ;
2026-03-19 07:24:04 -05:00
const files : Record < string , CompanyPortabilityFileEntry > = { } ;
2026-03-02 09:06:58 -06:00
const warnings : string [ ] = [ ] ;
2026-03-14 09:46:16 -05:00
const envInputs : CompanyPortabilityManifest [ "envInputs" ] = [ ] ;
2026-03-23 16:49:46 -05:00
const requestedSidebarOrder = normalizePortableSidebarOrder ( input . sidebarOrder ) ;
2026-03-13 22:29:30 -05:00
const rootPath = normalizeAgentUrlKey ( company . name ) ? ? "company-package" ;
2026-03-19 07:24:04 -05:00
let companyLogoPath : string | null = null ;
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-18 21:54:10 -05:00
const liveAgentRows = allAgentRows . filter ( ( agent ) = > agent . status !== "terminated" ) ;
2026-03-20 06:20:30 -05:00
const companySkillRows = include . skills || include . agents ? await companySkills . listFull ( companyId ) : [ ] ;
2026-03-02 10:31:48 -06:00
if ( include . agents ) {
2026-03-18 21:54:10 -05:00
const skipped = allAgentRows . length - liveAgentRows . length ;
2026-03-02 10:31:48 -06:00
if ( skipped > 0 ) {
warnings . push ( ` Skipped ${ skipped } terminated agent ${ skipped === 1 ? "" : "s" } from export. ` ) ;
}
}
2026-03-18 21:54:10 -05:00
const agentByReference = new Map < string , typeof liveAgentRows [ number ] > ( ) ;
for ( const agent of liveAgentRows ) {
agentByReference . set ( agent . id , agent ) ;
agentByReference . set ( agent . name , agent ) ;
const normalizedName = normalizeAgentUrlKey ( agent . name ) ;
if ( normalizedName ) {
agentByReference . set ( normalizedName , agent ) ;
}
}
const selectedAgents = new Map < string , typeof liveAgentRows [ number ] > ( ) ;
for ( const selector of input . agents ? ? [ ] ) {
const trimmed = selector . trim ( ) ;
if ( ! trimmed ) continue ;
const normalized = normalizeAgentUrlKey ( trimmed ) ? ? trimmed ;
const match = agentByReference . get ( trimmed ) ? ? agentByReference . get ( normalized ) ;
if ( ! match ) {
warnings . push ( ` Agent selector " ${ selector } " was not found and was skipped. ` ) ;
continue ;
}
selectedAgents . set ( match . id , match ) ;
}
if ( include . agents && selectedAgents . size === 0 ) {
for ( const agent of liveAgentRows ) {
selectedAgents . set ( agent . id , agent ) ;
}
}
const agentRows = Array . from ( selectedAgents . values ( ) )
. sort ( ( left , right ) = > left . name . localeCompare ( right . name ) ) ;
2026-03-02 10:31:48 -06:00
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 ) ;
2026-03-23 11:14:01 -05:00
const routinesSvc = routineService ( db ) ;
2026-03-18 21:29:42 -05:00
const allProjectsRaw = include . projects || include . issues ? await projectsSvc . list ( companyId ) : [ ] ;
const allProjects = allProjectsRaw . filter ( ( project ) = > ! project . archivedAt ) ;
2026-03-23 11:14:01 -05:00
const allRoutines = include . issues ? await routinesSvc . list ( companyId ) : [ ] ;
2026-03-14 09:46:16 -05:00
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 > > > ( ) ;
2026-03-23 11:14:01 -05:00
const selectedRoutines = new Map < string , typeof allRoutines [ number ] > ( ) ;
const routineById = new Map ( allRoutines . map ( ( routine ) = > [ routine . id , routine ] ) ) ;
2026-03-14 09:46:16 -05:00
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 ) {
2026-03-23 11:14:01 -05:00
const routine = routineById . get ( selector . trim ( ) ) ;
if ( routine ) {
selectedRoutines . set ( routine . id , routine ) ;
if ( routine . projectId ) {
const parentProject = projectById . get ( routine . projectId ) ;
if ( parentProject ) selectedProjects . set ( parentProject . id , parentProject ) ;
}
continue ;
}
2026-03-14 09:46:16 -05:00
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 ) ;
}
2026-03-23 11:14:01 -05:00
for ( const routine of allRoutines . filter ( ( entry ) = > entry . projectId === match . id ) ) {
selectedRoutines . set ( routine . id , routine ) ;
}
2026-03-14 09:46:16 -05:00
}
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 ) ;
}
}
2026-03-23 11:14:01 -05:00
if ( selectedRoutines . size === 0 ) {
for ( const routine of allRoutines ) {
selectedRoutines . set ( routine . id , routine ) ;
if ( routine . projectId ) {
const parentProject = projectById . get ( routine . projectId ) ;
if ( parentProject ) selectedProjects . set ( parentProject . id , parentProject ) ;
}
}
}
2026-03-14 09:46:16 -05:00
}
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 ) ) ;
2026-03-23 11:14:01 -05:00
const selectedRoutineSummaries = Array . from ( selectedRoutines . values ( ) )
. sort ( ( left , right ) = > left . title . localeCompare ( right . title ) ) ;
const selectedRoutineRows = (
await Promise . all ( selectedRoutineSummaries . map ( ( routine ) = > routinesSvc . getDetail ( routine . id ) ) )
) . filter ( ( routine ) : routine is RoutineLike = > routine !== null ) ;
2026-03-14 09:46:16 -05:00
const taskSlugByIssueId = new Map < string , string > ( ) ;
2026-03-23 11:14:01 -05:00
const taskSlugByRoutineId = new Map < string , string > ( ) ;
2026-03-14 09:46:16 -05:00
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 ) ) ;
}
2026-03-23 11:14:01 -05:00
for ( const routine of selectedRoutineRows ) {
const baseSlug = normalizeAgentUrlKey ( routine . title ) ? ? "task" ;
taskSlugByRoutineId . set ( routine . id , uniqueSlug ( baseSlug , usedTaskSlugs ) ) ;
}
2026-03-14 09:46:16 -05:00
const projectSlugById = new Map < string , string > ( ) ;
2026-03-23 11:14:01 -05:00
const projectWorkspaceKeyByProjectId = new Map < string , Map < string , string > > ( ) ;
2026-03-14 09:46:16 -05:00
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 ) ) ;
}
2026-03-23 16:49:46 -05:00
const sidebarOrder = requestedSidebarOrder ? ? stripEmptyValues ( {
agents : sortAgentsBySidebarOrder ( Array . from ( selectedAgents . values ( ) ) )
. map ( ( agent ) = > idToSlug . get ( agent . id ) )
. filter ( ( slug ) : slug is string = > Boolean ( slug ) ) ,
projects : selectedProjectRows
. map ( ( project ) = > projectSlugById . get ( project . id ) )
. filter ( ( slug ) : slug is string = > Boolean ( slug ) ) ,
} ) ;
2026-03-14 09:46:16 -05:00
const companyPath = "COMPANY.md" ;
files [ companyPath ] = buildMarkdown (
{
name : company.name ,
description : company.description ? ? null ,
schema : "agentcompanies/v1" ,
slug : rootPath ,
} ,
2026-03-20 08:05:55 -05:00
"" ,
2026-03-14 09:46:16 -05:00
) ;
2026-03-19 07:24:04 -05:00
if ( include . company && company . logoAssetId ) {
if ( ! storage ) {
warnings . push ( "Skipped company logo from export because storage is unavailable." ) ;
} else {
const logoAsset = await assetRecords . getById ( company . logoAssetId ) ;
if ( ! logoAsset ) {
warnings . push ( ` Skipped company logo ${ company . logoAssetId } because the asset record was not found. ` ) ;
} else {
try {
const object = await storage . getObject ( company . id , logoAsset . objectKey ) ;
const body = await streamToBuffer ( object . stream ) ;
companyLogoPath = ` images/ ${ COMPANY_LOGO_FILE_NAME } ${ resolveCompanyLogoExtension ( logoAsset . contentType , logoAsset . originalFilename ) } ` ;
files [ companyLogoPath ] = bufferToPortableBinaryFile ( body , logoAsset . contentType ) ;
} catch ( err ) {
warnings . push ( ` Failed to export company logo ${ company . logoAssetId } : ${ err instanceof Error ? err.message : String ( err ) } ` ) ;
}
}
}
}
2026-03-14 09:46:16 -05:00
const paperclipAgentsOut : Record < string , Record < string , unknown > > = { } ;
const paperclipProjectsOut : Record < string , Record < string , unknown > > = { } ;
const paperclipTasksOut : Record < string , Record < string , unknown > > = { } ;
2026-03-23 12:35:08 -05:00
const unportableTaskWorkspaceRefs = new Map < string , { workspaceId : string ; taskSlugs : string [ ] } > ( ) ;
2026-03-23 11:14:01 -05:00
const paperclipRoutinesOut : Record < string , Record < string , unknown > > = { } ;
2026-03-02 09:06:58 -06:00
2026-03-18 21:54:10 -05:00
const skillByReference = new Map < string , typeof companySkillRows [ number ] > ( ) ;
2026-03-14 18:59:26 -05:00
for ( const skill of companySkillRows ) {
2026-03-18 21:54:10 -05:00
skillByReference . set ( skill . id , skill ) ;
skillByReference . set ( skill . key , skill ) ;
skillByReference . set ( skill . slug , skill ) ;
skillByReference . set ( skill . name , skill ) ;
}
const selectedSkills = new Map < string , typeof companySkillRows [ number ] > ( ) ;
for ( const selector of input . skills ? ? [ ] ) {
const trimmed = selector . trim ( ) ;
if ( ! trimmed ) continue ;
const normalized = normalizeSkillKey ( trimmed ) ? ? normalizeSkillSlug ( trimmed ) ? ? trimmed ;
const match = skillByReference . get ( trimmed ) ? ? skillByReference . get ( normalized ) ;
if ( ! match ) {
warnings . push ( ` Skill selector " ${ selector } " was not found and was skipped. ` ) ;
continue ;
}
selectedSkills . set ( match . id , match ) ;
}
if ( selectedSkills . size === 0 ) {
for ( const skill of companySkillRows ) {
selectedSkills . set ( skill . id , skill ) ;
}
}
const selectedSkillRows = Array . from ( selectedSkills . values ( ) )
. sort ( ( left , right ) = > left . key . localeCompare ( right . key ) ) ;
const skillExportDirs = buildSkillExportDirMap ( selectedSkillRows , company . issuePrefix ) ;
for ( const skill of selectedSkillRows ) {
2026-03-18 16:23:19 -05:00
const packageDir = skillExportDirs . get ( skill . key ) ? ? ` skills/ ${ normalizeSkillSlug ( skill . slug ) ? ? "skill" } ` ;
2026-03-14 18:59:26 -05:00
if ( shouldReferenceSkillOnExport ( skill , Boolean ( input . expandReferencedSkills ) ) ) {
2026-03-16 18:27:20 -05:00
files [ ` ${ packageDir } /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 ;
2026-03-16 18:27:20 -05:00
const filePath = ` ${ packageDir } / ${ inventoryEntry . path } ` ;
2026-03-14 18:59:26 -05:00
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 ) ! ;
2026-03-17 13:42:00 -05:00
const exportedInstructions = await instructions . exportFiles ( agent ) ;
warnings . push ( . . . exportedInstructions . warnings ) ;
2026-03-02 09:06:58 -06:00
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-17 13:42:00 -05:00
for ( const [ relativePath , content ] of Object . entries ( exportedInstructions . files ) ) {
const targetPath = ` agents/ ${ slug } / ${ relativePath } ` ;
if ( relativePath === exportedInstructions . entryFile ) {
files [ targetPath ] = buildMarkdown (
stripEmptyValues ( {
name : agent.name ,
title : agent.title ? ? null ,
reportsTo : reportsToSlug ,
skills : desiredSkills.length > 0 ? desiredSkills : undefined ,
} ) as Record < string , unknown > ,
content ,
) ;
} else {
files [ targetPath ] = content ;
}
}
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 ` ;
2026-03-23 12:35:08 -05:00
const portableWorkspaces = await buildPortableProjectWorkspaces ( slug , project . workspaces , warnings ) ;
2026-03-23 11:14:01 -05:00
projectWorkspaceKeyByProjectId . set ( project . id , portableWorkspaces . workspaceKeyById ) ;
2026-03-14 09:46:16 -05:00
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 ,
2026-03-23 11:14:01 -05:00
executionWorkspacePolicy : exportPortableProjectExecutionWorkspacePolicy (
slug ,
project . executionWorkspacePolicy ,
portableWorkspaces . workspaceKeyById ,
warnings ,
) ? ? undefined ,
workspaces : portableWorkspaces.extension ,
2026-03-14 09:46:16 -05:00
} ) ;
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 ;
2026-03-23 11:14:01 -05:00
const projectWorkspaceKey = issue . projectId && issue . projectWorkspaceId
? projectWorkspaceKeyByProjectId . get ( issue . projectId ) ? . get ( issue . projectWorkspaceId ) ? ? null
: null ;
if ( issue . projectWorkspaceId && ! projectWorkspaceKey ) {
2026-03-23 12:35:08 -05:00
const aggregateKey = ` ${ issue . projectId ? ? "no-project" } : ${ issue . projectWorkspaceId } ` ;
const existing = unportableTaskWorkspaceRefs . get ( aggregateKey ) ;
if ( existing ) {
existing . taskSlugs . push ( taskSlug ) ;
} else {
unportableTaskWorkspaceRefs . set ( aggregateKey , {
workspaceId : issue.projectWorkspaceId ,
taskSlugs : [ taskSlug ] ,
} ) ;
}
2026-03-23 11:14:01 -05:00
}
2026-03-14 09:46:16 -05:00
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 ,
2026-03-23 11:14:01 -05:00
projectWorkspaceKey : projectWorkspaceKey ? ? undefined ,
2026-03-14 09:46:16 -05:00
executionWorkspaceSettings : issue.executionWorkspaceSettings ? ? undefined ,
assigneeAdapterOverrides : issue.assigneeAdapterOverrides ? ? undefined ,
} ) ;
paperclipTasksOut [ taskSlug ] = isPlainRecord ( extension ) ? extension : { } ;
}
2026-03-23 12:35:08 -05:00
for ( const { workspaceId , taskSlugs } of unportableTaskWorkspaceRefs . values ( ) ) {
const preview = taskSlugs . slice ( 0 , 4 ) . join ( ", " ) ;
const remainder = taskSlugs . length > 4 ? ` and ${ taskSlugs . length - 4 } more ` : "" ;
warnings . push ( ` Tasks ${ preview } ${ remainder } reference workspace ${ workspaceId } , but that workspace could not be exported portably. ` ) ;
}
2026-03-23 11:14:01 -05:00
for ( const routine of selectedRoutineRows ) {
const taskSlug = taskSlugByRoutineId . get ( routine . id ) ! ;
const projectSlug = projectSlugById . get ( routine . projectId ) ? ? null ;
const taskPath = ` tasks/ ${ taskSlug } /TASK.md ` ;
const assigneeSlug = idToSlug . get ( routine . assigneeAgentId ) ? ? null ;
files [ taskPath ] = buildMarkdown (
{
name : routine.title ,
project : projectSlug ,
assignee : assigneeSlug ,
recurring : true ,
} ,
routine . description ? ? "" ,
) ;
const extension = stripEmptyValues ( {
status : routine.status !== "active" ? routine.status : undefined ,
priority : routine.priority !== "medium" ? routine.priority : undefined ,
concurrencyPolicy : routine.concurrencyPolicy !== "coalesce_if_active" ? routine.concurrencyPolicy : undefined ,
catchUpPolicy : routine.catchUpPolicy !== "skip_missed" ? routine.catchUpPolicy : undefined ,
triggers : routine.triggers.map ( ( trigger ) = > stripEmptyValues ( {
kind : trigger.kind ,
label : trigger.label ? ? null ,
enabled : trigger.enabled ? undefined : false ,
cronExpression : trigger.kind === "schedule" ? trigger . cronExpression ? ? null : undefined ,
timezone : trigger.kind === "schedule" ? trigger . timezone ? ? null : undefined ,
signingMode : trigger.kind === "webhook" && trigger . signingMode !== "bearer" ? trigger . signingMode ? ? null : undefined ,
replayWindowSec : trigger.kind === "webhook" && trigger . replayWindowSec !== 300
? trigger . replayWindowSec ? ? null
: undefined ,
} ) ) ,
} ) ;
paperclipRoutinesOut [ taskSlug ] = isPlainRecord ( extension ) ? extension : { } ;
}
2026-03-14 09:46:16 -05:00
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 ) ,
) ;
2026-03-23 11:14:01 -05:00
const paperclipRoutines = Object . fromEntries (
Object . entries ( paperclipRoutinesOut ) . filter ( ( [ , value ] ) = > isPlainRecord ( value ) && Object . keys ( value ) . length > 0 ) ,
) ;
2026-03-14 09:46:16 -05:00
files [ paperclipExtensionPath ] = buildYamlFile (
{
schema : "paperclip/v1" ,
company : stripEmptyValues ( {
brandColor : company.brandColor ? ? null ,
2026-03-19 07:24:04 -05:00
logoPath : companyLogoPath ,
2026-03-14 09:46:16 -05:00
requireBoardApprovalForNewAgents : company.requireBoardApprovalForNewAgents ? undefined : false ,
} ) ,
2026-03-23 16:49:46 -05:00
sidebar : stripEmptyValues ( sidebarOrder ) ,
2026-03-14 09:46:16 -05:00
agents : Object.keys ( paperclipAgents ) . length > 0 ? paperclipAgents : undefined ,
projects : Object.keys ( paperclipProjects ) . length > 0 ? paperclipProjects : undefined ,
tasks : Object.keys ( paperclipTasks ) . length > 0 ? paperclipTasks : undefined ,
2026-03-23 11:14:01 -05:00
routines : Object.keys ( paperclipRoutines ) . length > 0 ? paperclipRoutines : undefined ,
2026-03-14 09:46:16 -05:00
} ,
{ preserveEmptyStrings : true } ,
) ;
2026-03-18 21:54:10 -05:00
let finalFiles = filterExportFiles ( files , input . selectedFiles , paperclipExtensionPath ) ;
let resolved = buildManifestFromPackageFiles ( finalFiles , {
2026-03-13 22:29:30 -05:00
sourceLabel : {
companyId : company.id ,
companyName : company.name ,
} ,
} ) ;
2026-03-18 21:54:10 -05:00
resolved . manifest . includes = {
company : resolved.manifest.company !== null ,
agents : resolved.manifest.agents.length > 0 ,
projects : resolved.manifest.projects.length > 0 ,
issues : resolved.manifest.issues.length > 0 ,
2026-03-20 06:20:30 -05:00
skills : resolved.manifest.skills.length > 0 ,
2026-03-18 21:54:10 -05:00
} ;
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-16 20:37:05 -05:00
2026-03-20 05:51:33 -05:00
// Generate org chart PNG from manifest agents
if ( resolved . manifest . agents . length > 0 ) {
try {
const orgNodes = buildOrgTreeFromManifest ( resolved . manifest . agents ) ;
const pngBuffer = await renderOrgChartPng ( orgNodes ) ;
finalFiles [ "images/org-chart.png" ] = bufferToPortableBinaryFile ( pngBuffer , "image/png" ) ;
} catch {
// Non-fatal: export still works without the org chart image
}
}
2026-03-18 21:54:10 -05:00
if ( ! input . selectedFiles || input . selectedFiles . some ( ( entry ) = > normalizePortablePath ( entry ) === "README.md" ) ) {
finalFiles [ "README.md" ] = generateReadme ( resolved . manifest , {
companyName : company.name ,
companyDescription : company.description ? ? null ,
} ) ;
}
resolved = buildManifestFromPackageFiles ( finalFiles , {
sourceLabel : {
companyId : company.id ,
companyName : company.name ,
} ,
2026-03-16 20:37:05 -05:00
} ) ;
2026-03-18 21:54:10 -05:00
resolved . manifest . includes = {
company : resolved.manifest.company !== null ,
agents : resolved.manifest.agents.length > 0 ,
projects : resolved.manifest.projects.length > 0 ,
issues : resolved.manifest.issues.length > 0 ,
2026-03-20 06:20:30 -05:00
skills : resolved.manifest.skills.length > 0 ,
2026-03-18 21:54:10 -05:00
} ;
resolved . manifest . envInputs = dedupeEnvInputs ( envInputs ) ;
resolved . warnings . unshift ( . . . warnings ) ;
2026-03-16 20:37:05 -05:00
2026-03-02 09:06:58 -06:00
return {
2026-03-13 22:29:30 -05:00
rootPath ,
manifest : resolved.manifest ,
2026-03-18 21:54:10 -05:00
files : finalFiles ,
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
} ;
}
2026-03-18 21:54:10 -05:00
async function previewExport (
companyId : string ,
input : CompanyPortabilityExport ,
) : Promise < CompanyPortabilityExportPreviewResult > {
const previewInput : CompanyPortabilityExport = {
. . . input ,
include : {
. . . input . include ,
issues :
input . include ? . issues
? ? Boolean ( ( input . issues && input . issues . length > 0 ) || ( input . projectIssues && input . projectIssues . length > 0 ) )
? ? false ,
} ,
} ;
if ( previewInput . include && previewInput . include . issues === undefined ) {
previewInput . include . issues = false ;
}
const exported = await exportBundle ( companyId , previewInput ) ;
return {
. . . exported ,
fileInventory : Object.keys ( exported . files )
. sort ( ( left , right ) = > left . localeCompare ( right ) )
. map ( ( filePath ) = > ( {
path : filePath ,
kind : classifyPortableFileKind ( filePath ) ,
} ) ) ,
counts : {
files : Object.keys ( exported . files ) . length ,
agents : exported.manifest.agents.length ,
skills : exported.manifest.skills.length ,
projects : exported.manifest.projects.length ,
issues : exported.manifest.issues.length ,
} ,
} ;
}
async function buildPreview (
input : CompanyPortabilityPreview ,
options? : ImportBehaviorOptions ,
) : Promise < ImportPlanInternal > {
const mode = resolveImportMode ( options ) ;
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-20 06:20:30 -05:00
skills : requestedInclude.skills && manifest . skills . length > 0 ,
2026-03-16 10:14:09 -05:00
} ;
2026-03-02 09:06:58 -06:00
const collisionStrategy = input . collisionStrategy ? ? DEFAULT_COLLISION_STRATEGY ;
2026-03-18 21:54:10 -05:00
if ( mode === "agent_safe" && collisionStrategy === "replace" ) {
throw unprocessable ( "Safe import routes do not allow replace collision strategy." ) ;
}
2026-03-02 09:06:58 -06:00
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-16 18:27:20 -05:00
const availableSkillKeys = new Set ( source . manifest . skills . map ( ( skill ) = > skill . key ) ) ;
const availableSkillSlugs = new Map < string , CompanyPortabilitySkillManifestEntry [ ] > ( ) ;
for ( const skill of source . manifest . skills ) {
const existing = availableSkillSlugs . get ( skill . slug ) ? ? [ ] ;
existing . push ( skill ) ;
availableSkillSlugs . set ( skill . slug , existing ) ;
}
2026-03-14 18:59:26 -05:00
2026-03-02 09:06:58 -06:00
for ( const agent of selectedAgents ) {
const filePath = ensureMarkdownPath ( agent . path ) ;
2026-03-19 07:24:04 -05:00
const markdown = readPortableTextFile ( source . files , filePath ) ;
2026-03-02 09:06:58 -06:00
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-16 18:27:20 -05:00
for ( const skillRef of agent . skills ) {
const slugMatches = availableSkillSlugs . get ( skillRef ) ? ? [ ] ;
if ( ! availableSkillKeys . has ( skillRef ) && slugMatches . length !== 1 ) {
warnings . push ( ` Agent ${ agent . slug } references skill ${ skillRef } , but that skill is not present in the package. ` ) ;
2026-03-14 18:59:26 -05:00
}
}
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 ) {
2026-03-19 07:24:04 -05:00
const markdown = readPortableTextFile ( source . files , ensureMarkdownPath ( project . path ) ) ;
2026-03-14 09:46:16 -05:00
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 ) {
2026-03-23 11:14:01 -05:00
const projectBySlug = new Map ( manifest . projects . map ( ( project ) = > [ project . slug , project ] ) ) ;
2026-03-14 09:46:16 -05:00
for ( const issue of manifest . issues ) {
2026-03-19 07:24:04 -05:00
const markdown = readPortableTextFile ( source . files , ensureMarkdownPath ( issue . path ) ) ;
2026-03-14 09:46:16 -05:00
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. ` ) ;
}
2026-03-23 11:14:01 -05:00
if ( issue . projectWorkspaceKey ) {
const project = issue . projectSlug ? projectBySlug . get ( issue . projectSlug ) ? ? null : null ;
if ( ! project ) {
warnings . push ( ` Task ${ issue . slug } references workspace key ${ issue . projectWorkspaceKey } , but its project is not present in the package. ` ) ;
} else if ( ! project . workspaces . some ( ( workspace ) = > workspace . key === issue . projectWorkspaceKey ) ) {
warnings . push ( ` Task ${ issue . slug } references missing project workspace key ${ issue . projectWorkspaceKey } . ` ) ;
}
}
if ( issue . recurring ) {
if ( ! issue . projectSlug ) {
errors . push ( ` Recurring task ${ issue . slug } must declare a project to import as a routine. ` ) ;
}
if ( ! issue . assigneeAgentSlug ) {
errors . push ( ` Recurring task ${ issue . slug } must declare an assignee to import as a routine. ` ) ;
}
const resolvedRoutine = resolvePortableRoutineDefinition ( issue , parsed . frontmatter . schedule ) ;
warnings . push ( . . . resolvedRoutine . warnings ) ;
errors . push ( . . . resolvedRoutine . errors ) ;
2026-03-14 09:46:16 -05:00
}
}
}
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-18 21:54:10 -05:00
const existingSkills = await companySkills . listFull ( input . target . companyId ) ;
const existingSkillKeys = new Set ( existingSkills . map ( ( skill ) = > skill . key ) ) ;
const existingSkillSlugs = new Set ( existingSkills . map ( ( skill ) = > normalizeSkillSlug ( skill . slug ) ? ? skill . slug ) ) ;
for ( const skill of manifest . skills ) {
const skillSlug = normalizeSkillSlug ( skill . slug ) ? ? skill . slug ;
if ( existingSkillKeys . has ( skill . key ) || existingSkillSlugs . has ( skillSlug ) ) {
if ( mode === "agent_safe" ) {
warnings . push ( ` Existing skill " ${ skill . slug } " matched during safe import and will ${ collisionStrategy === "skip" ? "be skipped" : "be renamed" } instead of overwritten. ` ) ;
} else if ( collisionStrategy === "replace" ) {
warnings . push ( ` Existing skill " ${ skill . slug } " ( ${ skill . key } ) will be overwritten by import. ` ) ;
}
}
}
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 ;
}
2026-03-18 21:54:10 -05:00
if ( mode === "board_full" && collisionStrategy === "replace" ) {
2026-03-02 09:06:58 -06:00
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 ;
}
2026-03-18 21:54:10 -05:00
if ( mode === "board_full" && collisionStrategy === "replace" ) {
2026-03-14 09:46:16 -05:00
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 ,
2026-03-23 11:14:01 -05:00
reason : manifestIssue.recurring ? "Recurring task will be imported as a routine." : null ,
2026-03-14 09:46:16 -05:00
} ) ;
}
}
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"
2026-03-18 21:54:10 -05:00
: include . company && mode === "board_full"
2026-03-02 09:06:58 -06:00
? "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 ,
} ;
}
2026-03-18 21:54:10 -05:00
async function previewImport (
input : CompanyPortabilityPreview ,
options? : ImportBehaviorOptions ,
) : Promise < CompanyPortabilityPreviewResult > {
const plan = await buildPreview ( input , options ) ;
2026-03-02 09:06:58 -06:00
return plan . preview ;
}
async function importBundle (
input : CompanyPortabilityImport ,
actorUserId : string | null | undefined ,
2026-03-18 21:54:10 -05:00
options? : ImportBehaviorOptions ,
2026-03-02 09:06:58 -06:00
) : Promise < CompanyPortabilityImportResult > {
2026-03-18 21:54:10 -05:00
const mode = resolveImportMode ( options ) ;
const plan = await buildPreview ( input , options ) ;
2026-03-02 09:06:58 -06:00
if ( plan . preview . errors . length > 0 ) {
throw unprocessable ( ` Import preview has errors: ${ plan . preview . errors . join ( "; " ) } ` ) ;
}
2026-03-18 21:54:10 -05:00
if (
mode === "agent_safe"
&& (
plan . preview . plan . companyAction === "update"
|| plan . preview . plan . agentPlans . some ( ( entry ) = > entry . action === "update" )
|| plan . preview . plan . projectPlans . some ( ( entry ) = > entry . action === "update" )
)
) {
throw unprocessable ( "Safe import routes only allow create or skip actions." ) ;
}
2026-03-02 09:06:58 -06:00
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" ) {
2026-03-18 21:54:10 -05:00
if ( mode === "agent_safe" && ! options ? . sourceCompanyId ) {
throw unprocessable ( "Safe new-company imports require a source company context." ) ;
}
if ( mode === "agent_safe" && options ? . sourceCompanyId ) {
const sourceMemberships = await access . listActiveUserMemberships ( options . sourceCompanyId ) ;
if ( sourceMemberships . length === 0 ) {
throw unprocessable ( "Safe new-company import requires at least one active user membership on the source company." ) ;
}
}
2026-03-02 09:06:58 -06:00
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 ,
} ) ;
2026-03-18 21:54:10 -05:00
if ( mode === "agent_safe" && options ? . sourceCompanyId ) {
await access . copyActiveUserMemberships ( options . sourceCompanyId , created . id ) ;
} else {
await access . ensureMembership ( created . id , "user" , actorUserId ? ? "board" , "owner" , "active" ) ;
}
2026-03-02 09:06:58 -06:00
targetCompany = created ;
companyAction = "created" ;
} else {
targetCompany = await companies . getById ( input . target . companyId ) ;
if ( ! targetCompany ) throw notFound ( "Target company not found" ) ;
2026-03-18 21:54:10 -05:00
if ( include . company && sourceManifest . company && mode === "board_full" ) {
2026-03-02 09:06:58 -06:00
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" ) ;
2026-03-19 07:24:04 -05:00
if ( include . company ) {
const logoPath = sourceManifest . company ? . logoPath ? ? null ;
if ( ! logoPath ) {
const cleared = await companies . update ( targetCompany . id , { logoAssetId : null } ) ;
targetCompany = cleared ? ? targetCompany ;
} else {
const logoFile = plan . source . files [ logoPath ] ;
if ( ! logoFile ) {
warnings . push ( ` Skipped company logo import because ${ logoPath } is missing from the package. ` ) ;
} else if ( ! storage ) {
warnings . push ( "Skipped company logo import because storage is unavailable." ) ;
} else {
const contentType = isPortableBinaryFile ( logoFile )
? ( logoFile . contentType ? ? inferContentTypeFromPath ( logoPath ) )
: inferContentTypeFromPath ( logoPath ) ;
if ( ! contentType || ! COMPANY_LOGO_CONTENT_TYPE_EXTENSIONS [ contentType ] ) {
warnings . push ( ` Skipped company logo import for ${ logoPath } because the file type is unsupported. ` ) ;
} else {
try {
const body = portableFileToBuffer ( logoFile , logoPath ) ;
const stored = await storage . putFile ( {
companyId : targetCompany.id ,
namespace : "assets/companies" ,
originalFilename : path.posix.basename ( logoPath ) ,
contentType ,
body ,
} ) ;
const createdAsset = await assetRecords . create ( targetCompany . id , {
provider : stored.provider ,
objectKey : stored.objectKey ,
contentType : stored.contentType ,
byteSize : stored.byteSize ,
sha256 : stored.sha256 ,
originalFilename : stored.originalFilename ,
createdByAgentId : null ,
createdByUserId : actorUserId ? ? null ,
} ) ;
const updated = await companies . update ( targetCompany . id , {
logoAssetId : createdAsset.id ,
} ) ;
targetCompany = updated ? ? targetCompany ;
} catch ( err ) {
warnings . push ( ` Failed to import company logo ${ logoPath } : ${ err instanceof Error ? err.message : String ( err ) } ` ) ;
}
}
}
}
}
2026-03-02 09:06:58 -06:00
const resultAgents : CompanyPortabilityImportResult [ "agents" ] = [ ] ;
2026-03-23 16:49:46 -05:00
const resultProjects : CompanyPortabilityImportResult [ "projects" ] = [ ] ;
2026-03-02 09:06:58 -06:00
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 > ( ) ;
2026-03-23 11:14:01 -05:00
const importedProjectWorkspaceIdByProjectSlug = new Map < string , Map < string , string > > ( ) ;
2026-03-14 09:46:16 -05:00
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-20 06:20:30 -05:00
const importedSkills = include . skills || include . agents
? await companySkills . importPackageFiles ( targetCompany . id , pickTextFiles ( plan . source . files ) , {
onConflict : resolveSkillConflictStrategy ( mode , plan . collisionStrategy ) ,
} )
: [ ] ;
2026-03-18 21:54:10 -05:00
const desiredSkillRefMap = new Map < string , string > ( ) ;
for ( const importedSkill of importedSkills ) {
desiredSkillRefMap . set ( importedSkill . originalKey , importedSkill . skill . key ) ;
desiredSkillRefMap . set ( importedSkill . originalSlug , importedSkill . skill . key ) ;
if ( importedSkill . action === "skipped" ) {
warnings . push ( ` Skipped skill ${ importedSkill . originalSlug } ; existing skill ${ importedSkill . skill . slug } was kept. ` ) ;
} else if ( importedSkill . originalKey !== importedSkill . skill . key ) {
warnings . push ( ` Imported skill ${ importedSkill . originalSlug } as ${ importedSkill . skill . slug } to avoid overwriting an existing skill. ` ) ;
}
}
2026-03-14 18:59:26 -05:00
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 ;
}
2026-03-17 13:42:00 -05:00
const bundlePrefix = ` agents/ ${ manifestAgent . slug } / ` ;
const bundleFiles = Object . fromEntries (
Object . entries ( plan . source . files )
. filter ( ( [ filePath ] ) = > filePath . startsWith ( bundlePrefix ) )
2026-03-19 07:24:04 -05:00
. flatMap ( ( [ filePath , content ] ) = > typeof content === "string"
? [ [ normalizePortablePath ( filePath . slice ( bundlePrefix . length ) ) , content ] as const ]
: [ ] ) ,
2026-03-17 13:42:00 -05:00
) ;
2026-03-19 07:24:04 -05:00
const markdownRaw = bundleFiles [ "AGENTS.md" ] ? ? readPortableTextFile ( plan . source . files , manifestAgent . path ) ;
2026-03-23 20:04:40 -05:00
const entryRelativePath = normalizePortablePath ( manifestAgent . path ) . startsWith ( bundlePrefix )
? normalizePortablePath ( manifestAgent . path ) . slice ( bundlePrefix . length )
: "AGENTS.md" ;
if ( typeof markdownRaw === "string" ) {
const importedInstructionsBody = parseFrontmatterMarkdown ( markdownRaw ) . body ;
bundleFiles [ entryRelativePath ] = importedInstructionsBody ;
2026-03-23 20:56:34 -05:00
if ( entryRelativePath !== "AGENTS.md" ) {
2026-03-23 20:04:40 -05:00
bundleFiles [ "AGENTS.md" ] = importedInstructionsBody ;
}
}
2026-03-17 13:42:00 -05:00
const fallbackPromptTemplate = asString ( ( manifestAgent . adapterConfig as Record < string , unknown > ) . promptTemplate ) || "" ;
if ( ! markdownRaw && fallbackPromptTemplate ) {
bundleFiles [ "AGENTS.md" ] = fallbackPromptTemplate ;
}
if ( ! markdownRaw && ! fallbackPromptTemplate ) {
warnings . push ( ` Missing AGENTS markdown for ${ manifestAgent . slug } ; imported with an empty managed bundle. ` ) ;
2026-03-02 09:06:58 -06:00
}
2026-03-16 10:28:44 -05:00
// Apply adapter overrides from request if present
const adapterOverride = input . adapterOverrides ? . [ planAgent . slug ] ;
const effectiveAdapterType = adapterOverride ? . adapterType ? ? manifestAgent . adapterType ;
const baseAdapterConfig = adapterOverride ? . adapterConfig
2026-03-17 13:42:00 -05:00
? { . . . adapterOverride . adapterConfig }
: { . . . manifestAgent . adapterConfig } as Record < string , unknown > ;
2026-03-16 10:28:44 -05:00
2026-03-18 21:54:10 -05:00
const desiredSkills = ( manifestAgent . skills ? ? [ ] ) . map ( ( skillRef ) = > desiredSkillRefMap . get ( skillRef ) ? ? skillRef ) ;
2026-03-14 18:59:26 -05:00
const adapterConfigWithSkills = writePaperclipSkillSyncPreference (
2026-03-16 10:28:44 -05:00
baseAdapterConfig ,
2026-03-14 18:59:26 -05:00
desiredSkills ,
) ;
2026-03-17 13:42:00 -05:00
delete adapterConfigWithSkills . promptTemplate ;
2026-03-26 07:23:44 -05:00
delete adapterConfigWithSkills . bootstrapPromptTemplate ; // deprecated
2026-03-16 12:17:28 -05:00
delete adapterConfigWithSkills . instructionsFilePath ;
2026-03-17 13:42:00 -05:00
delete adapterConfigWithSkills . instructionsBundleMode ;
delete adapterConfigWithSkills . instructionsRootPath ;
delete adapterConfigWithSkills . instructionsEntryFile ;
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-23 16:30:28 -05:00
runtimeConfig : disableImportedTimerHeartbeat ( manifestAgent . runtimeConfig ) ,
2026-03-02 09:06:58 -06:00
budgetMonthlyCents : manifestAgent.budgetMonthlyCents ,
permissions : manifestAgent.permissions ,
metadata : manifestAgent.metadata ,
} ;
if ( planAgent . action === "update" && planAgent . existingAgentId ) {
2026-03-17 13:42:00 -05:00
let updated = await agents . update ( planAgent . existingAgentId , patch ) ;
2026-03-02 09:06:58 -06:00
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 ;
}
2026-03-17 13:42:00 -05:00
try {
const materialized = await instructions . materializeManagedBundle ( updated , bundleFiles , {
clearLegacyPromptTemplate : true ,
replaceExisting : true ,
} ) ;
updated = await agents . update ( updated . id , { adapterConfig : materialized.adapterConfig } ) ? ? updated ;
} catch ( err ) {
warnings . push ( ` Failed to materialize instructions bundle for ${ manifestAgent . slug } : ${ err instanceof Error ? err.message : String ( err ) } ` ) ;
}
2026-03-02 09:06:58 -06:00
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 ;
}
2026-03-17 13:42:00 -05:00
let created = await agents . create ( targetCompany . id , patch ) ;
2026-03-19 08:14:29 -05:00
await access . ensureMembership ( targetCompany . id , "agent" , created . id , "member" , "active" ) ;
await access . setPrincipalPermission (
targetCompany . id ,
"agent" ,
created . id ,
"tasks:assign" ,
true ,
actorUserId ? ? null ,
) ;
2026-03-17 13:42:00 -05:00
try {
const materialized = await instructions . materializeManagedBundle ( created , bundleFiles , {
clearLegacyPromptTemplate : true ,
replaceExisting : true ,
} ) ;
created = await agents . update ( created . id , { adapterConfig : materialized.adapterConfig } ) ? ? created ;
} catch ( err ) {
warnings . push ( ` Failed to materialize instructions bundle for ${ manifestAgent . slug } : ${ err instanceof Error ? err.message : String ( err ) } ` ) ;
}
2026-03-02 09:06:58 -06:00
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 ;
2026-03-23 16:49:46 -05:00
if ( planProject . action === "skip" ) {
resultProjects . push ( {
slug : planProject.slug ,
id : planProject.existingProjectId ,
action : "skipped" ,
name : planProject.plannedName ,
reason : planProject.reason ,
} ) ;
continue ;
}
2026-03-14 09:46:16 -05:00
const projectLeadAgentId = manifestProject . leadAgentSlug
? importedSlugToAgentId . get ( manifestProject . leadAgentSlug )
? ? existingSlugToAgentId . get ( manifestProject . leadAgentSlug )
? ? null
: null ;
2026-03-23 11:14:01 -05:00
const projectWorkspaceIdByKey = new Map < string , string > ( ) ;
2026-03-14 09:46:16 -05:00
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" ,
2026-03-23 11:14:01 -05:00
executionWorkspacePolicy : stripPortableProjectExecutionWorkspaceRefs ( manifestProject . executionWorkspacePolicy ) ,
2026-03-14 09:46:16 -05:00
} ;
2026-03-23 11:14:01 -05:00
let projectId : string | null = null ;
2026-03-14 09:46:16 -05:00
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 } . ` ) ;
2026-03-23 16:49:46 -05:00
resultProjects . push ( {
slug : planProject.slug ,
id : null ,
action : "skipped" ,
name : planProject.plannedName ,
reason : "Existing target project not found." ,
} ) ;
2026-03-14 09:46:16 -05:00
continue ;
}
2026-03-23 11:14:01 -05:00
projectId = updated . id ;
2026-03-14 09:46:16 -05:00
importedSlugToProjectId . set ( planProject . slug , updated . id ) ;
existingProjectSlugToId . set ( updated . urlKey , updated . id ) ;
2026-03-23 16:49:46 -05:00
resultProjects . push ( {
slug : planProject.slug ,
id : updated.id ,
action : "updated" ,
name : updated.name ,
reason : planProject.reason ,
} ) ;
2026-03-23 11:14:01 -05:00
} else {
const created = await projects . create ( targetCompany . id , projectPatch ) ;
projectId = created . id ;
importedSlugToProjectId . set ( planProject . slug , created . id ) ;
existingProjectSlugToId . set ( created . urlKey , created . id ) ;
2026-03-23 16:49:46 -05:00
resultProjects . push ( {
slug : planProject.slug ,
id : created.id ,
action : "created" ,
name : created.name ,
reason : planProject.reason ,
} ) ;
2026-03-14 09:46:16 -05:00
}
2026-03-23 11:14:01 -05:00
if ( ! projectId ) continue ;
for ( const workspace of manifestProject . workspaces ) {
const createdWorkspace = await projects . createWorkspace ( projectId , {
name : workspace.name ,
sourceType : workspace.sourceType ? ? undefined ,
repoUrl : workspace.repoUrl ? ? undefined ,
repoRef : workspace.repoRef ? ? undefined ,
defaultRef : workspace.defaultRef ? ? undefined ,
visibility : workspace.visibility ? ? undefined ,
setupCommand : workspace.setupCommand ? ? undefined ,
cleanupCommand : workspace.cleanupCommand ? ? undefined ,
metadata : workspace.metadata ? ? undefined ,
isPrimary : workspace.isPrimary ,
} ) ;
if ( ! createdWorkspace ) {
warnings . push ( ` Project ${ planProject . slug } workspace ${ workspace . key } could not be created during import. ` ) ;
continue ;
}
projectWorkspaceIdByKey . set ( workspace . key , createdWorkspace . id ) ;
}
importedProjectWorkspaceIdByProjectSlug . set ( planProject . slug , projectWorkspaceIdByKey ) ;
const hydratedProjectExecutionWorkspacePolicy = importPortableProjectExecutionWorkspacePolicy (
planProject . slug ,
manifestProject . executionWorkspacePolicy ,
projectWorkspaceIdByKey ,
warnings ,
) ;
if ( hydratedProjectExecutionWorkspacePolicy ) {
await projects . update ( projectId , {
executionWorkspacePolicy : hydratedProjectExecutionWorkspacePolicy ,
} ) ;
}
2026-03-14 09:46:16 -05:00
}
}
if ( include . issues ) {
2026-03-23 11:14:01 -05:00
const routines = routineService ( db ) ;
2026-03-14 09:46:16 -05:00
for ( const manifestIssue of sourceManifest . issues ) {
2026-03-19 07:24:04 -05:00
const markdownRaw = readPortableTextFile ( plan . source . files , manifestIssue . path ) ;
2026-03-14 09:46:16 -05:00
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 ;
2026-03-23 11:14:01 -05:00
const projectWorkspaceId = manifestIssue . projectSlug && manifestIssue . projectWorkspaceKey
? importedProjectWorkspaceIdByProjectSlug . get ( manifestIssue . projectSlug ) ? . get ( manifestIssue . projectWorkspaceKey ) ? ? null
: null ;
if ( manifestIssue . projectWorkspaceKey && ! projectWorkspaceId ) {
warnings . push ( ` Task ${ manifestIssue . slug } references workspace key ${ manifestIssue . projectWorkspaceKey } , but that workspace was not imported. ` ) ;
}
if ( manifestIssue . recurring ) {
if ( ! projectId || ! assigneeAgentId ) {
throw unprocessable ( ` Recurring task ${ manifestIssue . slug } is missing the project or assignee required to create a routine. ` ) ;
}
const resolvedRoutine = resolvePortableRoutineDefinition ( manifestIssue , parsed ? . frontmatter . schedule ) ;
if ( resolvedRoutine . errors . length > 0 ) {
throw unprocessable ( ` Recurring task ${ manifestIssue . slug } could not be imported as a routine: ${ resolvedRoutine . errors . join ( "; " ) } ` ) ;
}
warnings . push ( . . . resolvedRoutine . warnings ) ;
const routineDefinition = resolvedRoutine . routine ? ? {
concurrencyPolicy : null ,
catchUpPolicy : null ,
triggers : [ ] ,
} ;
const createdRoutine = await routines . create ( targetCompany . id , {
projectId ,
goalId : null ,
parentIssueId : null ,
title : manifestIssue.title ,
description ,
assigneeAgentId ,
priority : manifestIssue.priority && ISSUE_PRIORITIES . includes ( manifestIssue . priority as any )
? manifestIssue . priority as typeof ISSUE_PRIORITIES [ number ]
: "medium" ,
status : manifestIssue.status && ROUTINE_STATUSES . includes ( manifestIssue . status as any )
? manifestIssue . status as typeof ROUTINE_STATUSES [ number ]
: "active" ,
concurrencyPolicy :
routineDefinition . concurrencyPolicy && ROUTINE_CONCURRENCY_POLICIES . includes ( routineDefinition . concurrencyPolicy as any )
? routineDefinition . concurrencyPolicy as typeof ROUTINE_CONCURRENCY_POLICIES [ number ]
: "coalesce_if_active" ,
catchUpPolicy :
routineDefinition . catchUpPolicy && ROUTINE_CATCH_UP_POLICIES . includes ( routineDefinition . catchUpPolicy as any )
? routineDefinition . catchUpPolicy as typeof ROUTINE_CATCH_UP_POLICIES [ number ]
: "skip_missed" ,
} , {
agentId : null ,
userId : actorUserId ? ? null ,
} ) ;
for ( const trigger of routineDefinition . triggers ) {
if ( trigger . kind === "schedule" ) {
await routines . createTrigger ( createdRoutine . id , {
kind : "schedule" ,
label : trigger.label ,
enabled : trigger.enabled ,
cronExpression : trigger.cronExpression ! ,
timezone : trigger.timezone ! ,
} , {
agentId : null ,
userId : actorUserId ? ? null ,
} ) ;
continue ;
}
if ( trigger . kind === "webhook" ) {
await routines . createTrigger ( createdRoutine . id , {
kind : "webhook" ,
label : trigger.label ,
enabled : trigger.enabled ,
signingMode :
trigger . signingMode && ROUTINE_TRIGGER_SIGNING_MODES . includes ( trigger . signingMode as any )
? trigger . signingMode as typeof ROUTINE_TRIGGER_SIGNING_MODES [ number ]
: "bearer" ,
replayWindowSec : trigger.replayWindowSec ? ? 300 ,
} , {
agentId : null ,
userId : actorUserId ? ? null ,
} ) ;
continue ;
}
await routines . createTrigger ( createdRoutine . id , {
kind : "api" ,
label : trigger.label ,
enabled : trigger.enabled ,
} , {
agentId : null ,
userId : actorUserId ? ? null ,
} ) ;
}
continue ;
}
2026-03-14 09:46:16 -05:00
await issues . create ( targetCompany . id , {
projectId ,
2026-03-23 11:14:01 -05:00
projectWorkspaceId ,
2026-03-14 09:46:16 -05:00
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 : [ ] ,
} ) ;
}
}
2026-03-02 09:06:58 -06:00
return {
company : {
id : targetCompany.id ,
name : targetCompany.name ,
action : companyAction ,
} ,
agents : resultAgents ,
2026-03-23 16:49:46 -05:00
projects : resultProjects ,
2026-03-14 09:46:16 -05:00
envInputs : sourceManifest.envInputs ? ? [ ] ,
2026-03-02 09:06:58 -06:00
warnings ,
} ;
}
return {
exportBundle ,
2026-03-18 21:54:10 -05:00
previewExport ,
2026-03-02 09:06:58 -06:00
previewImport ,
importBundle ,
} ;
}