Deduplicate Codex skill commands (#992)
This commit is contained in:
@@ -11,6 +11,7 @@ import type {
|
|||||||
AgentLaunchContext,
|
AgentLaunchContext,
|
||||||
AgentSession,
|
AgentSession,
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
|
AgentSlashCommand,
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import {
|
import {
|
||||||
@@ -199,6 +200,63 @@ function capturedThreadStartConfig(records: CapturedFakeCodexRecord[]): unknown
|
|||||||
return params?.config;
|
return params?.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listCommandsFromFakeCodex(skills: unknown[]): Promise<AgentSlashCommand[]> {
|
||||||
|
const tempDir = await mkdtemp(path.join(tmpdir(), "codex-command-list-"));
|
||||||
|
const fakeCodexPath = path.join(tempDir, "fake-codex.cjs");
|
||||||
|
writeFileSync(
|
||||||
|
fakeCodexPath,
|
||||||
|
`
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
function resultFor(method) {
|
||||||
|
if (method === "initialize") return {};
|
||||||
|
if (method === "collaborationMode/list") return { data: [] };
|
||||||
|
if (method === "skills/list") {
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
cwd: "/tmp/codex-question-test",
|
||||||
|
skills: ${JSON.stringify(skills)},
|
||||||
|
errors: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error("Unexpected Codex request: " + method);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdin.on("data", (chunk) => {
|
||||||
|
buffer += chunk.toString();
|
||||||
|
for (;;) {
|
||||||
|
const newlineIndex = buffer.indexOf("\\n");
|
||||||
|
if (newlineIndex === -1) break;
|
||||||
|
const line = buffer.slice(0, newlineIndex).trim();
|
||||||
|
buffer = buffer.slice(newlineIndex + 1);
|
||||||
|
if (!line) continue;
|
||||||
|
const message = JSON.parse(line);
|
||||||
|
if (typeof message.id !== "number") continue;
|
||||||
|
try {
|
||||||
|
process.stdout.write(JSON.stringify({ id: message.id, result: resultFor(message.method) }) + "\\n");
|
||||||
|
} catch (error) {
|
||||||
|
process.stdout.write(JSON.stringify({ id: message.id, error: { message: error.message } }) + "\\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = new CodexAppServerAgentClient(createTestLogger(), {
|
||||||
|
command: { mode: "replace", argv: [process.execPath, fakeCodexPath] },
|
||||||
|
});
|
||||||
|
const session = await client.createSession(createConfig());
|
||||||
|
try {
|
||||||
|
return await session.listCommands();
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("Codex app-server provider", () => {
|
describe("Codex app-server provider", () => {
|
||||||
test("getAvailableModes includes auto-review when the Codex version supports it", async () => {
|
test("getAvailableModes includes auto-review when the Codex version supports it", async () => {
|
||||||
const session = createSession({}, { autoReviewEnabled: true });
|
const session = createSession({}, { autoReviewEnabled: true });
|
||||||
@@ -824,6 +882,29 @@ describe("Codex app-server provider", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("deduplicates Codex skill slash commands returned from multiple skill roots", async () => {
|
||||||
|
const commands = await listCommandsFromFakeCodex([
|
||||||
|
{
|
||||||
|
name: "paseo",
|
||||||
|
description: "Shared orchestration skill.",
|
||||||
|
path: "/Users/test/.agents/skills/paseo/SKILL.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "paseo",
|
||||||
|
description: "Shared orchestration skill.",
|
||||||
|
path: "/Users/test/.codex/skills/paseo/SKILL.md",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(commands.filter((command) => command.name === "paseo")).toEqual([
|
||||||
|
{
|
||||||
|
name: "paseo",
|
||||||
|
description: "Shared orchestration skill.",
|
||||||
|
argumentHint: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("maps image prompt blocks to Codex localImage input", async () => {
|
test("maps image prompt blocks to Codex localImage input", async () => {
|
||||||
const input = await codexAppServerTurnInputFromPrompt(
|
const input = await codexAppServerTurnInputFromPrompt(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -2852,7 +2852,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const entries = Array.isArray(response?.data) ? response.data : [];
|
const entries = Array.isArray(response?.data) ? response.data : [];
|
||||||
const skills: Array<{ name: string; description: string; path: string }> = [];
|
const skillsByName = new Map<string, { name: string; description: string; path: string }>();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const entryRecord = toObjectRecord(entry);
|
const entryRecord = toObjectRecord(entry);
|
||||||
const list = Array.isArray(entryRecord?.skills) ? entryRecord.skills : [];
|
const list = Array.isArray(entryRecord?.skills) ? entryRecord.skills : [];
|
||||||
@@ -2860,14 +2860,16 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
const skillRecord = toObjectRecord(skill);
|
const skillRecord = toObjectRecord(skill);
|
||||||
if (typeof skillRecord?.name !== "string" || typeof skillRecord?.path !== "string")
|
if (typeof skillRecord?.name !== "string" || typeof skillRecord?.path !== "string")
|
||||||
continue;
|
continue;
|
||||||
skills.push({
|
if (!skillsByName.has(skillRecord.name)) {
|
||||||
name: skillRecord.name,
|
skillsByName.set(skillRecord.name, {
|
||||||
description: resolveSkillDescription(skillRecord),
|
name: skillRecord.name,
|
||||||
path: skillRecord.path,
|
description: resolveSkillDescription(skillRecord),
|
||||||
});
|
path: skillRecord.path,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.cachedSkills = skills;
|
this.cachedSkills = Array.from(skillsByName.values());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.trace(
|
this.logger.trace(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user