Add ServerAdapterModule capabilities from fork's adapter-utils 0.3.1
Bring the K8s adapter up to parity with the fork's ServerAdapterModule contract by adding sessionManagement, listSkills/syncSkills, listModels with Bedrock detection, and promptBundleKey support in the session codec. - Declare sessionManagement with nativeContextManagement: "confirmed" so Paperclip skips threshold-based session compaction (Claude manages its own context) - Add ephemeral skill management (listSkills/syncSkills) mirroring claude_local — reports skill state without runtime persistence since skills are injected via prompt bundle into ephemeral Job pods - Add listModels() with Bedrock environment detection, returning region-qualified model IDs when CLAUDE_CODE_USE_BEDROCK or ANTHROPIC_BEDROCK_BASE_URL are set - Extend session codec to round-trip promptBundleKey field - Remove the `as ServerAdapterModule` cast — the return type now satisfies the full interface Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
+19
-2
@@ -1,9 +1,22 @@
|
||||
import type { ServerAdapterModule } from "@paperclipai/adapter-utils";
|
||||
import type { ServerAdapterModule, AdapterSessionManagement } from "@paperclipai/adapter-utils";
|
||||
import { type, models, agentConfigurationDoc } from "../index.js";
|
||||
import { execute } from "./execute.js";
|
||||
import { testEnvironment } from "./test.js";
|
||||
import { sessionCodec } from "./session.js";
|
||||
import { getConfigSchema } from "./config-schema.js";
|
||||
import { listK8sSkills, syncK8sSkills } from "./skills.js";
|
||||
import { listK8sModels } from "./models.js";
|
||||
|
||||
const sessionManagement: AdapterSessionManagement = {
|
||||
supportsSessionResume: true,
|
||||
nativeContextManagement: "confirmed",
|
||||
defaultSessionCompaction: {
|
||||
enabled: true,
|
||||
maxSessionRuns: 0,
|
||||
maxRawInputTokens: 0,
|
||||
maxSessionAgeHours: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export function createServerAdapter(): ServerAdapterModule {
|
||||
return {
|
||||
@@ -11,11 +24,15 @@ export function createServerAdapter(): ServerAdapterModule {
|
||||
execute,
|
||||
testEnvironment,
|
||||
sessionCodec,
|
||||
sessionManagement,
|
||||
models,
|
||||
listModels: listK8sModels,
|
||||
listSkills: listK8sSkills,
|
||||
syncSkills: syncK8sSkills,
|
||||
supportsLocalAgentJwt: true,
|
||||
agentConfigurationDoc,
|
||||
getConfigSchema,
|
||||
} as ServerAdapterModule;
|
||||
};
|
||||
}
|
||||
|
||||
export { execute, testEnvironment, sessionCodec };
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { listK8sModels } from "./models.js";
|
||||
|
||||
describe("listK8sModels", () => {
|
||||
const savedEnv: Record<string, string | undefined> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
savedEnv.CLAUDE_CODE_USE_BEDROCK = process.env.CLAUDE_CODE_USE_BEDROCK;
|
||||
savedEnv.ANTHROPIC_BEDROCK_BASE_URL = process.env.ANTHROPIC_BEDROCK_BASE_URL;
|
||||
delete process.env.CLAUDE_CODE_USE_BEDROCK;
|
||||
delete process.env.ANTHROPIC_BEDROCK_BASE_URL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const [key, value] of Object.entries(savedEnv)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
it("returns direct API models by default", async () => {
|
||||
const models = await listK8sModels();
|
||||
expect(models.some((m) => m.id === "claude-opus-4-6")).toBe(true);
|
||||
expect(models.every((m) => !m.id.includes("anthropic."))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns Bedrock models when CLAUDE_CODE_USE_BEDROCK=1", async () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
const models = await listK8sModels();
|
||||
expect(models.some((m) => m.id.includes("anthropic."))).toBe(true);
|
||||
expect(models.every((m) => !m.id.startsWith("claude-"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns Bedrock models when CLAUDE_CODE_USE_BEDROCK=true", async () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "true";
|
||||
const models = await listK8sModels();
|
||||
expect(models.some((m) => m.id.includes("anthropic."))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns Bedrock models when ANTHROPIC_BEDROCK_BASE_URL is set", async () => {
|
||||
process.env.ANTHROPIC_BEDROCK_BASE_URL = "https://bedrock.us-east-1.amazonaws.com";
|
||||
const models = await listK8sModels();
|
||||
expect(models.some((m) => m.id.includes("anthropic."))).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores blank ANTHROPIC_BEDROCK_BASE_URL", async () => {
|
||||
process.env.ANTHROPIC_BEDROCK_BASE_URL = " ";
|
||||
const models = await listK8sModels();
|
||||
expect(models.some((m) => m.id === "claude-opus-4-6")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AdapterModel } from "@paperclipai/adapter-utils";
|
||||
import { models as DIRECT_MODELS } from "../index.js";
|
||||
|
||||
const BEDROCK_MODELS: AdapterModel[] = [
|
||||
{ id: "us.anthropic.claude-opus-4-6-v1", label: "Bedrock Opus 4.6" },
|
||||
{ id: "us.anthropic.claude-sonnet-4-5-20250929-v2:0", label: "Bedrock Sonnet 4.5" },
|
||||
{ id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", label: "Bedrock Haiku 4.5" },
|
||||
];
|
||||
|
||||
function isBedrockEnv(): boolean {
|
||||
return (
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK === "1" ||
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK === "true" ||
|
||||
(typeof process.env.ANTHROPIC_BEDROCK_BASE_URL === "string" &&
|
||||
process.env.ANTHROPIC_BEDROCK_BASE_URL.trim().length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
export async function listK8sModels(): Promise<AdapterModel[]> {
|
||||
return isBedrockEnv() ? BEDROCK_MODELS : DIRECT_MODELS;
|
||||
}
|
||||
@@ -92,6 +92,37 @@ describe("sessionCodec", () => {
|
||||
repoRef: "main",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps promptBundleKey", () => {
|
||||
expect(deserialize({ sessionId: "s", promptBundleKey: "bundle-1" })?.promptBundleKey).toBe("bundle-1");
|
||||
});
|
||||
|
||||
it("maps prompt_bundle_key as fallback", () => {
|
||||
expect(deserialize({ sessionId: "s", prompt_bundle_key: "bundle-2" })?.promptBundleKey).toBe("bundle-2");
|
||||
});
|
||||
|
||||
it("prefers promptBundleKey over prompt_bundle_key", () => {
|
||||
const result = deserialize({ sessionId: "s", promptBundleKey: "a", prompt_bundle_key: "b" });
|
||||
expect(result?.promptBundleKey).toBe("a");
|
||||
});
|
||||
|
||||
it("omits promptBundleKey when empty", () => {
|
||||
const result = deserialize({ sessionId: "s", promptBundleKey: "" });
|
||||
expect(result).not.toHaveProperty("promptBundleKey");
|
||||
});
|
||||
|
||||
it("includes promptBundleKey in full round-trip", () => {
|
||||
const result = deserialize({
|
||||
sessionId: "sess_abc",
|
||||
cwd: "/work",
|
||||
promptBundleKey: "bundle-key-123",
|
||||
});
|
||||
expect(result).toEqual({
|
||||
sessionId: "sess_abc",
|
||||
cwd: "/work",
|
||||
promptBundleKey: "bundle-key-123",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("serialize", () => {
|
||||
@@ -125,6 +156,16 @@ describe("sessionCodec", () => {
|
||||
expect(serialize(input)).toEqual(input);
|
||||
});
|
||||
|
||||
it("serializes promptBundleKey", () => {
|
||||
const result = serialize({ sessionId: "s", promptBundleKey: "bundle-1" });
|
||||
expect(result?.promptBundleKey).toBe("bundle-1");
|
||||
});
|
||||
|
||||
it("omits promptBundleKey when empty", () => {
|
||||
const result = serialize({ sessionId: "s", promptBundleKey: "" });
|
||||
expect(result).not.toHaveProperty("promptBundleKey");
|
||||
});
|
||||
|
||||
it("omits undefined fields", () => {
|
||||
const result = serialize({ sessionId: "sess_abc" });
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
+20
-17
@@ -4,44 +4,47 @@ function readNonEmptyString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function extractSessionFields(record: Record<string, unknown>) {
|
||||
const sessionId = readNonEmptyString(record.sessionId) ?? readNonEmptyString(record.session_id);
|
||||
const cwd =
|
||||
readNonEmptyString(record.cwd) ??
|
||||
readNonEmptyString(record.workdir) ??
|
||||
readNonEmptyString(record.folder);
|
||||
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
|
||||
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
|
||||
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
|
||||
const promptBundleKey =
|
||||
readNonEmptyString(record.promptBundleKey) ?? readNonEmptyString(record.prompt_bundle_key);
|
||||
return { sessionId, cwd, workspaceId, repoUrl, repoRef, promptBundleKey };
|
||||
}
|
||||
|
||||
export const sessionCodec: AdapterSessionCodec = {
|
||||
deserialize(raw: unknown) {
|
||||
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
||||
const record = raw as Record<string, unknown>;
|
||||
const sessionId = readNonEmptyString(record.sessionId) ?? readNonEmptyString(record.session_id);
|
||||
const { sessionId, cwd, workspaceId, repoUrl, repoRef, promptBundleKey } =
|
||||
extractSessionFields(raw as Record<string, unknown>);
|
||||
if (!sessionId) return null;
|
||||
const cwd =
|
||||
readNonEmptyString(record.cwd) ??
|
||||
readNonEmptyString(record.workdir) ??
|
||||
readNonEmptyString(record.folder);
|
||||
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
|
||||
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
|
||||
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
|
||||
return {
|
||||
sessionId,
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(repoUrl ? { repoUrl } : {}),
|
||||
...(repoRef ? { repoRef } : {}),
|
||||
...(promptBundleKey ? { promptBundleKey } : {}),
|
||||
};
|
||||
},
|
||||
serialize(params: Record<string, unknown> | null) {
|
||||
if (!params) return null;
|
||||
const sessionId = readNonEmptyString(params.sessionId) ?? readNonEmptyString(params.session_id);
|
||||
const { sessionId, cwd, workspaceId, repoUrl, repoRef, promptBundleKey } =
|
||||
extractSessionFields(params);
|
||||
if (!sessionId) return null;
|
||||
const cwd =
|
||||
readNonEmptyString(params.cwd) ??
|
||||
readNonEmptyString(params.workdir) ??
|
||||
readNonEmptyString(params.folder);
|
||||
const workspaceId = readNonEmptyString(params.workspaceId) ?? readNonEmptyString(params.workspace_id);
|
||||
const repoUrl = readNonEmptyString(params.repoUrl) ?? readNonEmptyString(params.repo_url);
|
||||
const repoRef = readNonEmptyString(params.repoRef) ?? readNonEmptyString(params.repo_ref);
|
||||
return {
|
||||
sessionId,
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(repoUrl ? { repoUrl } : {}),
|
||||
...(repoRef ? { repoRef } : {}),
|
||||
...(promptBundleKey ? { promptBundleKey } : {}),
|
||||
};
|
||||
},
|
||||
getDisplayId(params: Record<string, unknown> | null) {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { AdapterSkillContext } from "@paperclipai/adapter-utils";
|
||||
|
||||
vi.mock("@paperclipai/adapter-utils/server-utils", () => ({
|
||||
readPaperclipRuntimeSkillEntries: vi.fn().mockResolvedValue([]),
|
||||
resolvePaperclipDesiredSkillNames: vi.fn().mockReturnValue([]),
|
||||
readInstalledSkillTargets: vi.fn().mockResolvedValue(new Map()),
|
||||
}));
|
||||
|
||||
import { listK8sSkills, syncK8sSkills } from "./skills.js";
|
||||
import {
|
||||
readPaperclipRuntimeSkillEntries,
|
||||
resolvePaperclipDesiredSkillNames,
|
||||
readInstalledSkillTargets,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
||||
const mockedReadEntries = vi.mocked(readPaperclipRuntimeSkillEntries);
|
||||
const mockedResolveDesired = vi.mocked(resolvePaperclipDesiredSkillNames);
|
||||
const mockedReadInstalled = vi.mocked(readInstalledSkillTargets);
|
||||
|
||||
const ctx: AdapterSkillContext = {
|
||||
agentId: "agent-1",
|
||||
companyId: "company-1",
|
||||
adapterType: "claude_k8s",
|
||||
config: {},
|
||||
};
|
||||
|
||||
describe("listK8sSkills", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedReadEntries.mockResolvedValue([]);
|
||||
mockedResolveDesired.mockReturnValue([]);
|
||||
mockedReadInstalled.mockResolvedValue(new Map());
|
||||
});
|
||||
|
||||
it("returns empty snapshot when no skills available", async () => {
|
||||
const snapshot = await listK8sSkills(ctx);
|
||||
expect(snapshot.adapterType).toBe("claude_k8s");
|
||||
expect(snapshot.supported).toBe(true);
|
||||
expect(snapshot.mode).toBe("ephemeral");
|
||||
expect(snapshot.entries).toEqual([]);
|
||||
expect(snapshot.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("marks desired available skills as configured", async () => {
|
||||
mockedReadEntries.mockResolvedValue([
|
||||
{ key: "paperclip/skill-a", runtimeName: "skill-a", source: "/skills/skill-a", required: false },
|
||||
]);
|
||||
mockedResolveDesired.mockReturnValue(["paperclip/skill-a"]);
|
||||
|
||||
const snapshot = await listK8sSkills(ctx);
|
||||
expect(snapshot.entries).toHaveLength(1);
|
||||
expect(snapshot.entries[0].state).toBe("configured");
|
||||
expect(snapshot.entries[0].desired).toBe(true);
|
||||
expect(snapshot.entries[0].origin).toBe("company_managed");
|
||||
});
|
||||
|
||||
it("marks required skills with paperclip_required origin", async () => {
|
||||
mockedReadEntries.mockResolvedValue([
|
||||
{ key: "paperclip/core", runtimeName: "core", source: "/skills/core", required: true, requiredReason: "Bundled" },
|
||||
]);
|
||||
mockedResolveDesired.mockReturnValue(["paperclip/core"]);
|
||||
|
||||
const snapshot = await listK8sSkills(ctx);
|
||||
expect(snapshot.entries[0].origin).toBe("paperclip_required");
|
||||
expect(snapshot.entries[0].required).toBe(true);
|
||||
});
|
||||
|
||||
it("adds warning for desired skill not in available entries", async () => {
|
||||
mockedReadEntries.mockResolvedValue([]);
|
||||
mockedResolveDesired.mockReturnValue(["missing/skill"]);
|
||||
|
||||
const snapshot = await listK8sSkills(ctx);
|
||||
expect(snapshot.warnings).toHaveLength(1);
|
||||
expect(snapshot.warnings[0]).toContain("missing/skill");
|
||||
expect(snapshot.entries).toHaveLength(1);
|
||||
expect(snapshot.entries[0].state).toBe("missing");
|
||||
});
|
||||
|
||||
it("lists external user-installed skills", async () => {
|
||||
mockedReadInstalled.mockResolvedValue(
|
||||
new Map([["user-skill", { targetPath: "/paperclip/.claude/skills/user-skill" }]]),
|
||||
);
|
||||
|
||||
const snapshot = await listK8sSkills(ctx);
|
||||
expect(snapshot.entries).toHaveLength(1);
|
||||
expect(snapshot.entries[0].state).toBe("external");
|
||||
expect(snapshot.entries[0].origin).toBe("user_installed");
|
||||
expect(snapshot.entries[0].managed).toBe(false);
|
||||
});
|
||||
|
||||
it("does not duplicate externally installed skills that match available entries", async () => {
|
||||
mockedReadEntries.mockResolvedValue([
|
||||
{ key: "paperclip/skill-a", runtimeName: "skill-a", source: "/skills/skill-a", required: false },
|
||||
]);
|
||||
mockedReadInstalled.mockResolvedValue(
|
||||
new Map([["skill-a", { targetPath: "/paperclip/.claude/skills/skill-a" }]]),
|
||||
);
|
||||
|
||||
const snapshot = await listK8sSkills(ctx);
|
||||
expect(snapshot.entries).toHaveLength(1);
|
||||
expect(snapshot.entries[0].key).toBe("paperclip/skill-a");
|
||||
});
|
||||
|
||||
it("sorts entries alphabetically by key", async () => {
|
||||
mockedReadEntries.mockResolvedValue([
|
||||
{ key: "paperclip/zebra", runtimeName: "zebra", source: "/skills/zebra", required: false },
|
||||
{ key: "paperclip/alpha", runtimeName: "alpha", source: "/skills/alpha", required: false },
|
||||
]);
|
||||
|
||||
const snapshot = await listK8sSkills(ctx);
|
||||
expect(snapshot.entries[0].key).toBe("paperclip/alpha");
|
||||
expect(snapshot.entries[1].key).toBe("paperclip/zebra");
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncK8sSkills", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedReadEntries.mockResolvedValue([]);
|
||||
mockedResolveDesired.mockReturnValue([]);
|
||||
mockedReadInstalled.mockResolvedValue(new Map());
|
||||
});
|
||||
|
||||
it("returns same snapshot as listK8sSkills (ephemeral pass-through)", async () => {
|
||||
mockedReadEntries.mockResolvedValue([
|
||||
{ key: "paperclip/skill-a", runtimeName: "skill-a", source: "/skills/skill-a", required: false },
|
||||
]);
|
||||
mockedResolveDesired.mockReturnValue(["paperclip/skill-a"]);
|
||||
|
||||
const listResult = await listK8sSkills(ctx);
|
||||
vi.clearAllMocks();
|
||||
mockedReadEntries.mockResolvedValue([
|
||||
{ key: "paperclip/skill-a", runtimeName: "skill-a", source: "/skills/skill-a", required: false },
|
||||
]);
|
||||
mockedResolveDesired.mockReturnValue(["paperclip/skill-a"]);
|
||||
mockedReadInstalled.mockResolvedValue(new Map());
|
||||
|
||||
const syncResult = await syncK8sSkills(ctx, ["paperclip/skill-a"]);
|
||||
expect(syncResult).toEqual(listResult);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import type {
|
||||
AdapterSkillContext,
|
||||
AdapterSkillSnapshot,
|
||||
AdapterSkillEntry,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
readPaperclipRuntimeSkillEntries,
|
||||
resolvePaperclipDesiredSkillNames,
|
||||
readInstalledSkillTargets,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import path from "node:path";
|
||||
|
||||
const SKILLS_HOME = "/paperclip/.claude/skills";
|
||||
|
||||
async function buildK8sSkillSnapshot(
|
||||
config: Record<string, unknown>,
|
||||
): Promise<AdapterSkillSnapshot> {
|
||||
const availableEntries = await readPaperclipRuntimeSkillEntries(config, import.meta.dirname ?? __dirname);
|
||||
const availableByKey = new Map(availableEntries.map((e) => [e.key, e]));
|
||||
const desiredSkills = resolvePaperclipDesiredSkillNames(config, availableEntries);
|
||||
const desiredSet = new Set(desiredSkills);
|
||||
const installed = await readInstalledSkillTargets(SKILLS_HOME);
|
||||
|
||||
const entries: AdapterSkillEntry[] = availableEntries.map((entry) => ({
|
||||
key: entry.key,
|
||||
runtimeName: entry.runtimeName,
|
||||
desired: desiredSet.has(entry.key),
|
||||
managed: true,
|
||||
state: desiredSet.has(entry.key) ? "configured" : "available",
|
||||
origin: entry.required ? "paperclip_required" : "company_managed",
|
||||
originLabel: entry.required ? "Required by Paperclip" : "Managed by Paperclip",
|
||||
readOnly: false,
|
||||
sourcePath: entry.source,
|
||||
targetPath: null,
|
||||
detail: desiredSet.has(entry.key)
|
||||
? "Injected via prompt bundle into ephemeral K8s Job pods."
|
||||
: null,
|
||||
required: Boolean(entry.required),
|
||||
requiredReason: entry.requiredReason ?? null,
|
||||
}));
|
||||
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const desiredSkill of desiredSkills) {
|
||||
if (availableByKey.has(desiredSkill)) continue;
|
||||
warnings.push(`Desired skill "${desiredSkill}" is not available from the Paperclip skills directory.`);
|
||||
entries.push({
|
||||
key: desiredSkill,
|
||||
runtimeName: null,
|
||||
desired: true,
|
||||
managed: true,
|
||||
state: "missing",
|
||||
origin: "external_unknown",
|
||||
originLabel: "External or unavailable",
|
||||
readOnly: false,
|
||||
sourcePath: undefined,
|
||||
targetPath: undefined,
|
||||
detail: "Paperclip cannot find this skill in the runtime skills directory.",
|
||||
});
|
||||
}
|
||||
|
||||
for (const [name, installedEntry] of installed.entries()) {
|
||||
if (availableEntries.some((e) => e.runtimeName === name)) continue;
|
||||
entries.push({
|
||||
key: name,
|
||||
runtimeName: name,
|
||||
desired: false,
|
||||
managed: false,
|
||||
state: "external",
|
||||
origin: "user_installed",
|
||||
originLabel: "User-installed",
|
||||
locationLabel: "~/.claude/skills",
|
||||
readOnly: true,
|
||||
sourcePath: null,
|
||||
targetPath: installedEntry.targetPath ?? path.join(SKILLS_HOME, name),
|
||||
detail: "Installed outside Paperclip management in the Claude skills home.",
|
||||
});
|
||||
}
|
||||
|
||||
entries.sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
return {
|
||||
adapterType: "claude_k8s",
|
||||
supported: true,
|
||||
mode: "ephemeral",
|
||||
desiredSkills,
|
||||
entries,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listK8sSkills(ctx: AdapterSkillContext): Promise<AdapterSkillSnapshot> {
|
||||
return buildK8sSkillSnapshot(ctx.config);
|
||||
}
|
||||
|
||||
export async function syncK8sSkills(
|
||||
ctx: AdapterSkillContext,
|
||||
_desiredSkills: string[],
|
||||
): Promise<AdapterSkillSnapshot> {
|
||||
return buildK8sSkillSnapshot(ctx.config);
|
||||
}
|
||||
Reference in New Issue
Block a user