diff --git a/docs/user-guide/asset-registry-commands.md b/docs/user-guide/asset-registry-commands.md index 8f3e56e..300e60e 100644 --- a/docs/user-guide/asset-registry-commands.md +++ b/docs/user-guide/asset-registry-commands.md @@ -160,3 +160,26 @@ Options: - `--assetType ` (required) – The asset type identifier - `--json` – Write the methodology to a JSON file in the working directory + +## List Skills + +List all available agent skills registered in the asset registry (name, description, and path). + +``` +content-cli asset-registry skills list +``` + +Example output: + +``` +skill-one (platform/skill-one) - First platform skill +board-authoring (asset/BOARD_V2/board-authoring) +``` + +Each line is ` ()` followed by ` - ` when the skill provides one. + +Use `--json` to write the full response to a file in the working directory: + +``` +content-cli asset-registry skills list --json +``` diff --git a/src/commands/asset-registry/asset-registry-api.ts b/src/commands/asset-registry/asset-registry-api.ts index 3fb9fe7..2543723 100644 --- a/src/commands/asset-registry/asset-registry-api.ts +++ b/src/commands/asset-registry/asset-registry-api.ts @@ -1,6 +1,10 @@ import { HttpClient } from "../../core/http/http-client"; import { Context } from "../../core/command/cli-context"; -import { AssetRegistryDescriptor, AssetRegistryMetadata } from "./asset-registry.interfaces"; +import { + AgentSkillsResponse, + AssetRegistryDescriptor, + AssetRegistryMetadata, +} from "./asset-registry.interfaces"; import { FatalError } from "../../core/utils/logger"; export class AssetRegistryApi { @@ -18,6 +22,14 @@ export class AssetRegistryApi { }); } + public async listSkills(): Promise { + return this.httpClient() + .get("/pacman/api/core/asset-registry/skills") + .catch((e) => { + throw new FatalError(`Problem listing asset registry skills: ${e}`); + }); + } + public async getType(assetType: string): Promise { return this.httpClient() .get(`/pacman/api/core/asset-registry/types/${encodeURIComponent(assetType)}`) diff --git a/src/commands/asset-registry/asset-registry.interfaces.ts b/src/commands/asset-registry/asset-registry.interfaces.ts index 23acc4b..a3b0d6e 100644 --- a/src/commands/asset-registry/asset-registry.interfaces.ts +++ b/src/commands/asset-registry/asset-registry.interfaces.ts @@ -42,3 +42,18 @@ export interface ValidateOptions { file?: string; json: boolean; } + +export interface AgentSkillMetadata { + version: string; +} + +export interface AgentSkill { + name: string; + description: string; + path: string; + metadata: AgentSkillMetadata; +} + +export interface AgentSkillsResponse { + skills: AgentSkill[]; +} diff --git a/src/commands/asset-registry/asset-registry.service.ts b/src/commands/asset-registry/asset-registry.service.ts index becc5ae..7b39a90 100644 --- a/src/commands/asset-registry/asset-registry.service.ts +++ b/src/commands/asset-registry/asset-registry.service.ts @@ -1,5 +1,5 @@ import { AssetRegistryApi } from "./asset-registry-api"; -import { AssetRegistryDescriptor, ValidateOptions } from "./asset-registry.interfaces"; +import { AgentSkill, AssetRegistryDescriptor, ValidateOptions } from "./asset-registry.interfaces"; import { Context } from "../../core/command/cli-context"; import { fileService, FileService } from "../../core/utils/file-service"; import { FatalError, logger } from "../../core/utils/logger"; @@ -32,6 +32,24 @@ export class AssetRegistryService { } } + public async listSkills(jsonResponse: boolean): Promise { + const response = await this.api.listSkills(); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(response), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + if (response.skills.length === 0) { + logger.info("No agent skills registered."); + return; + } + response.skills.forEach((skill) => { + this.logSkillSummary(skill); + }); + } + } + public async getType(assetType: string, jsonResponse: boolean): Promise { const descriptor = await this.api.getType(assetType); @@ -136,6 +154,15 @@ export class AssetRegistryService { ); } + private logSkillSummary(skill: AgentSkill): void { + const base = `${skill.name} (${skill.path})`; + if (skill.description) { + logger.info(`${base} - ${skill.description}`); + } else { + logger.info(base); + } + } + private logDescriptorDetail(descriptor: AssetRegistryDescriptor): void { logger.info(`Asset Type: ${descriptor.assetType}`); logger.info(`Display Name: ${descriptor.displayName}`); diff --git a/src/commands/asset-registry/module.ts b/src/commands/asset-registry/module.ts index 9e2f5ec..9560734 100644 --- a/src/commands/asset-registry/module.ts +++ b/src/commands/asset-registry/module.ts @@ -47,6 +47,14 @@ class Module extends IModule { .requiredOption("--assetType ", "The asset type identifier (e.g., BOARD_V2)") .option("--json", "Return the response as a JSON file") .action(this.getMethodology); + + const skillsCommand = assetRegistryCommand.command("skills") + .description("Discover agent skills exposed by the asset registry"); + + skillsCommand.command("list") + .description("List all available agent skills (name, description, path)") + .option("--json", "Return the response as a JSON file") + .action(this.listSkills); } private async listTypes(context: Context, command: Command, options: OptionValues): Promise { @@ -79,6 +87,10 @@ class Module extends IModule { private async getMethodology(context: Context, command: Command, options: OptionValues): Promise { await new AssetRegistryService(context).getMethodology(options.assetType, !!options.json); } + + private async listSkills(context: Context, command: Command, options: OptionValues): Promise { + await new AssetRegistryService(context).listSkills(!!options.json); + } } export = Module; diff --git a/tests/commands/asset-registry/asset-registry-module.spec.ts b/tests/commands/asset-registry/asset-registry-module.spec.ts index 9ebd9ea..fc6fa67 100644 --- a/tests/commands/asset-registry/asset-registry-module.spec.ts +++ b/tests/commands/asset-registry/asset-registry-module.spec.ts @@ -17,6 +17,7 @@ describe("Asset Registry Module", () => { mockService = { listTypes: jest.fn().mockResolvedValue(undefined), + listSkills: jest.fn().mockResolvedValue(undefined), getType: jest.fn().mockResolvedValue(undefined), getSchema: jest.fn().mockResolvedValue(undefined), validate: jest.fn().mockResolvedValue(undefined), @@ -105,6 +106,17 @@ describe("Asset Registry Module", () => { expect(mockService.listTypes).toHaveBeenCalledWith(true); }); + it("should call listSkills", async () => { + const options: OptionValues = { json: true }; + await (module as any).listSkills(testContext, mockCommand, options); + expect(mockService.listSkills).toHaveBeenCalledWith(true); + + jest.clearAllMocks(); + const optionsNoJson: OptionValues = { json: "" }; + await (module as any).listSkills(testContext, mockCommand, optionsNoJson); + expect(mockService.listSkills).toHaveBeenCalledWith(false); + }); + it("should call getType", async () => { const options: OptionValues = { assetType: "BOARD_V2", json: "" }; await (module as any).getType(testContext, mockCommand, options); diff --git a/tests/commands/asset-registry/asset-registry-skills-list.spec.ts b/tests/commands/asset-registry/asset-registry-skills-list.spec.ts new file mode 100644 index 0000000..4a63d99 --- /dev/null +++ b/tests/commands/asset-registry/asset-registry-skills-list.spec.ts @@ -0,0 +1,70 @@ +import { AgentSkillsResponse } from "../../../src/commands/asset-registry/asset-registry.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { AssetRegistryService } from "../../../src/commands/asset-registry/asset-registry.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Asset registry skills list", () => { + const skillsResponse: AgentSkillsResponse = { + skills: [ + { + name: "skill-one", + description: "First platform skill", + path: "platform/skill-one", + metadata: { version: "1.0.0" }, + }, + { + name: "board-authoring", + description: "", + path: "asset/BOARD_V2/board-authoring", + metadata: { version: "2.0.0" }, + }, + ], + }; + + it("Should list all skills with description when present", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/skills", skillsResponse); + + await new AssetRegistryService(testContext).listSkills(false); + + expect(loggingTestTransport.logMessages.length).toBe(2); + + expect(loggingTestTransport.logMessages[0].message).toContain( + "skill-one (platform/skill-one) - First platform skill" + ); + + expect(loggingTestTransport.logMessages[1].message).toContain( + "board-authoring (asset/BOARD_V2/board-authoring)" + ); + expect(loggingTestTransport.logMessages[1].message).not.toMatch(/ - /); + }); + + it("Should list all skills as JSON", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/skills", skillsResponse); + + await new AssetRegistryService(testContext).listSkills(true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + expect(mockWriteFileSync).toHaveBeenCalledWith( + path.resolve(process.cwd(), expectedFileName), + expect.any(String), + { encoding: "utf-8", mode: 0o600 } + ); + + const written = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as AgentSkillsResponse; + expect(written.skills.length).toBe(2); + expect(written.skills[0].name).toBe("skill-one"); + expect(written.skills[1].path).toBe("asset/BOARD_V2/board-authoring"); + }); + + it("Should handle empty skills list", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/skills", { skills: [] }); + + await new AssetRegistryService(testContext).listSkills(false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain("No agent skills registered"); + }); +});