feat(quick-260327-x64): implement listSkills/syncSkills on Claude, Codex, and Gemini adapters
- Add skills.ts to Claude adapter: ephemeral mode, listClaudeSkills/syncClaudeSkills - Add skills.ts to Codex adapter: ephemeral mode, listCodexSkills/syncCodexSkills - Add skills.ts to Gemini adapter: persistent mode with symlink sync, listGeminiSkills/syncGeminiSkills - Re-export skill functions from each adapter's server/index.ts
This commit is contained in:
1 parent
ed4988c5aa
commit
bc1c7aefe5
6 files changed
+346
No files matched your search
@@ -6,6 +6,7 @@ export {
|
|||||||
isClaudeMaxTurnsResult,
|
isClaudeMaxTurnsResult,
|
||||||
isClaudeUnknownSessionError,
|
isClaudeUnknownSessionError,
|
||||||
} from "./parse.js";
|
} from "./parse.js";
|
||||||
|
export { listClaudeSkills, syncClaudeSkills } from "./skills.js";
|
||||||
import type { AdapterSessionCodec } from "@agentcompany/adapter-utils";
|
import type { AdapterSessionCodec } from "@agentcompany/adapter-utils";
|
||||||
|
|
||||||
function readNonEmptyString(value: unknown): string | null {
|
function readNonEmptyString(value: unknown): string | null {
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Claude adapter skill listing / sync -- ephemeral mode
|
||||||
|
// Skills are mounted into a temp dir via --add-dir at run time, not persisted.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import type {
|
||||||
|
AdapterSkillContext,
|
||||||
|
AdapterSkillEntry,
|
||||||
|
AdapterSkillSnapshot,
|
||||||
|
} from "@agentcompany/adapter-utils";
|
||||||
|
import {
|
||||||
|
parseObject,
|
||||||
|
asString,
|
||||||
|
readAgentCompanyRuntimeSkillEntries,
|
||||||
|
readInstalledSkillTargets,
|
||||||
|
resolveAgentCompanyDesiredSkillNames,
|
||||||
|
} from "@agentcompany/adapter-utils/server-utils";
|
||||||
|
|
||||||
|
function resolveClaudeSkillsHome(config: Record<string, unknown>): string {
|
||||||
|
const env = parseObject(config.env);
|
||||||
|
const home = asString(env.HOME, asString(env.USERPROFILE, ""));
|
||||||
|
const base = home || (process.env.HOME ?? process.env.USERPROFILE ?? "");
|
||||||
|
return path.join(base, ".claude", "skills");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildClaudeSkillSnapshot(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
const moduleDir = path.resolve(import.meta.dirname ?? __dirname);
|
||||||
|
const availableEntries = await readAgentCompanyRuntimeSkillEntries(config, moduleDir);
|
||||||
|
const desiredSkills = resolveAgentCompanyDesiredSkillNames(config, availableEntries);
|
||||||
|
const desiredSet = new Set(desiredSkills);
|
||||||
|
const entries: AdapterSkillEntry[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
for (const available of availableEntries) {
|
||||||
|
const desired = desiredSet.has(available.key);
|
||||||
|
entries.push({
|
||||||
|
key: available.key,
|
||||||
|
runtimeName: available.runtimeName,
|
||||||
|
desired,
|
||||||
|
managed: true,
|
||||||
|
state: desired ? "configured" : "available",
|
||||||
|
sourcePath: available.source,
|
||||||
|
targetPath: null,
|
||||||
|
detail: desired
|
||||||
|
? "Will be mounted into the ephemeral Claude skill directory on the next run."
|
||||||
|
: null,
|
||||||
|
required: Boolean(available.required),
|
||||||
|
requiredReason: available.requiredReason ?? null,
|
||||||
|
origin: available.required ? "paperclip_required" : "company_managed",
|
||||||
|
originLabel: available.required ? "Required by AgentCompany" : "Managed by AgentCompany",
|
||||||
|
readOnly: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan installed Claude skills for external entries
|
||||||
|
const skillsHome = resolveClaudeSkillsHome(config);
|
||||||
|
const installed = await readInstalledSkillTargets(skillsHome);
|
||||||
|
for (const [name, installedEntry] of installed.entries()) {
|
||||||
|
if (availableEntries.some((entry) => entry.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(skillsHome, name),
|
||||||
|
detail: "Installed outside AgentCompany management.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for desired skills not in available
|
||||||
|
for (const desiredSkill of desiredSkills) {
|
||||||
|
if (availableEntries.some((entry) => entry.key === desiredSkill)) continue;
|
||||||
|
if (entries.some((entry) => entry.key === desiredSkill)) continue;
|
||||||
|
warnings.push(`Desired skill "${desiredSkill}" is not available from the AgentCompany skills directory.`);
|
||||||
|
entries.push({
|
||||||
|
key: desiredSkill,
|
||||||
|
runtimeName: null,
|
||||||
|
desired: true,
|
||||||
|
managed: true,
|
||||||
|
state: "missing",
|
||||||
|
sourcePath: null,
|
||||||
|
targetPath: null,
|
||||||
|
detail: "AgentCompany cannot find this skill in the local runtime skills directory.",
|
||||||
|
origin: "external_unknown",
|
||||||
|
originLabel: "External or unavailable",
|
||||||
|
readOnly: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort((left, right) => left.key.localeCompare(right.key));
|
||||||
|
|
||||||
|
return {
|
||||||
|
adapterType: "claude_local",
|
||||||
|
supported: true,
|
||||||
|
mode: "ephemeral",
|
||||||
|
desiredSkills,
|
||||||
|
entries,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the current skill state for the Claude adapter.
|
||||||
|
* Ephemeral mode: skills are injected at run time via --add-dir, not persisted.
|
||||||
|
*/
|
||||||
|
export async function listClaudeSkills(
|
||||||
|
ctx: AdapterSkillContext,
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
return buildClaudeSkillSnapshot(ctx.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync skills for the Claude adapter.
|
||||||
|
* Ephemeral mode: no on-disk sync needed; skills are injected at run time.
|
||||||
|
* Returns the current snapshot for informational purposes.
|
||||||
|
*/
|
||||||
|
export async function syncClaudeSkills(
|
||||||
|
ctx: AdapterSkillContext,
|
||||||
|
_desiredSkills: string[],
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
return buildClaudeSkillSnapshot(ctx.config);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export { execute } from "./execute.js";
|
export { execute } 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";
|
||||||
|
export { listCodexSkills, syncCodexSkills } from "./skills.js";
|
||||||
import type { AdapterSessionCodec } from "@agentcompany/adapter-utils";
|
import type { AdapterSessionCodec } from "@agentcompany/adapter-utils";
|
||||||
|
|
||||||
function readNonEmptyString(value: unknown): string | null {
|
function readNonEmptyString(value: unknown): string | null {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Codex adapter skill listing / sync -- ephemeral mode
|
||||||
|
// Skills are linked into the workspace .agents/skills directory at run time.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import type {
|
||||||
|
AdapterSkillContext,
|
||||||
|
AdapterSkillEntry,
|
||||||
|
AdapterSkillSnapshot,
|
||||||
|
} from "@agentcompany/adapter-utils";
|
||||||
|
import {
|
||||||
|
readAgentCompanyRuntimeSkillEntries,
|
||||||
|
resolveAgentCompanyDesiredSkillNames,
|
||||||
|
} from "@agentcompany/adapter-utils/server-utils";
|
||||||
|
|
||||||
|
async function buildCodexSkillSnapshot(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
const moduleDir = path.resolve(import.meta.dirname ?? __dirname);
|
||||||
|
const availableEntries = await readAgentCompanyRuntimeSkillEntries(config, moduleDir);
|
||||||
|
const desiredSkills = resolveAgentCompanyDesiredSkillNames(config, availableEntries);
|
||||||
|
const desiredSet = new Set(desiredSkills);
|
||||||
|
const entries: AdapterSkillEntry[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
for (const available of availableEntries) {
|
||||||
|
const desired = desiredSet.has(available.key);
|
||||||
|
entries.push({
|
||||||
|
key: available.key,
|
||||||
|
runtimeName: available.runtimeName,
|
||||||
|
desired,
|
||||||
|
managed: true,
|
||||||
|
state: desired ? "configured" : "available",
|
||||||
|
sourcePath: available.source,
|
||||||
|
targetPath: null,
|
||||||
|
detail: desired
|
||||||
|
? "Will be linked into the workspace .agents/skills directory on the next run."
|
||||||
|
: null,
|
||||||
|
required: Boolean(available.required),
|
||||||
|
requiredReason: available.requiredReason ?? null,
|
||||||
|
origin: available.required ? "paperclip_required" : "company_managed",
|
||||||
|
originLabel: available.required ? "Required by AgentCompany" : "Managed by AgentCompany",
|
||||||
|
readOnly: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for desired skills not in available
|
||||||
|
for (const desiredSkill of desiredSkills) {
|
||||||
|
if (availableEntries.some((entry) => entry.key === desiredSkill)) continue;
|
||||||
|
warnings.push(`Desired skill "${desiredSkill}" is not available from the AgentCompany skills directory.`);
|
||||||
|
entries.push({
|
||||||
|
key: desiredSkill,
|
||||||
|
runtimeName: null,
|
||||||
|
desired: true,
|
||||||
|
managed: true,
|
||||||
|
state: "missing",
|
||||||
|
sourcePath: null,
|
||||||
|
targetPath: null,
|
||||||
|
detail: "AgentCompany cannot find this skill in the local runtime skills directory.",
|
||||||
|
origin: "external_unknown",
|
||||||
|
originLabel: "External or unavailable",
|
||||||
|
readOnly: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort((left, right) => left.key.localeCompare(right.key));
|
||||||
|
|
||||||
|
return {
|
||||||
|
adapterType: "codex_local",
|
||||||
|
supported: true,
|
||||||
|
mode: "ephemeral",
|
||||||
|
desiredSkills,
|
||||||
|
entries,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the current skill state for the Codex adapter.
|
||||||
|
* Ephemeral mode: skills are linked into workspace at run time.
|
||||||
|
*/
|
||||||
|
export async function listCodexSkills(
|
||||||
|
ctx: AdapterSkillContext,
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
return buildCodexSkillSnapshot(ctx.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync skills for the Codex adapter.
|
||||||
|
* Ephemeral mode: no on-disk sync needed; skills are linked at run time.
|
||||||
|
* Returns the current snapshot for informational purposes.
|
||||||
|
*/
|
||||||
|
export async function syncCodexSkills(
|
||||||
|
ctx: AdapterSkillContext,
|
||||||
|
_desiredSkills: string[],
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
return buildCodexSkillSnapshot(ctx.config);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export {
|
|||||||
detectGeminiAuthRequired,
|
detectGeminiAuthRequired,
|
||||||
isGeminiTurnLimitResult,
|
isGeminiTurnLimitResult,
|
||||||
} from "./parse.js";
|
} from "./parse.js";
|
||||||
|
export { listGeminiSkills, syncGeminiSkills } from "./skills.js";
|
||||||
import type { AdapterSessionCodec } from "@agentcompany/adapter-utils";
|
import type { AdapterSessionCodec } from "@agentcompany/adapter-utils";
|
||||||
|
|
||||||
function readNonEmptyString(value: unknown): string | null {
|
function readNonEmptyString(value: unknown): string | null {
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Gemini adapter skill listing / sync -- persistent mode
|
||||||
|
// Skills are symlinked into ~/.gemini/skills/ and persist across runs.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import type {
|
||||||
|
AdapterSkillContext,
|
||||||
|
AdapterSkillSnapshot,
|
||||||
|
} from "@agentcompany/adapter-utils";
|
||||||
|
import {
|
||||||
|
parseObject,
|
||||||
|
asString,
|
||||||
|
readAgentCompanyRuntimeSkillEntries,
|
||||||
|
readInstalledSkillTargets,
|
||||||
|
resolveAgentCompanyDesiredSkillNames,
|
||||||
|
buildPersistentSkillSnapshot,
|
||||||
|
ensureAgentCompanySkillSymlink,
|
||||||
|
removeMaintainerOnlySkillSymlinks,
|
||||||
|
} from "@agentcompany/adapter-utils/server-utils";
|
||||||
|
|
||||||
|
function resolveGeminiSkillsHome(config: Record<string, unknown>): string {
|
||||||
|
const env = parseObject(config.env);
|
||||||
|
const home = asString(env.HOME, asString(env.USERPROFILE, ""));
|
||||||
|
const base = home || (process.env.HOME ?? process.env.USERPROFILE ?? "");
|
||||||
|
return path.join(base, ".gemini", "skills");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildGeminiSkillSnapshot(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
const moduleDir = path.resolve(import.meta.dirname ?? __dirname);
|
||||||
|
const availableEntries = await readAgentCompanyRuntimeSkillEntries(config, moduleDir);
|
||||||
|
const desiredSkills = resolveAgentCompanyDesiredSkillNames(config, availableEntries);
|
||||||
|
const skillsHome = resolveGeminiSkillsHome(config);
|
||||||
|
const installed = await readInstalledSkillTargets(skillsHome);
|
||||||
|
|
||||||
|
return buildPersistentSkillSnapshot({
|
||||||
|
adapterType: "gemini_local",
|
||||||
|
availableEntries,
|
||||||
|
desiredSkills,
|
||||||
|
installed,
|
||||||
|
skillsHome,
|
||||||
|
locationLabel: "~/.gemini/skills",
|
||||||
|
missingDetail: "Configured but not currently linked into the Gemini skills home.",
|
||||||
|
externalConflictDetail: "Skill name is occupied by an external installation.",
|
||||||
|
externalDetail: "Installed outside AgentCompany management.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the current skill state for the Gemini adapter.
|
||||||
|
* Persistent mode: reports actual installed state in ~/.gemini/skills/.
|
||||||
|
*/
|
||||||
|
export async function listGeminiSkills(
|
||||||
|
ctx: AdapterSkillContext,
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
return buildGeminiSkillSnapshot(ctx.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync skills for the Gemini adapter.
|
||||||
|
* Persistent mode: creates symlinks for desired skills, removes stale ones,
|
||||||
|
* then returns a fresh snapshot.
|
||||||
|
*/
|
||||||
|
export async function syncGeminiSkills(
|
||||||
|
ctx: AdapterSkillContext,
|
||||||
|
desiredSkills: string[],
|
||||||
|
): Promise<AdapterSkillSnapshot> {
|
||||||
|
const moduleDir = path.resolve(import.meta.dirname ?? __dirname);
|
||||||
|
const config = ctx.config;
|
||||||
|
const availableEntries = await readAgentCompanyRuntimeSkillEntries(config, moduleDir);
|
||||||
|
const skillsHome = resolveGeminiSkillsHome(config);
|
||||||
|
|
||||||
|
// Ensure the skills home directory exists
|
||||||
|
await fs.mkdir(skillsHome, { recursive: true });
|
||||||
|
|
||||||
|
// Build a set of desired skill keys for quick lookup
|
||||||
|
const desiredSet = new Set(desiredSkills);
|
||||||
|
|
||||||
|
// Symlink type for Windows compatibility
|
||||||
|
const symlinkType = process.platform === "win32" ? ("junction" as const) : undefined;
|
||||||
|
const linkSkill = async (source: string, target: string) => {
|
||||||
|
await fs.symlink(source, target, symlinkType);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create/repair symlinks for desired skills
|
||||||
|
const syncedRuntimeNames = new Set<string>();
|
||||||
|
for (const available of availableEntries) {
|
||||||
|
if (!desiredSet.has(available.key)) continue;
|
||||||
|
const target = path.join(skillsHome, available.runtimeName);
|
||||||
|
await ensureAgentCompanySkillSymlink(available.source, target, linkSkill);
|
||||||
|
syncedRuntimeNames.add(available.runtimeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stale maintainer-only symlinks that are no longer desired
|
||||||
|
await removeMaintainerOnlySkillSymlinks(skillsHome, syncedRuntimeNames);
|
||||||
|
|
||||||
|
// Return fresh snapshot reflecting the new state
|
||||||
|
const installed = await readInstalledSkillTargets(skillsHome);
|
||||||
|
return buildPersistentSkillSnapshot({
|
||||||
|
adapterType: "gemini_local",
|
||||||
|
availableEntries,
|
||||||
|
desiredSkills,
|
||||||
|
installed,
|
||||||
|
skillsHome,
|
||||||
|
locationLabel: "~/.gemini/skills",
|
||||||
|
missingDetail: "Configured but not currently linked into the Gemini skills home.",
|
||||||
|
externalConflictDetail: "Skill name is occupied by an external installation.",
|
||||||
|
externalDetail: "Installed outside AgentCompany management.",
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user