diff --git a/src/agents/index.ts b/src/agents/index.ts index f29a86d..7b4621f 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -3,7 +3,11 @@ import { OneCLIRequestError, toOneCLIError, } from "../errors.js"; -import type { CreateAgentInput, CreateAgentResponse } from "./types.js"; +import type { + CreateAgentInput, + CreateAgentResponse, + EnsureAgentResponse, +} from "./types.js"; export class AgentsClient { private baseUrl: string; @@ -57,4 +61,26 @@ export class AgentsClient { throw toOneCLIError(error); } }; + + /** + * Ensure an agent exists. Creates it if missing, returns normally if it already exists. + * Unlike `createAgent`, this method treats a 409 conflict as success. + */ + ensureAgent = async ( + input: CreateAgentInput, + ): Promise => { + try { + await this.createAgent(input); + return { name: input.name, identifier: input.identifier, created: true }; + } catch (error) { + if (error instanceof OneCLIRequestError && error.statusCode === 409) { + return { + name: input.name, + identifier: input.identifier, + created: false, + }; + } + throw error; + } + }; } diff --git a/src/agents/types.ts b/src/agents/types.ts index 24d86ac..d2913ed 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -12,3 +12,10 @@ export interface CreateAgentResponse { identifier: string; createdAt: string; } + +export interface EnsureAgentResponse { + name: string; + identifier: string; + /** Whether the agent was newly created. `false` if it already existed. */ + created: boolean; +} diff --git a/src/client.ts b/src/client.ts index 2bfcccb..6b3e87c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -8,6 +8,7 @@ import type { import type { CreateAgentInput, CreateAgentResponse, + EnsureAgentResponse, } from "./agents/types.js"; const DEFAULT_URL = "https://app.onecli.sh"; @@ -50,4 +51,11 @@ export class OneCLI { createAgent = (input: CreateAgentInput): Promise => { return this.agentsClient.createAgent(input); }; + + /** + * Ensure an agent exists. Creates it if missing, returns normally if it already exists. + */ + ensureAgent = (input: CreateAgentInput): Promise => { + return this.agentsClient.ensureAgent(input); + }; } diff --git a/src/index.ts b/src/index.ts index 01219ca..1c6fb97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,4 +11,5 @@ export type { export type { CreateAgentInput, CreateAgentResponse, + EnsureAgentResponse, } from "./agents/types.js"; diff --git a/test/agents/client.test.ts b/test/agents/client.test.ts index e949055..94ec54e 100644 --- a/test/agents/client.test.ts +++ b/test/agents/client.test.ts @@ -162,7 +162,7 @@ describe("AgentsClient", () => { ).rejects.toThrow("fetch failed"); }); - it("re-throws OneCLIRequestError without wrapping", async () => { + it("re-throws OneCLIRequestError on 500 without wrapping", async () => { fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response("", { status: 500, statusText: "Internal Server Error" }), ); @@ -180,4 +180,88 @@ describe("AgentsClient", () => { expect((err as OneCLIRequestError).name).toBe("OneCLIRequestError"); }); }); + + describe("ensureAgent", () => { + it("returns created: true when agent is newly created", async () => { + fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify(MOCK_AGENT), { status: 201 }), + ); + + const client = new AgentsClient( + "http://localhost:3000", + "oc_test", + 5000, + ); + const result = await client.ensureAgent({ + name: "My Agent", + identifier: "my-agent", + }); + + expect(result).toEqual({ + name: "My Agent", + identifier: "my-agent", + created: true, + }); + }); + + it("returns created: false when agent already exists (409)", async () => { + fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify({ error: "identifier already exists" }), + { status: 409, statusText: "Conflict" }, + ), + ); + + const client = new AgentsClient( + "http://localhost:3000", + "oc_test", + 5000, + ); + const result = await client.ensureAgent({ + name: "My Agent", + identifier: "my-agent", + }); + + expect(result).toEqual({ + name: "My Agent", + identifier: "my-agent", + created: false, + }); + }); + + it("throws on non-409 errors", async () => { + fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + statusText: "Unauthorized", + }), + ); + + const client = new AgentsClient( + "http://localhost:3000", + "oc_bad", + 5000, + ); + + await expect( + client.ensureAgent({ name: "Test", identifier: "test" }), + ).rejects.toThrow(OneCLIRequestError); + }); + + it("throws on network errors", async () => { + fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockRejectedValue(new TypeError("fetch failed")); + + const client = new AgentsClient( + "http://localhost:3000", + "oc_test", + 5000, + ); + + await expect( + client.ensureAgent({ name: "Test", identifier: "test" }), + ).rejects.toThrow(OneCLIError); + }); + }); });