diff --git a/packages/core/src/memory/skillsSection.spec.ts b/packages/core/src/memory/skillsSection.spec.ts index 0da3272..dc14a02 100644 --- a/packages/core/src/memory/skillsSection.spec.ts +++ b/packages/core/src/memory/skillsSection.spec.ts @@ -5,9 +5,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; // replaced in place, and never disturbs content outside the markers. const files = new Map(); let skillDirs: string[] = []; +let codexSkillDirs: string[] = []; vi.mock("node:fs/promises", () => ({ - readdir: vi.fn(async () => skillDirs.map((name) => ({ name, isDirectory: () => true }))), + readdir: vi.fn(async (dir: string) => (dir === "/codex-skills" ? codexSkillDirs : skillDirs).map((name) => ({ name, isDirectory: () => true }))), readFile: vi.fn(async (f: string) => { if (files.has(f)) return files.get(f)!; throw new Error("ENOENT"); @@ -17,6 +18,7 @@ vi.mock("node:fs/promises", () => ({ vi.mock("../core/paths.js", () => ({ claudeSkillsDir: () => "/skills", + codexSkillsDir: () => "/codex-skills", claudeMemoryDir: () => "/mem", codexAgentsFile: (cwd: string) => `${cwd}/AGENTS.md`, })); @@ -26,7 +28,7 @@ import { updateSkillsSection } from "./skillsSection.js"; const MEMORY = "/mem/MEMORY.md"; describe("memory/skillsSection", () => { - beforeEach(() => { files.clear(); skillDirs = []; }); + beforeEach(() => { files.clear(); skillDirs = []; codexSkillDirs = []; }); it("writes a one-line block naming installed skills (claude MEMORY.md)", async () => { skillDirs = ["code-review", "changelog-generator"]; @@ -35,6 +37,7 @@ describe("memory/skillsSection", () => { expect(out).toContain(""); expect(out).toContain(""); expect(out).toContain("changelog-generator, code-review"); // sorted, comma-joined + expect(out).toContain("~/.claude/skills/"); }); it("says none when no skills are installed", async () => { @@ -56,9 +59,13 @@ describe("memory/skillsSection", () => { }); it("targets AGENTS.md for codex", async () => { - skillDirs = ["x"]; + skillDirs = ["claude-only"]; + codexSkillDirs = ["codex-only"]; await updateSkillsSection("codex", "/proj"); - expect(files.has("/proj/AGENTS.md")).toBe(true); + const out = files.get("/proj/AGENTS.md")!; + expect(out).toContain("codex-only"); + expect(out).toContain("~/.codex/skills/"); + expect(out).not.toContain("claude-only"); expect(files.has(MEMORY)).toBe(false); }); }); diff --git a/packages/core/src/memory/skillsSection.ts b/packages/core/src/memory/skillsSection.ts index f2d5b4d..62cadbd 100644 --- a/packages/core/src/memory/skillsSection.ts +++ b/packages/core/src/memory/skillsSection.ts @@ -18,19 +18,20 @@ import { readdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { claudeSkillsDir, claudeMemoryDir, codexAgentsFile } from "../core/paths.js"; +import { claudeSkillsDir, claudeMemoryDir, codexSkillsDir, codexAgentsFile } from "../core/paths.js"; import { spliceMarkedBlock } from "./convert/codex.js"; const START = ""; const END = ""; /** Read installed skill titles from the local skills dir (one subdir per skill). - * Pure filesystem, no RPC. Both runtimes install the same SKILL.md, so claude's - * dir is the single read. Returns [] when nothing is installed (or the dir is - * missing) — the caller then writes an empty/cleared block. */ -async function installedSkillNames(): Promise { + * Pure filesystem, no RPC. Read the active runtime's skills dir so the memory + * line points to the exact SKILL.md files that runtime can load. Returns [] + * when nothing is installed (or the dir is missing). */ +async function installedSkillNames(cli: "claude" | "codex"): Promise { try { - const entries = await readdir(claudeSkillsDir(), { withFileTypes: true }); + const dir = cli === "claude" ? claudeSkillsDir() : codexSkillsDir(); + const entries = await readdir(dir, { withFileTypes: true }); return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort(); } catch { return []; @@ -41,9 +42,10 @@ async function installedSkillNames(): Promise { * the agent knows they exist and can reach for them. The full SKILL.md body stays * in the skills dir (read on demand) — we only list titles here. Empty list → * a block that says so (keeps the markers present and idempotent). */ -function renderBlock(names: string[]): string { +function renderBlock(cli: "claude" | "codex", names: string[]): string { + const path = cli === "claude" ? "~/.claude/skills/" : "~/.codex/skills/"; const line = names.length - ? `The following skills are installed and ready — use one when it fits the task: ${names.join(", ")}.` + ? `The following skills are installed and ready under ${path} — to use a skill, view its instructions in ${path}/SKILL.md: ${names.join(", ")}.` : "No skills are installed yet."; return `${START}\n${line}\n${END}`; } @@ -73,7 +75,7 @@ async function spliceIntoFile(file: string, block: string): Promise { */ export async function updateSkillsSection(cli: "claude" | "codex", cwd: string): Promise { try { - const block = renderBlock(await installedSkillNames()); + const block = renderBlock(cli, await installedSkillNames(cli)); const file = cli === "claude" ? join(claudeMemoryDir(cwd), "MEMORY.md") : codexAgentsFile(cwd); await spliceIntoFile(file, block); } catch {