Add adapter skill sync for codex and claude
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
parseObject,
|
||||
parseJson,
|
||||
buildPaperclipEnv,
|
||||
listPaperclipSkillEntries,
|
||||
joinPromptSections,
|
||||
redactEnvForLogs,
|
||||
ensureAbsoluteDirectory,
|
||||
@@ -27,40 +28,32 @@ import {
|
||||
isClaudeMaxTurnsResult,
|
||||
isClaudeUnknownSessionError,
|
||||
} from "./parse.js";
|
||||
import { resolveClaudeDesiredSkillNames } from "./skills.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PAPERCLIP_SKILLS_CANDIDATES = [
|
||||
path.resolve(__moduleDir, "../../skills"), // published: <pkg>/dist/server/ -> <pkg>/skills/
|
||||
path.resolve(__moduleDir, "../../../../../skills"), // dev: src/server/ -> repo root/skills/
|
||||
];
|
||||
|
||||
async function resolvePaperclipSkillsDir(): Promise<string | null> {
|
||||
for (const candidate of PAPERCLIP_SKILLS_CANDIDATES) {
|
||||
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
|
||||
if (isDir) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tmpdir with `.claude/skills/` containing symlinks to skills from
|
||||
* the repo's `skills/` directory, so `--add-dir` makes Claude Code discover
|
||||
* them as proper registered skills.
|
||||
*/
|
||||
async function buildSkillsDir(): Promise<string> {
|
||||
async function buildSkillsDir(config: Record<string, unknown>): Promise<string> {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-skills-"));
|
||||
const target = path.join(tmp, ".claude", "skills");
|
||||
await fs.mkdir(target, { recursive: true });
|
||||
const skillsDir = await resolvePaperclipSkillsDir();
|
||||
if (!skillsDir) return tmp;
|
||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
await fs.symlink(
|
||||
path.join(skillsDir, entry.name),
|
||||
path.join(target, entry.name),
|
||||
);
|
||||
}
|
||||
const availableEntries = await listPaperclipSkillEntries(__moduleDir);
|
||||
const desiredNames = new Set(
|
||||
resolveClaudeDesiredSkillNames(
|
||||
config,
|
||||
availableEntries.map((entry) => entry.name),
|
||||
),
|
||||
);
|
||||
for (const entry of availableEntries) {
|
||||
if (!desiredNames.has(entry.name)) continue;
|
||||
await fs.symlink(
|
||||
entry.source,
|
||||
path.join(target, entry.name),
|
||||
);
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
@@ -337,7 +330,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
extraArgs,
|
||||
} = runtimeConfig;
|
||||
const billingType = resolveClaudeBillingType(env);
|
||||
const skillsDir = await buildSkillsDir();
|
||||
const skillsDir = await buildSkillsDir(config);
|
||||
|
||||
// When instructionsFilePath is configured, create a combined temp file that
|
||||
// includes both the file content and the path directive, so we only need
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { execute, runClaudeLogin } from "./execute.js";
|
||||
export { listClaudeSkills, syncClaudeSkills } from "./skills.js";
|
||||
export { testEnvironment } from "./test.js";
|
||||
export {
|
||||
parseClaudeStreamJson,
|
||||
|
||||
83
packages/adapters/claude-local/src/server/skills.ts
Normal file
83
packages/adapters/claude-local/src/server/skills.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type {
|
||||
AdapterSkillContext,
|
||||
AdapterSkillEntry,
|
||||
AdapterSkillSnapshot,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
listPaperclipSkillEntries,
|
||||
readPaperclipSkillSyncPreference,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function resolveDesiredSkillNames(config: Record<string, unknown>, availableSkillNames: string[]) {
|
||||
const preference = readPaperclipSkillSyncPreference(config);
|
||||
return preference.explicit ? preference.desiredSkills : availableSkillNames;
|
||||
}
|
||||
|
||||
async function buildClaudeSkillSnapshot(config: Record<string, unknown>): Promise<AdapterSkillSnapshot> {
|
||||
const availableEntries = await listPaperclipSkillEntries(__moduleDir);
|
||||
const availableByName = new Map(availableEntries.map((entry) => [entry.name, entry]));
|
||||
const desiredSkills = resolveDesiredSkillNames(
|
||||
config,
|
||||
availableEntries.map((entry) => entry.name),
|
||||
);
|
||||
const desiredSet = new Set(desiredSkills);
|
||||
const entries: AdapterSkillEntry[] = availableEntries.map((entry) => ({
|
||||
name: entry.name,
|
||||
desired: desiredSet.has(entry.name),
|
||||
managed: true,
|
||||
state: desiredSet.has(entry.name) ? "configured" : "available",
|
||||
sourcePath: entry.source,
|
||||
targetPath: null,
|
||||
detail: desiredSet.has(entry.name)
|
||||
? "Will be mounted into the ephemeral Claude skill directory on the next run."
|
||||
: null,
|
||||
}));
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const desiredSkill of desiredSkills) {
|
||||
if (availableByName.has(desiredSkill)) continue;
|
||||
warnings.push(`Desired skill "${desiredSkill}" is not available from the Paperclip skills directory.`);
|
||||
entries.push({
|
||||
name: desiredSkill,
|
||||
desired: true,
|
||||
managed: true,
|
||||
state: "missing",
|
||||
sourcePath: undefined,
|
||||
targetPath: undefined,
|
||||
detail: "Paperclip cannot find this skill in the local runtime skills directory.",
|
||||
});
|
||||
}
|
||||
|
||||
entries.sort((left, right) => left.name.localeCompare(right.name));
|
||||
|
||||
return {
|
||||
adapterType: "claude_local",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills,
|
||||
entries,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listClaudeSkills(ctx: AdapterSkillContext): Promise<AdapterSkillSnapshot> {
|
||||
return buildClaudeSkillSnapshot(ctx.config);
|
||||
}
|
||||
|
||||
export async function syncClaudeSkills(
|
||||
ctx: AdapterSkillContext,
|
||||
_desiredSkills: string[],
|
||||
): Promise<AdapterSkillSnapshot> {
|
||||
return buildClaudeSkillSnapshot(ctx.config);
|
||||
}
|
||||
|
||||
export function resolveClaudeDesiredSkillNames(
|
||||
config: Record<string, unknown>,
|
||||
availableSkillNames: string[],
|
||||
) {
|
||||
return resolveDesiredSkillNames(config, availableSkillNames);
|
||||
}
|
||||
Reference in New Issue
Block a user