fix: prefer .agents skills and repair codex symlink targets\n\nCo-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -32,6 +32,17 @@ export const runningProcesses = new Map<string, RunningProcess>();
|
|||||||
export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
|
export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
|
||||||
export const MAX_EXCERPT_BYTES = 32 * 1024;
|
export const MAX_EXCERPT_BYTES = 32 * 1024;
|
||||||
const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i;
|
const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i;
|
||||||
|
const PAPERCLIP_SKILL_ROOT_RELATIVE_CANDIDATES = [
|
||||||
|
"../../.agents/skills",
|
||||||
|
"../../skills",
|
||||||
|
"../../../../../.agents/skills",
|
||||||
|
"../../../../../skills",
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface PaperclipSkillEntry {
|
||||||
|
name: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseObject(value: unknown): Record<string, unknown> {
|
export function parseObject(value: unknown): Record<string, unknown> {
|
||||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||||
@@ -245,6 +256,90 @@ export async function ensureAbsoluteDirectory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listPaperclipSkillEntries(moduleDir: string): Promise<PaperclipSkillEntry[]> {
|
||||||
|
const entriesByName = new Map<string, PaperclipSkillEntry>();
|
||||||
|
const seenRoots = new Set<string>();
|
||||||
|
|
||||||
|
for (const relativePath of PAPERCLIP_SKILL_ROOT_RELATIVE_CANDIDATES) {
|
||||||
|
const root = path.resolve(moduleDir, relativePath);
|
||||||
|
if (seenRoots.has(root)) continue;
|
||||||
|
seenRoots.add(root);
|
||||||
|
|
||||||
|
const isDirectory = await fs.stat(root).then((stats) => stats.isDirectory()).catch(() => false);
|
||||||
|
if (!isDirectory) continue;
|
||||||
|
|
||||||
|
let entries: Awaited<ReturnType<typeof fs.readdir>>;
|
||||||
|
try {
|
||||||
|
entries = await fs.readdir(root, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
if (entriesByName.has(entry.name)) continue;
|
||||||
|
entriesByName.set(entry.name, {
|
||||||
|
name: entry.name,
|
||||||
|
source: path.join(root, entry.name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(entriesByName.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readPaperclipSkillMarkdown(
|
||||||
|
moduleDir: string,
|
||||||
|
skillName: string,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const normalized = skillName.trim().toLowerCase();
|
||||||
|
if (!normalized) return null;
|
||||||
|
|
||||||
|
const entries = await listPaperclipSkillEntries(moduleDir);
|
||||||
|
const match = entries.find((entry) => entry.name === normalized);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fs.readFile(path.join(match.source, "SKILL.md"), "utf8");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensurePaperclipSkillSymlink(
|
||||||
|
source: string,
|
||||||
|
target: string,
|
||||||
|
linkSkill: (source: string, target: string) => Promise<void> = (linkSource, linkTarget) =>
|
||||||
|
fs.symlink(linkSource, linkTarget),
|
||||||
|
): Promise<"created" | "repaired" | "skipped"> {
|
||||||
|
const existing = await fs.lstat(target).catch(() => null);
|
||||||
|
if (!existing) {
|
||||||
|
await linkSkill(source, target);
|
||||||
|
return "created";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.isSymbolicLink()) {
|
||||||
|
return "skipped";
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedPath = await fs.readlink(target).catch(() => null);
|
||||||
|
if (!linkedPath) return "skipped";
|
||||||
|
|
||||||
|
const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
|
||||||
|
if (resolvedLinkedPath === source) {
|
||||||
|
return "skipped";
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedPathExists = await fs.stat(resolvedLinkedPath).then(() => true).catch(() => false);
|
||||||
|
if (linkedPathExists) {
|
||||||
|
return "skipped";
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.unlink(target);
|
||||||
|
await linkSkill(source, target);
|
||||||
|
return "repaired";
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureCommandResolvable(command: string, cwd: string, env: NodeJS.ProcessEnv) {
|
export async function ensureCommandResolvable(command: string, cwd: string, env: NodeJS.ProcessEnv) {
|
||||||
const resolved = await resolveCommandPath(command, cwd, env);
|
const resolved = await resolveCommandPath(command, cwd, env);
|
||||||
if (resolved) return;
|
if (resolved) return;
|
||||||
|
|||||||
@@ -13,17 +13,15 @@ import {
|
|||||||
redactEnvForLogs,
|
redactEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
|
ensurePaperclipSkillSymlink,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
|
listPaperclipSkillEntries,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} 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 __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
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/
|
|
||||||
];
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -67,33 +65,32 @@ function codexHomeDir(): string {
|
|||||||
return path.join(os.homedir(), ".codex");
|
return path.join(os.homedir(), ".codex");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolvePaperclipSkillsDir(): Promise<string | null> {
|
type EnsureCodexSkillsInjectedOptions = {
|
||||||
for (const candidate of PAPERCLIP_SKILLS_CANDIDATES) {
|
skillsHome?: string;
|
||||||
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
|
skillsEntries?: Awaited<ReturnType<typeof listPaperclipSkillEntries>>;
|
||||||
if (isDir) return candidate;
|
linkSkill?: (source: string, target: string) => Promise<void>;
|
||||||
}
|
};
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureCodexSkillsInjected(onLog: AdapterExecutionContext["onLog"]) {
|
export async function ensureCodexSkillsInjected(
|
||||||
const skillsDir = await resolvePaperclipSkillsDir();
|
onLog: AdapterExecutionContext["onLog"],
|
||||||
if (!skillsDir) return;
|
options: EnsureCodexSkillsInjectedOptions = {},
|
||||||
|
) {
|
||||||
|
const skillsEntries = options.skillsEntries ?? await listPaperclipSkillEntries(__moduleDir);
|
||||||
|
if (skillsEntries.length === 0) return;
|
||||||
|
|
||||||
const skillsHome = path.join(codexHomeDir(), "skills");
|
const skillsHome = options.skillsHome ?? path.join(codexHomeDir(), "skills");
|
||||||
await fs.mkdir(skillsHome, { recursive: true });
|
await fs.mkdir(skillsHome, { recursive: true });
|
||||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
const linkSkill = options.linkSkill;
|
||||||
for (const entry of entries) {
|
for (const entry of skillsEntries) {
|
||||||
if (!entry.isDirectory()) continue;
|
|
||||||
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);
|
|
||||||
if (existing) continue;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.symlink(source, target);
|
const result = await ensurePaperclipSkillSymlink(entry.source, target, linkSkill);
|
||||||
|
if (result === "skipped") continue;
|
||||||
|
|
||||||
await onLog(
|
await onLog(
|
||||||
"stderr",
|
"stderr",
|
||||||
`[paperclip] Injected Codex skill "${entry.name}" into ${skillsHome}\n`,
|
`[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Codex skill "${entry.name}" into ${skillsHome}\n`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await onLog(
|
await onLog(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { execute } from "./execute.js";
|
export { execute, ensureCodexSkillsInjected } from "./execute.js";
|
||||||
export { testEnvironment } from "./test.js";
|
export { testEnvironment } from "./test.js";
|
||||||
export { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
|
export { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
|
||||||
import type { AdapterSessionCodec } from "@paperclipai/adapter-utils";
|
import type { AdapterSessionCodec } from "@paperclipai/adapter-utils";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import type { Dirent } from "node:fs";
|
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -13,7 +12,9 @@ import {
|
|||||||
redactEnvForLogs,
|
redactEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
|
ensurePaperclipSkillSymlink,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
|
listPaperclipSkillEntries,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
@@ -23,10 +24,6 @@ import { normalizeCursorStreamLine } from "../shared/stream.js";
|
|||||||
import { hasCursorTrustBypassArg } from "../shared/trust.js";
|
import { hasCursorTrustBypassArg } from "../shared/trust.js";
|
||||||
|
|
||||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const PAPERCLIP_SKILLS_CANDIDATES = [
|
|
||||||
path.resolve(__moduleDir, "../../skills"),
|
|
||||||
path.resolve(__moduleDir, "../../../../../skills"),
|
|
||||||
];
|
|
||||||
|
|
||||||
function firstNonEmptyLine(text: string): string {
|
function firstNonEmptyLine(text: string): string {
|
||||||
return (
|
return (
|
||||||
@@ -82,16 +79,9 @@ function cursorSkillsHome(): string {
|
|||||||
return path.join(os.homedir(), ".cursor", "skills");
|
return path.join(os.homedir(), ".cursor", "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;
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnsureCursorSkillsInjectedOptions = {
|
type EnsureCursorSkillsInjectedOptions = {
|
||||||
skillsDir?: string | null;
|
skillsDir?: string | null;
|
||||||
|
skillsEntries?: Array<{ name: string; source: string }>;
|
||||||
skillsHome?: string;
|
skillsHome?: string;
|
||||||
linkSkill?: (source: string, target: string) => Promise<void>;
|
linkSkill?: (source: string, target: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
@@ -100,8 +90,13 @@ export async function ensureCursorSkillsInjected(
|
|||||||
onLog: AdapterExecutionContext["onLog"],
|
onLog: AdapterExecutionContext["onLog"],
|
||||||
options: EnsureCursorSkillsInjectedOptions = {},
|
options: EnsureCursorSkillsInjectedOptions = {},
|
||||||
) {
|
) {
|
||||||
const skillsDir = options.skillsDir ?? await resolvePaperclipSkillsDir();
|
const skillsEntries = options.skillsEntries
|
||||||
if (!skillsDir) return;
|
?? (options.skillsDir
|
||||||
|
? (await fs.readdir(options.skillsDir, { withFileTypes: true }))
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => ({ name: entry.name, source: path.join(options.skillsDir!, entry.name) }))
|
||||||
|
: await listPaperclipSkillEntries(__moduleDir));
|
||||||
|
if (skillsEntries.length === 0) return;
|
||||||
|
|
||||||
const skillsHome = options.skillsHome ?? cursorSkillsHome();
|
const skillsHome = options.skillsHome ?? cursorSkillsHome();
|
||||||
try {
|
try {
|
||||||
@@ -113,31 +108,16 @@ export async function ensureCursorSkillsInjected(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries: Dirent[];
|
|
||||||
try {
|
|
||||||
entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
||||||
} catch (err) {
|
|
||||||
await onLog(
|
|
||||||
"stderr",
|
|
||||||
`[paperclip] Failed to read Paperclip skills from ${skillsDir}: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkSkill = options.linkSkill ?? ((source: string, target: string) => fs.symlink(source, target));
|
const linkSkill = options.linkSkill ?? ((source: string, target: string) => fs.symlink(source, target));
|
||||||
for (const entry of entries) {
|
for (const entry of skillsEntries) {
|
||||||
if (!entry.isDirectory()) continue;
|
|
||||||
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);
|
|
||||||
if (existing) continue;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await linkSkill(source, target);
|
const result = await ensurePaperclipSkillSymlink(entry.source, target, linkSkill);
|
||||||
|
if (result === "skipped") continue;
|
||||||
|
|
||||||
await onLog(
|
await onLog(
|
||||||
"stderr",
|
"stderr",
|
||||||
`[paperclip] Injected Cursor skill "${entry.name}" into ${skillsHome}\n`,
|
`[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Cursor skill "${entry.name}" into ${skillsHome}\n`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await onLog(
|
await onLog(
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import {
|
|||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
|
ensurePaperclipSkillSymlink,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
|
listPaperclipSkillEntries,
|
||||||
parseObject,
|
parseObject,
|
||||||
redactEnvForLogs,
|
redactEnvForLogs,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
@@ -29,10 +31,6 @@ import {
|
|||||||
import { firstNonEmptyLine } from "./utils.js";
|
import { firstNonEmptyLine } from "./utils.js";
|
||||||
|
|
||||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const PAPERCLIP_SKILLS_CANDIDATES = [
|
|
||||||
path.resolve(__moduleDir, "../../skills"),
|
|
||||||
path.resolve(__moduleDir, "../../../../../skills"),
|
|
||||||
];
|
|
||||||
|
|
||||||
function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
|
function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
|
||||||
const raw = env[key];
|
const raw = env[key];
|
||||||
@@ -73,14 +71,6 @@ function renderApiAccessNote(env: Record<string, string>): string {
|
|||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function geminiSkillsHome(): string {
|
function geminiSkillsHome(): string {
|
||||||
return path.join(os.homedir(), ".gemini", "skills");
|
return path.join(os.homedir(), ".gemini", "skills");
|
||||||
}
|
}
|
||||||
@@ -93,8 +83,8 @@ function geminiSkillsHome(): string {
|
|||||||
async function ensureGeminiSkillsInjected(
|
async function ensureGeminiSkillsInjected(
|
||||||
onLog: AdapterExecutionContext["onLog"],
|
onLog: AdapterExecutionContext["onLog"],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const skillsDir = await resolvePaperclipSkillsDir();
|
const skillsEntries = await listPaperclipSkillEntries(__moduleDir);
|
||||||
if (!skillsDir) return;
|
if (skillsEntries.length === 0) return;
|
||||||
|
|
||||||
const skillsHome = geminiSkillsHome();
|
const skillsHome = geminiSkillsHome();
|
||||||
try {
|
try {
|
||||||
@@ -107,27 +97,16 @@ async function ensureGeminiSkillsInjected(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries: Dirent[];
|
for (const entry of skillsEntries) {
|
||||||
try {
|
|
||||||
entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
||||||
} catch (err) {
|
|
||||||
await onLog(
|
|
||||||
"stderr",
|
|
||||||
`[paperclip] Failed to read Paperclip skills from ${skillsDir}: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.isDirectory()) continue;
|
|
||||||
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);
|
|
||||||
if (existing) continue;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.symlink(source, target);
|
const result = await ensurePaperclipSkillSymlink(entry.source, target);
|
||||||
await onLog("stderr", `[paperclip] Linked Gemini skill: ${entry.name}\n`);
|
if (result === "skipped") continue;
|
||||||
|
await onLog(
|
||||||
|
"stderr",
|
||||||
|
`[paperclip] ${result === "repaired" ? "Repaired" : "Linked"} Gemini skill: ${entry.name}\n`,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await onLog(
|
await onLog(
|
||||||
"stderr",
|
"stderr",
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import {
|
|||||||
redactEnvForLogs,
|
redactEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
|
ensurePaperclipSkillSymlink,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
|
listPaperclipSkillEntries,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
@@ -20,10 +22,6 @@ import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js";
|
|||||||
import { ensurePiModelConfiguredAndAvailable } from "./models.js";
|
import { ensurePiModelConfiguredAndAvailable } from "./models.js";
|
||||||
|
|
||||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const PAPERCLIP_SKILLS_CANDIDATES = [
|
|
||||||
path.resolve(__moduleDir, "../../skills"),
|
|
||||||
path.resolve(__moduleDir, "../../../../../skills"),
|
|
||||||
];
|
|
||||||
|
|
||||||
const PAPERCLIP_SESSIONS_DIR = path.join(os.homedir(), ".pi", "paperclips");
|
const PAPERCLIP_SESSIONS_DIR = path.join(os.homedir(), ".pi", "paperclips");
|
||||||
|
|
||||||
@@ -50,34 +48,22 @@ function parseModelId(model: string | null): string | null {
|
|||||||
return trimmed.slice(trimmed.indexOf("/") + 1).trim() || null;
|
return trimmed.slice(trimmed.indexOf("/") + 1).trim() || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ensurePiSkillsInjected(onLog: AdapterExecutionContext["onLog"]) {
|
async function ensurePiSkillsInjected(onLog: AdapterExecutionContext["onLog"]) {
|
||||||
const skillsDir = await resolvePaperclipSkillsDir();
|
const skillsEntries = await listPaperclipSkillEntries(__moduleDir);
|
||||||
if (!skillsDir) return;
|
if (skillsEntries.length === 0) return;
|
||||||
|
|
||||||
const piSkillsHome = path.join(os.homedir(), ".pi", "agent", "skills");
|
const piSkillsHome = path.join(os.homedir(), ".pi", "agent", "skills");
|
||||||
await fs.mkdir(piSkillsHome, { recursive: true });
|
await fs.mkdir(piSkillsHome, { recursive: true });
|
||||||
|
|
||||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
for (const entry of skillsEntries) {
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.isDirectory()) continue;
|
|
||||||
const source = path.join(skillsDir, entry.name);
|
|
||||||
const target = path.join(piSkillsHome, entry.name);
|
const target = path.join(piSkillsHome, entry.name);
|
||||||
const existing = await fs.lstat(target).catch(() => null);
|
|
||||||
if (existing) continue;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.symlink(source, target);
|
const result = await ensurePaperclipSkillSymlink(entry.source, target);
|
||||||
|
if (result === "skipped") continue;
|
||||||
await onLog(
|
await onLog(
|
||||||
"stderr",
|
"stderr",
|
||||||
`[paperclip] Injected Pi skill "${entry.name}" into ${piSkillsHome}\n`,
|
`[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.name}" into ${piSkillsHome}\n`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await onLog(
|
await onLog(
|
||||||
|
|||||||
Reference in New Issue
Block a user