Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions packages/core/src/memory/skillsSection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>();
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");
Expand All @@ -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`,
}));
Expand All @@ -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"];
Expand All @@ -35,6 +37,7 @@ describe("memory/skillsSection", () => {
expect(out).toContain("<!-- agentnet:skills:start -->");
expect(out).toContain("<!-- agentnet:skills:end -->");
expect(out).toContain("changelog-generator, code-review"); // sorted, comma-joined
expect(out).toContain("~/.claude/skills/");
});

it("says none when no skills are installed", async () => {
Expand All @@ -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);
});
});
20 changes: 11 additions & 9 deletions packages/core/src/memory/skillsSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<!-- agentnet:skills:start -->";
const END = "<!-- agentnet:skills: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<string[]> {
* 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<string[]> {
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 [];
Expand All @@ -41,9 +42,10 @@ async function installedSkillNames(): Promise<string[]> {
* 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-name>/SKILL.md: ${names.join(", ")}.`
: "No skills are installed yet.";
return `${START}\n${line}\n${END}`;
}
Expand Down Expand Up @@ -73,7 +75,7 @@ async function spliceIntoFile(file: string, block: string): Promise<void> {
*/
export async function updateSkillsSection(cli: "claude" | "codex", cwd: string): Promise<void> {
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 {
Expand Down