fix: bundle skills directory into npm packages for runtime discovery
The claude-local, codex-local adapters and the server all resolve a skills/ directory using __dirname-relative paths that only work inside the monorepo. When installed from npm the paths point outside the package and cause ENOENT on readdir/readFile. - Update both adapter execute.ts files to try a published-path candidate (../../skills from dist/) before falling back to the monorepo dev path (../../../../../skills from src/). - Update server readSkillMarkdown() to try the published path first. - Add "skills" to the files array in server, claude-local, and codex-local package.json so npm includes them. - Update release.sh to copy the repo-root skills/ into each package before publish, and clean up after. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@paperclipai/adapter-claude-local",
|
"name": "@paperclipai/adapter-claude-local",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
"types": "./dist/index.d.ts"
|
"types": "./dist/index.d.ts"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist",
|
||||||
|
"skills"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|||||||
@@ -27,10 +27,19 @@ import {
|
|||||||
isClaudeUnknownSessionError,
|
isClaudeUnknownSessionError,
|
||||||
} from "./parse.js";
|
} from "./parse.js";
|
||||||
|
|
||||||
const PAPERCLIP_SKILLS_DIR = path.resolve(
|
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
const PAPERCLIP_SKILLS_CANDIDATES = [
|
||||||
"../../../../../skills",
|
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
|
* Create a tmpdir with `.claude/skills/` containing symlinks to skills from
|
||||||
@@ -41,11 +50,13 @@ async function buildSkillsDir(): Promise<string> {
|
|||||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-skills-"));
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-skills-"));
|
||||||
const target = path.join(tmp, ".claude", "skills");
|
const target = path.join(tmp, ".claude", "skills");
|
||||||
await fs.mkdir(target, { recursive: true });
|
await fs.mkdir(target, { recursive: true });
|
||||||
const entries = await fs.readdir(PAPERCLIP_SKILLS_DIR, { withFileTypes: true });
|
const skillsDir = await resolvePaperclipSkillsDir();
|
||||||
|
if (!skillsDir) return tmp;
|
||||||
|
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
await fs.symlink(
|
await fs.symlink(
|
||||||
path.join(PAPERCLIP_SKILLS_DIR, entry.name),
|
path.join(skillsDir, entry.name),
|
||||||
path.join(target, entry.name),
|
path.join(target, entry.name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@paperclipai/adapter-codex-local",
|
"name": "@paperclipai/adapter-codex-local",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
"types": "./dist/index.d.ts"
|
"types": "./dist/index.d.ts"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist",
|
||||||
|
"skills"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ import {
|
|||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
|
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
|
||||||
|
|
||||||
const PAPERCLIP_SKILLS_DIR = path.resolve(
|
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
const PAPERCLIP_SKILLS_CANDIDATES = [
|
||||||
"../../../../../skills",
|
path.resolve(__moduleDir, "../../skills"), // published: <pkg>/dist/server/ -> <pkg>/skills/
|
||||||
);
|
path.resolve(__moduleDir, "../../../../../skills"), // dev: src/server/ -> repo root/skills/
|
||||||
|
];
|
||||||
const CODEX_ROLLOUT_NOISE_RE =
|
const CODEX_ROLLOUT_NOISE_RE =
|
||||||
/^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i;
|
/^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i;
|
||||||
|
|
||||||
@@ -66,19 +67,24 @@ function codexHomeDir(): string {
|
|||||||
return path.join(os.homedir(), ".codex");
|
return path.join(os.homedir(), ".codex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureCodexSkillsInjected(onLog: AdapterExecutionContext["onLog"]) {
|
async function ensureCodexSkillsInjected(onLog: AdapterExecutionContext["onLog"]) {
|
||||||
const sourceExists = await fs
|
const skillsDir = await resolvePaperclipSkillsDir();
|
||||||
.stat(PAPERCLIP_SKILLS_DIR)
|
if (!skillsDir) return;
|
||||||
.then((stats) => stats.isDirectory())
|
|
||||||
.catch(() => false);
|
|
||||||
if (!sourceExists) return;
|
|
||||||
|
|
||||||
const skillsHome = path.join(codexHomeDir(), "skills");
|
const skillsHome = path.join(codexHomeDir(), "skills");
|
||||||
await fs.mkdir(skillsHome, { recursive: true });
|
await fs.mkdir(skillsHome, { recursive: true });
|
||||||
const entries = await fs.readdir(PAPERCLIP_SKILLS_DIR, { withFileTypes: true });
|
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isDirectory()) continue;
|
if (!entry.isDirectory()) continue;
|
||||||
const source = path.join(PAPERCLIP_SKILLS_DIR, entry.name);
|
const source = path.join(skillsDir, entry.name);
|
||||||
const target = path.join(skillsHome, entry.name);
|
const target = path.join(skillsHome, entry.name);
|
||||||
const existing = await fs.lstat(target).catch(() => null);
|
const existing = await fs.lstat(target).catch(() => null);
|
||||||
if (existing) continue;
|
if (existing) continue;
|
||||||
|
|||||||
@@ -138,7 +138,13 @@ pnpm --filter @paperclipai/server build
|
|||||||
pnpm --filter @paperclipai/ui build
|
pnpm --filter @paperclipai/ui build
|
||||||
rm -rf "$REPO_ROOT/server/ui-dist"
|
rm -rf "$REPO_ROOT/server/ui-dist"
|
||||||
cp -r "$REPO_ROOT/ui/dist" "$REPO_ROOT/server/ui-dist"
|
cp -r "$REPO_ROOT/ui/dist" "$REPO_ROOT/server/ui-dist"
|
||||||
echo " ✓ All packages built (including UI)"
|
|
||||||
|
# Bundle skills into packages that need them (adapters + server)
|
||||||
|
for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do
|
||||||
|
rm -rf "$REPO_ROOT/$pkg_dir/skills"
|
||||||
|
cp -r "$REPO_ROOT/skills" "$REPO_ROOT/$pkg_dir/skills"
|
||||||
|
done
|
||||||
|
echo " ✓ All packages built (including UI + skills)"
|
||||||
|
|
||||||
# ── Step 5: Build CLI bundle ─────────────────────────────────────────────────
|
# ── Step 5: Build CLI bundle ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -191,6 +197,11 @@ fi
|
|||||||
# Remove UI dist bundled into server for publishing
|
# Remove UI dist bundled into server for publishing
|
||||||
rm -rf "$REPO_ROOT/server/ui-dist"
|
rm -rf "$REPO_ROOT/server/ui-dist"
|
||||||
|
|
||||||
|
# Remove skills bundled into packages for publishing
|
||||||
|
for pkg_dir in server packages/adapters/claude-local packages/adapters/codex-local; do
|
||||||
|
rm -rf "$REPO_ROOT/$pkg_dir/skills"
|
||||||
|
done
|
||||||
|
|
||||||
# Stage only release-related files (avoid sweeping unrelated changes with -A)
|
# Stage only release-related files (avoid sweeping unrelated changes with -A)
|
||||||
git add \
|
git add \
|
||||||
.changeset/ \
|
.changeset/ \
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@paperclipai/server",
|
"name": "@paperclipai/server",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"ui-dist"
|
"ui-dist",
|
||||||
|
"skills"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts",
|
||||||
|
|||||||
@@ -59,8 +59,9 @@ function readSkillMarkdown(skillName: string): string | null {
|
|||||||
if (normalized !== "paperclip" && normalized !== "paperclip-create-agent") return null;
|
if (normalized !== "paperclip" && normalized !== "paperclip-create-agent") return null;
|
||||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.resolve(process.cwd(), "skills", normalized, "SKILL.md"),
|
path.resolve(moduleDir, "../../skills", normalized, "SKILL.md"), // published: dist/routes/ -> <pkg>/skills/
|
||||||
path.resolve(moduleDir, "../../../skills", normalized, "SKILL.md"),
|
path.resolve(process.cwd(), "skills", normalized, "SKILL.md"), // cwd (e.g. monorepo root)
|
||||||
|
path.resolve(moduleDir, "../../../skills", normalized, "SKILL.md"), // dev: src/routes/ -> repo root/skills/
|
||||||
];
|
];
|
||||||
for (const skillPath of candidates) {
|
for (const skillPath of candidates) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user