Skip to content

Commit 0db2648

Browse files
feat: LLM-27831 Fix skills in 0.130.0
1 parent e4148d9 commit 0db2648

13 files changed

Lines changed: 1168 additions & 19 deletions

package-lock.json

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"@openai/codex": "^0.128.0",
6666
"diff": "^8.0.3",
6767
"open": "^11.0.0",
68-
"vscode-jsonrpc": "^8.2.1"
68+
"vscode-jsonrpc": "^8.2.1",
69+
"yaml": "^2.9.0"
6970
}
7071
}

src/AcpExtensions.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1-
export type ExtMethodRequest = AuthenticationStatusRequest | AuthenticationLogoutRequest
1+
import type {PluginListMarketplaceKind} from "./app-server/v2";
2+
3+
export type ExtMethodRequest =
4+
| AuthenticationStatusRequest
5+
| AuthenticationLogoutRequest
6+
| MarketplaceListRequest
7+
| MarketplaceRemoveRequest
28

39
export function isExtMethodRequest(request: { method: string, params: Record<string, unknown> }): request is ExtMethodRequest {
4-
return request.method === "authentication/status" || request.method === "authentication/logout";
10+
return request.method === "authentication/status"
11+
|| request.method === "authentication/logout"
12+
|| request.method === "marketplace/list"
13+
|| request.method === "marketplace/remove";
514
}
615

716
export type AuthenticationStatusRequest = { method: "authentication/status", params: {} }
817
export type AuthenticationStatusResponse = { type: "api-key" } | { type: "chat-gpt", email: string } | { type: "gateway", name: string } | { type: "unauthenticated" }
918

1019
export type AuthenticationLogoutRequest = { method: "authentication/logout", params: {} }
11-
export type AuthenticationLogoutResponse = {}
20+
export type AuthenticationLogoutResponse = {}
21+
22+
export type MarketplaceListRequest = { method: "marketplace/list", params: { cwd?: string, marketplaceKinds: PluginListMarketplaceKind[] } }
23+
export type MarketplaceListResponse = { marketplaces: Array<{ name: string }> }
24+
25+
export type MarketplaceRemoveRequest = { method: "marketplace/remove", params: { marketplaceName: string } }
26+
export type MarketplaceRemoveResponse = {}

src/CodexAcpClient.ts

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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";
3839
import packageJson from "../package.json";
3940
import type {AuthenticationStatusResponse} from "./AcpExtensions";
41+
import {CODEX_SKILL_FILE_NAME} from "./SkillDirectoryParser";
42+
import {installAdditionalRootSkillMarketplaces} from "./LocalSkillMarketplace";
4043

4144
const 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

656708
function 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)));

src/CodexAcpServer.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,19 @@ export class CodexAcpServer implements acp.Agent {
141141
await this.unstable_logout({});
142142
return {};
143143
}
144+
case "marketplace/list": {
145+
const cwd = typeof methodRequest.params.cwd === "string" ? methodRequest.params.cwd : "";
146+
const marketplaces = await this.runWithProcessCheck(() =>
147+
this.codexAcpClient.listMarketplaces(cwd, methodRequest.params.marketplaceKinds)
148+
);
149+
return {
150+
marketplaces: marketplaces.map((name) => ({name})),
151+
};
152+
}
153+
case "marketplace/remove": {
154+
await this.runWithProcessCheck(() => this.codexAcpClient.removeMarketplace(methodRequest.params.marketplaceName));
155+
return {};
156+
}
144157
}
145158
}
146159

src/CodexAppServerClient.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {
1212
GetAccountResponse,
1313
ListMcpServerStatusParams,
1414
ListMcpServerStatusResponse,
15+
MarketplaceAddParams,
16+
MarketplaceAddResponse,
1517
LoginAccountParams,
1618
LoginAccountResponse,
1719
LogoutAccountResponse,
@@ -21,6 +23,12 @@ import type {
2123
McpServerStatusUpdatedNotification,
2224
ModelListParams,
2325
ModelListResponse,
26+
PluginInstallParams,
27+
PluginInstallResponse,
28+
PluginListParams,
29+
PluginListResponse,
30+
PluginReadParams,
31+
PluginReadResponse,
2432
SkillsListParams,
2533
SkillsListResponse,
2634
ThreadLoadedListParams,
@@ -42,6 +50,8 @@ import type {
4250
CommandExecutionRequestApprovalResponse,
4351
FileChangeRequestApprovalParams,
4452
FileChangeRequestApprovalResponse,
53+
MarketplaceRemoveParams,
54+
MarketplaceRemoveResponse,
4555
} from "./app-server/v2";
4656

4757
export interface ApprovalHandler {
@@ -264,6 +274,26 @@ export class CodexAppServerClient {
264274
return await this.sendRequest({ method: "skills/list", params });
265275
}
266276

277+
async marketplaceAdd(params: MarketplaceAddParams): Promise<MarketplaceAddResponse> {
278+
return await this.sendRequest({ method: "marketplace/add", params });
279+
}
280+
281+
async marketplaceRemove(params: MarketplaceRemoveParams): Promise<MarketplaceRemoveResponse> {
282+
return await this.sendRequest({ method: "marketplace/remove", params });
283+
}
284+
285+
async pluginList(params: PluginListParams): Promise<PluginListResponse> {
286+
return await this.sendRequest({ method: "plugin/list", params });
287+
}
288+
289+
async pluginRead(params: PluginReadParams): Promise<PluginReadResponse> {
290+
return await this.sendRequest({ method: "plugin/read", params });
291+
}
292+
293+
async pluginInstall(params: PluginInstallParams): Promise<PluginInstallResponse> {
294+
return await this.sendRequest({ method: "plugin/install", params });
295+
}
296+
267297
/**
268298
* Registers a notification handler for a specific session.
269299
* Replaces any existing handler for the same session, preventing handler accumulation.

0 commit comments

Comments
 (0)