@@ -28,6 +28,7 @@ import type {
2828 GetAccountResponse ,
2929 ListMcpServerStatusResponse ,
3030 Model ,
31+ PluginListMarketplaceKind ,
3132 SkillsListParams ,
3233 SkillsListResponse ,
3334 Thread ,
@@ -37,11 +38,16 @@ import type {
3738} from "./app-server/v2" ;
3839import packageJson from "../package.json" ;
3940import type { AuthenticationStatusResponse } from "./AcpExtensions" ;
41+ import { CODEX_SKILL_FILE_NAME } from "./SkillDirectoryParser" ;
42+ import { installAdditionalRootSkillMarketplaces } from "./LocalSkillMarketplace" ;
4043
4144const FILE_URI_PREFIX = "file://" ;
4245// From the Codex Rust implementation, `codex-rs/core-skills/src/loader.rs` defines `SKILLS_FILENAME = "SKILL.md"` and only parses files whose discovered filename exactly matches that value. The app-server README and bundled skill-creator sample also document `SKILL.md` as the required file for a skill. There is
4346// some UI/mention handling that recognizes paths ending in `SKILL.md`, but the actual loader discovery path is hardcoded around this filename.
44- const SKILL_FILE_NAME = "SKILL.md" ;
47+
48+ type SessionId = string ;
49+ type AdditionalRootPath = string ;
50+ type AdditionalRootPaths = AdditionalRootPath [ ] ;
4551
4652/**
4753 * API for accessing the Codex App Server using ACP requests.
@@ -52,6 +58,7 @@ export class CodexAcpClient {
5258 private readonly config : JsonObject ;
5359 private readonly modelProvider : string | null ;
5460 private gatewayConfig : GatewayConfig | null ;
61+ private readonly additionalRootPathsBySessionId = new Map < SessionId , AdditionalRootPaths > ( ) ;
5562 private pendingLoginCompleted : Promise < AccountLoginCompletedNotification > | null = null ;
5663 private pendingAccountUpdated : Promise < AccountUpdatedNotification > | null = null ;
5764
@@ -188,6 +195,18 @@ export class CodexAcpClient {
188195 await accountUpdatedPromise ;
189196 }
190197
198+ async removeMarketplace ( marketplaceName : string ) : Promise < void > {
199+ await this . codexClient . marketplaceRemove ( { marketplaceName} ) ;
200+ }
201+
202+ async listMarketplaces ( cwd : string , marketplaceKinds : PluginListMarketplaceKind [ ] ) : Promise < string [ ] > {
203+ const pluginList = await this . codexClient . pluginList ( {
204+ cwds : cwd ? [ cwd ] : [ ] ,
205+ marketplaceKinds : marketplaceKinds ,
206+ } ) ;
207+ return pluginList . marketplaces . map ( ( marketplace ) => marketplace . name ) ;
208+ }
209+
191210 async authRequired ( ) : Promise < Boolean > {
192211 if ( this . gatewayConfig != null ) {
193212 // The authentication is already in progress:
@@ -206,7 +225,14 @@ export class CodexAcpClient {
206225 }
207226
208227 async resumeSession ( request : acp . ResumeSessionRequest ) : Promise < SessionMetadata > {
228+ const additionalRootPaths = readAdditionalRootPaths ( request . _meta , request . additionalDirectories ) ;
209229 await this . refreshSkills ( request . cwd , request . _meta ) ;
230+ await installAdditionalRootSkillMarketplaces ( {
231+ codexClient : this . codexClient ,
232+ cwd : request . cwd ,
233+ additionalRootPaths,
234+ } ) ;
235+ this . additionalRootPathsBySessionId . set ( request . sessionId , additionalRootPaths ) ;
210236
211237 const response = await this . codexClient . threadResume ( {
212238 config : await this . createSessionConfig ( request . cwd , request . mcpServers ?? [ ] ) ,
@@ -225,6 +251,15 @@ export class CodexAcpClient {
225251 }
226252
227253 async loadSession ( request : acp . LoadSessionRequest ) : Promise < SessionMetadataWithThread > {
254+ const additionalRootPaths = readAdditionalRootPaths ( request . _meta , request . additionalDirectories ) ;
255+ await this . refreshSkills ( request . cwd ) ;
256+ await installAdditionalRootSkillMarketplaces ( {
257+ codexClient : this . codexClient ,
258+ cwd : request . cwd ,
259+ additionalRootPaths,
260+ } ) ;
261+ this . additionalRootPathsBySessionId . set ( request . sessionId , additionalRootPaths ) ;
262+
228263 const response = await this . codexClient . threadResume ( {
229264 config : await this . createSessionConfig ( request . cwd , request . mcpServers ?? [ ] ) ,
230265 cwd : request . cwd ,
@@ -243,7 +278,13 @@ export class CodexAcpClient {
243278 }
244279
245280 async newSession ( request : acp . NewSessionRequest ) : Promise < SessionMetadata > {
281+ const additionalRootPaths = readAdditionalRootPaths ( request . _meta , request . additionalDirectories ) ;
246282 await this . refreshSkills ( request . cwd , request . _meta ) ;
283+ await installAdditionalRootSkillMarketplaces ( {
284+ codexClient : this . codexClient ,
285+ cwd : request . cwd ,
286+ additionalRootPaths,
287+ } ) ;
247288
248289 const response = await this . codexClient . threadStart ( {
249290 config : await this . createSessionConfig ( request . cwd , request . mcpServers ) ,
@@ -256,6 +297,7 @@ export class CodexAcpClient {
256297 throw new Error ( "Codex did not return any models" ) ;
257298 }
258299 const currentModelId = this . createModelId ( codexModels , response . model , response . reasoningEffort ) . toString ( ) ;
300+ this . additionalRootPathsBySessionId . set ( response . thread . id , additionalRootPaths ) ;
259301 return {
260302 sessionId : response . thread . id ,
261303 currentModelId : currentModelId ,
@@ -395,7 +437,17 @@ export class CodexAcpClient {
395437 const input = buildPromptItems ( request . prompt ) ;
396438 const effort = modelId . effort as ReasoningEffort | null ; //TODO remove unsafe conversion
397439
440+ const additionalRootPaths = mergeAdditionalRootPaths (
441+ this . additionalRootPathsBySessionId . get ( request . sessionId ) ?? [ ] ,
442+ readAdditionalRootPaths ( request . _meta )
443+ ) ;
444+ this . additionalRootPathsBySessionId . set ( request . sessionId , additionalRootPaths ) ;
398445 await this . refreshSkills ( cwd , request . _meta ) ;
446+ await installAdditionalRootSkillMarketplaces ( {
447+ codexClient : this . codexClient ,
448+ cwd,
449+ additionalRootPaths,
450+ } ) ;
399451 return await this . codexClient . runTurn ( {
400452 threadId : request . sessionId ,
401453 input : input ,
@@ -650,11 +702,11 @@ function parseSkillPath(uri: string): string | null {
650702 return null ;
651703 }
652704
653- return path . basename ( filePath ) === SKILL_FILE_NAME ? filePath : null ;
705+ return path . basename ( filePath ) === CODEX_SKILL_FILE_NAME ? filePath : null ;
654706}
655707
656708function readSkillName ( block : acp . ResourceLink , skillPath : string ) : string | null {
657- const rawName = block . name === SKILL_FILE_NAME
709+ const rawName = block . name === CODEX_SKILL_FILE_NAME
658710 ? path . basename ( path . dirname ( skillPath ) )
659711 : block . name ;
660712 const name = rawName . trim ( ) . replace ( / ^ \$ / , "" ) ;
@@ -671,13 +723,21 @@ interface GatewayConfig {
671723 }
672724}
673725
674- function readAdditionalRoots ( meta : Record < string , unknown > | null | undefined ) : string [ ] {
726+ function readAdditionalRootPaths (
727+ meta : Record < string , unknown > | null | undefined ,
728+ additionalDirectories : AdditionalRootPaths | undefined = undefined
729+ ) : AdditionalRootPaths {
675730 const rawRoots = meta ?. [ "additionalRoots" ] ;
676- if ( ! Array . isArray ( rawRoots ) ) {
677- return [ ] ;
678- }
731+ const metaRoots = Array . isArray ( rawRoots ) ? rawRoots : [ ] ;
732+ return normalizeAdditionalRootPaths ( [ ...metaRoots , ...( additionalDirectories ?? [ ] ) ] ) ;
733+ }
734+
735+ function mergeAdditionalRootPaths ( left : AdditionalRootPaths , right : AdditionalRootPaths ) : AdditionalRootPaths {
736+ return normalizeAdditionalRootPaths ( [ ...left , ...right ] ) ;
737+ }
679738
680- return Array . from ( new Set ( rawRoots
739+ function normalizeAdditionalRootPaths ( values : unknown [ ] ) : AdditionalRootPaths {
740+ return Array . from ( new Set ( values
681741 . filter ( ( value ) : value is string => typeof value === "string" )
682742 . map ( value => value . trim ( ) )
683743 . filter ( value => value . length > 0 ) ) ) ;
0 commit comments