diff --git a/packages/plugins/official/hubspot-crm/__tests__/index.test.ts b/packages/plugins/official/hubspot-crm/__tests__/index.test.ts
new file mode 100644
index 0000000..fab0223
--- /dev/null
+++ b/packages/plugins/official/hubspot-crm/__tests__/index.test.ts
@@ -0,0 +1,1386 @@
+///
+/**
+ * HubSpot CRM — Unit Tests
+ *
+ * Covers: DB key helpers, hubspotRequest (success + HTTP error),
+ * searchContacts, getContact, getDeals (with/without pipeline filter),
+ * createNote, associateNoteWithContact, updateDealStage,
+ * generateEnrichmentSummary (choices shape, content shape, error),
+ * plugin manifest/settings, app:init (6 endpoints),
+ * POST /connect (success, missing key, invalid key),
+ * GET /contacts (success, no API key, with query, with limit),
+ * GET /contacts/:id (success, no API key, 404, other error),
+ * POST /contacts/:id/enrich (success, no API key, missing text, with conversationId, AI error, HubSpot error),
+ * GET /deals (success, no API key, pipelineId from query, pipelineId from config, limit),
+ * POST /deals/:id/update-stage (success, no API key, missing stage, HubSpot error),
+ * conversation:end (autoLog false, no conversationId, stores pending, logs to HubSpot when association found, no API key stores pending only).
+ */
+import plugin, {
+ buildContactKey,
+ buildEnrichmentKey,
+ buildPendingKey,
+ buildAssociationKey,
+ hubspotRequest,
+ searchContacts,
+ getContact,
+ getDeals,
+ createNote,
+ associateNoteWithContact,
+ updateDealStage,
+ generateEnrichmentSummary,
+ HUBSPOT_API_BASE,
+ AI_COMPLETIONS_PATH,
+ CONNECTION_KEY,
+ DEFAULT_ENRICH_MODEL,
+ HubSpotContact,
+ HubSpotDeal,
+ HubSpotNote,
+ ContactInteractionRecord,
+ EnrichmentRecord,
+ PendingInteractionRecord,
+} from "../src/index";
+import {
+ PluginContext,
+ PluginAPI,
+ PluginDatabaseAPI,
+ PluginEventBus,
+ EndpointDefinition,
+ EndpointRequest,
+ EndpointResponse,
+} from "@agentbase/plugin-sdk";
+
+// ── Mock factory ──────────────────────────────────────────────────────────────
+
+function createMockAPI(
+ configOverrides: Record = {},
+): PluginAPI & { _endpoints: EndpointDefinition[] } {
+ const store = new Map();
+ const _endpoints: EndpointDefinition[] = [];
+
+ const db: PluginDatabaseAPI = {
+ set: jest
+ .fn()
+ .mockImplementation(async (k: string, v: unknown) => store.set(k, v)),
+ get: jest
+ .fn()
+ .mockImplementation(async (k: string) => store.get(k) ?? null),
+ delete: jest.fn().mockImplementation(async (k: string) => {
+ const had = store.has(k);
+ store.delete(k);
+ return had;
+ }),
+ keys: jest
+ .fn()
+ .mockImplementation(async (prefix?: string) =>
+ [...store.keys()].filter((k) => !prefix || k.startsWith(prefix)),
+ ),
+ find: jest.fn().mockResolvedValue([]),
+ count: jest.fn().mockResolvedValue(0),
+ };
+
+ const events: PluginEventBus = {
+ emit: jest.fn().mockResolvedValue(undefined),
+ on: jest.fn(),
+ off: jest.fn(),
+ };
+
+ const defaultConfig = new Map([
+ ["hubspotApiKey", "pat-test-key"],
+ ["autoLogConversations", true],
+ ["enrichModel", "gpt-4o-mini"],
+ ["pipelineId", ""],
+ ...Object.entries(configOverrides),
+ ]);
+
+ return {
+ _endpoints,
+ getConfig: jest
+ .fn()
+ .mockImplementation((k: string) => defaultConfig.get(k) ?? undefined),
+ setConfig: jest
+ .fn()
+ .mockImplementation(async (k: string, v: unknown) =>
+ defaultConfig.set(k, v),
+ ),
+ makeRequest: jest.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue(""),
+ }),
+ log: jest.fn(),
+ db,
+ events,
+ registerEndpoint: jest
+ .fn()
+ .mockImplementation((def: EndpointDefinition) => _endpoints.push(def)),
+ registerCronJob: jest.fn(),
+ registerWebhook: jest.fn(),
+ registerAdminPage: jest.fn(),
+ } as unknown as PluginAPI & { _endpoints: EndpointDefinition[] };
+}
+
+type MockAPI = ReturnType;
+type MockCtx = PluginContext & { api: MockAPI };
+
+function makeCtx(
+ overrides: Partial = {},
+ configOverrides: Record = {},
+): MockCtx {
+ const api = createMockAPI(configOverrides);
+ return {
+ appId: "app-1",
+ userId: "user-1",
+ config: {},
+ api,
+ ...overrides,
+ } as MockCtx;
+}
+
+interface MockRes {
+ status: jest.Mock;
+ json: jest.Mock;
+ _status: number;
+ _body: unknown;
+}
+
+function makeRes(): MockRes {
+ const r: MockRes = {
+ _status: 200,
+ _body: undefined,
+ status: jest.fn(),
+ json: jest.fn(),
+ };
+ r.status.mockImplementation((code: number) => {
+ r._status = code;
+ return r;
+ });
+ r.json.mockImplementation((body: unknown) => {
+ r._body = body;
+ });
+ return r;
+}
+
+function makeReq(overrides: Partial = {}): EndpointRequest {
+ return {
+ method: "GET",
+ path: "/",
+ params: {},
+ query: {},
+ body: {},
+ headers: {},
+ ...overrides,
+ };
+}
+
+async function initPlugin(api: MockAPI): Promise {
+ const hook = plugin.definition.hooks?.["app:init"];
+ await hook?.({ appId: "app-1", userId: "user-1", config: {}, api });
+ return api._endpoints;
+}
+
+function getEp(
+ endpoints: EndpointDefinition[],
+ method: string,
+ path: string,
+): EndpointDefinition | undefined {
+ return endpoints.find((e) => e.method === method && e.path === path);
+}
+
+// Stub a successful makeRequest returning JSON
+function okJson(data: T): jest.Mock {
+ return jest.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ json: jest.fn().mockResolvedValue(data),
+ text: jest.fn().mockResolvedValue(""),
+ });
+}
+
+// Stub a failed makeRequest
+function failHttp(status: number, body = "Bad Request"): jest.Mock {
+ return jest.fn().mockResolvedValue({
+ ok: false,
+ status,
+ statusText: body,
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue(body),
+ });
+}
+
+// ── DB Key helpers ────────────────────────────────────────────────────────────
+
+describe("buildContactKey", () => {
+ it("formats contact:hubspotId:conversationId", () => {
+ expect(buildContactKey("hs-123", "conv-456")).toBe(
+ "contact:hs-123:conv-456",
+ );
+ });
+
+ it("handles ids with special characters", () => {
+ expect(buildContactKey("a:b", "c:d")).toBe("contact:a:b:c:d");
+ });
+});
+
+describe("buildEnrichmentKey", () => {
+ it("formats enrichment:contactId", () => {
+ expect(buildEnrichmentKey("contact-99")).toBe("enrichment:contact-99");
+ });
+});
+
+describe("buildPendingKey", () => {
+ it("formats pending:conversationId", () => {
+ expect(buildPendingKey("conv-abc")).toBe("pending:conv-abc");
+ });
+});
+
+describe("buildAssociationKey", () => {
+ it("formats association:conversationId", () => {
+ expect(buildAssociationKey("conv-xyz")).toBe("association:conv-xyz");
+ });
+});
+
+// ── Constants ─────────────────────────────────────────────────────────────────
+
+describe("constants", () => {
+ it("HUBSPOT_API_BASE is the HubSpot API root", () => {
+ expect(HUBSPOT_API_BASE).toBe("https://api.hubapi.com");
+ });
+
+ it("CONNECTION_KEY is connection:config", () => {
+ expect(CONNECTION_KEY).toBe("connection:config");
+ });
+
+ it("DEFAULT_ENRICH_MODEL is gpt-4o-mini", () => {
+ expect(DEFAULT_ENRICH_MODEL).toBe("gpt-4o-mini");
+ });
+
+ it("AI_COMPLETIONS_PATH has the correct prefix", () => {
+ expect(AI_COMPLETIONS_PATH).toMatch(/^\/api\/v1/);
+ });
+});
+
+// ── hubspotRequest ────────────────────────────────────────────────────────────
+
+describe("hubspotRequest", () => {
+ it("sends GET with Authorization header", async () => {
+ const mr = okJson({ results: [] });
+ await hubspotRequest(mr, "my-key", "GET", "/crm/v3/objects/contacts");
+ expect(mr).toHaveBeenCalledWith(
+ `${HUBSPOT_API_BASE}/crm/v3/objects/contacts`,
+ expect.objectContaining({
+ method: "GET",
+ headers: expect.objectContaining({ Authorization: "Bearer my-key" }),
+ }),
+ );
+ });
+
+ it("sends POST body as JSON string", async () => {
+ const mr = okJson({ id: "1", properties: {} });
+ await hubspotRequest(mr, "key", "POST", "/crm/v3/objects/notes", {
+ properties: { hs_note_body: "hello" },
+ });
+ const [, init] = (mr as jest.Mock).mock.calls[0] as [string, RequestInit];
+ expect(JSON.parse(init.body as string)).toMatchObject({
+ properties: { hs_note_body: "hello" },
+ });
+ });
+
+ it("sends PATCH without body when body is undefined", async () => {
+ const mr = okJson({ id: "d1", properties: {} });
+ await hubspotRequest(mr, "key", "GET", "/crm/v3/objects/deals/d1");
+ const [, init] = (mr as jest.Mock).mock.calls[0] as [string, RequestInit];
+ expect(init.body).toBeUndefined();
+ });
+
+ it("throws on non-2xx response", async () => {
+ const mr = failHttp(401, "Unauthorized");
+ await expect(
+ hubspotRequest(mr, "bad-key", "GET", "/crm/v3/objects/contacts"),
+ ).rejects.toThrow("HubSpot API error 401");
+ });
+
+ it("returns the parsed JSON on success", async () => {
+ const payload = { id: "42", properties: { firstname: "Ada" } };
+ const mr = okJson(payload);
+ const result = await hubspotRequest(
+ mr,
+ "key",
+ "GET",
+ "/crm/v3/objects/contacts/42",
+ );
+ expect(result.id).toBe("42");
+ expect(result.properties.firstname).toBe("Ada");
+ });
+});
+
+// ── searchContacts ────────────────────────────────────────────────────────────
+
+describe("searchContacts", () => {
+ it("calls POST /crm/v3/objects/contacts/search", async () => {
+ const contacts: HubSpotContact[] = [
+ { id: "1", properties: { email: "ada@example.com" } },
+ ];
+ const mr = okJson({ results: contacts, total: 1 });
+ const result = await searchContacts(mr, "key", "ada", 10);
+ expect(mr).toHaveBeenCalledWith(
+ `${HUBSPOT_API_BASE}/crm/v3/objects/contacts/search`,
+ expect.objectContaining({ method: "POST" }),
+ );
+ expect(result).toHaveLength(1);
+ expect(result[0].properties.email).toBe("ada@example.com");
+ });
+
+ it("returns empty array when results is empty", async () => {
+ const mr = okJson({ results: [], total: 0 });
+ const result = await searchContacts(mr, "key", "nobody", 20);
+ expect(result).toEqual([]);
+ });
+
+ it("passes query and limit in POST body", async () => {
+ const mr = okJson({ results: [], total: 0 });
+ await searchContacts(mr, "key", "test query", 15);
+ const [, init] = (mr as jest.Mock).mock.calls[0] as [string, RequestInit];
+ const body = JSON.parse(init.body as string);
+ expect(body.query).toBe("test query");
+ expect(body.limit).toBe(15);
+ });
+});
+
+// ── getContact ────────────────────────────────────────────────────────────────
+
+describe("getContact", () => {
+ it("calls GET /crm/v3/objects/contacts/:id with properties", async () => {
+ const contact: HubSpotContact = {
+ id: "c1",
+ properties: {
+ firstname: "Grace",
+ lastname: "Hopper",
+ email: "grace@navy.mil",
+ },
+ };
+ const mr = okJson(contact);
+ const result = await getContact(mr, "key", "c1");
+ const [url] = (mr as jest.Mock).mock.calls[0] as [string];
+ expect(url).toContain("/crm/v3/objects/contacts/c1");
+ expect(url).toContain("properties=");
+ expect(result.properties.firstname).toBe("Grace");
+ });
+
+ it("propagates 404 errors from HubSpot", async () => {
+ const mr = failHttp(404, "Not Found");
+ await expect(getContact(mr, "key", "missing")).rejects.toThrow("404");
+ });
+
+ it("returns full contact properties", async () => {
+ const contact: HubSpotContact = {
+ id: "c2",
+ properties: { company: "ENIAC Corp", phone: "555-0100" },
+ };
+ const mr = okJson(contact);
+ const result = await getContact(mr, "key", "c2");
+ expect(result.properties.company).toBe("ENIAC Corp");
+ });
+});
+
+// ── getDeals ──────────────────────────────────────────────────────────────────
+
+describe("getDeals", () => {
+ const allDeals: HubSpotDeal[] = [
+ {
+ id: "d1",
+ properties: {
+ dealname: "Deal A",
+ dealstage: "appointmentscheduled",
+ pipeline: "pipe-1",
+ },
+ },
+ {
+ id: "d2",
+ properties: {
+ dealname: "Deal B",
+ dealstage: "qualifiedtobuy",
+ pipeline: "pipe-2",
+ },
+ },
+ ];
+
+ it("returns all deals when no pipelineId", async () => {
+ const mr = okJson({ results: allDeals });
+ const result = await getDeals(mr, "key");
+ expect(result).toHaveLength(2);
+ });
+
+ it("filters deals by pipelineId", async () => {
+ const mr = okJson({ results: allDeals });
+ const result = await getDeals(mr, "key", "pipe-1");
+ expect(result).toHaveLength(1);
+ expect(result[0].id).toBe("d1");
+ });
+
+ it("includes limit and properties in query string", async () => {
+ const mr = okJson({ results: [] });
+ await getDeals(mr, "key", undefined, 50);
+ const [url] = (mr as jest.Mock).mock.calls[0] as [string];
+ expect(url).toContain("limit=50");
+ expect(url).toContain("properties=");
+ });
+
+ it("returns empty array when no deals exist", async () => {
+ const mr = okJson({ results: [] });
+ const result = await getDeals(mr, "key");
+ expect(result).toEqual([]);
+ });
+});
+
+// ── createNote ────────────────────────────────────────────────────────────────
+
+describe("createNote", () => {
+ it("calls POST /crm/v3/objects/notes with note body", async () => {
+ const note: HubSpotNote = {
+ id: "n1",
+ properties: {
+ hs_note_body: "Test note",
+ hs_timestamp: "2026-01-01T00:00:00.000Z",
+ },
+ };
+ const mr = okJson(note);
+ const result = await createNote(mr, "key", "Test note");
+ expect(mr).toHaveBeenCalledWith(
+ `${HUBSPOT_API_BASE}/crm/v3/objects/notes`,
+ expect.objectContaining({ method: "POST" }),
+ );
+ const [, init] = (mr as jest.Mock).mock.calls[0] as [string, RequestInit];
+ const body = JSON.parse(init.body as string);
+ expect(body.properties.hs_note_body).toBe("Test note");
+ expect(result.id).toBe("n1");
+ });
+
+ it("includes hs_timestamp in the request", async () => {
+ const mr = okJson({ id: "n2", properties: {} });
+ await createNote(mr, "key", "Hello");
+ const [, init] = (mr as jest.Mock).mock.calls[0] as [string, RequestInit];
+ const body = JSON.parse(init.body as string);
+ expect(typeof body.properties.hs_timestamp).toBe("string");
+ expect(body.properties.hs_timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
+ });
+
+ it("throws on HubSpot error", async () => {
+ const mr = failHttp(403, "Forbidden");
+ await expect(createNote(mr, "key", "body")).rejects.toThrow("403");
+ });
+});
+
+// ── associateNoteWithContact ──────────────────────────────────────────────────
+
+describe("associateNoteWithContact", () => {
+ it("posts to associations batch endpoint with correct payload", async () => {
+ const mr = okJson({ results: [] });
+ await associateNoteWithContact(mr, "key", "note-1", "contact-1");
+ const [url, init] = (mr as jest.Mock).mock.calls[0] as [
+ string,
+ RequestInit,
+ ];
+ expect(url).toContain("/crm/v3/associations/notes/contacts/batch/create");
+ const body = JSON.parse(init.body as string);
+ expect(body.inputs[0].from.id).toBe("note-1");
+ expect(body.inputs[0].to.id).toBe("contact-1");
+ expect(body.inputs[0].type).toBe("note_to_contact");
+ });
+
+ it("throws when association POST fails", async () => {
+ const mr = failHttp(400, "Bad input");
+ await expect(
+ associateNoteWithContact(mr, "key", "n1", "c1"),
+ ).rejects.toThrow("400");
+ });
+});
+
+// ── updateDealStage ───────────────────────────────────────────────────────────
+
+describe("updateDealStage", () => {
+ it("sends PATCH to /crm/v3/objects/deals/:id with stage", async () => {
+ const deal: HubSpotDeal = {
+ id: "d1",
+ properties: { dealstage: "closedwon", pipeline: "pipe-1" },
+ };
+ const mr = okJson(deal);
+ const result = await updateDealStage(mr, "key", "d1", "closedwon");
+ const [url, init] = (mr as jest.Mock).mock.calls[0] as [
+ string,
+ RequestInit,
+ ];
+ expect(url).toContain("/crm/v3/objects/deals/d1");
+ expect((init as RequestInit).method).toBe("PATCH");
+ const body = JSON.parse((init as RequestInit).body as string);
+ expect(body.properties.dealstage).toBe("closedwon");
+ expect(result.properties.dealstage).toBe("closedwon");
+ });
+
+ it("throws on HubSpot error", async () => {
+ const mr = failHttp(404, "Deal not found");
+ await expect(
+ updateDealStage(mr, "key", "missing", "stage"),
+ ).rejects.toThrow("404");
+ });
+});
+
+// ── generateEnrichmentSummary ─────────────────────────────────────────────────
+
+describe("generateEnrichmentSummary", () => {
+ it("extracts summary from choices[0].message.content", async () => {
+ const mr = okJson({
+ choices: [{ message: { content: "CRM summary text." } }],
+ });
+ const result = await generateEnrichmentSummary(
+ mr,
+ "User said hi.",
+ "gpt-4o-mini",
+ );
+ expect(result).toBe("CRM summary text.");
+ });
+
+ it("falls back to top-level content field", async () => {
+ const mr = okJson({ content: "Fallback summary." });
+ const result = await generateEnrichmentSummary(mr, "Hello world");
+ expect(result).toBe("Fallback summary.");
+ });
+
+ it("throws on unexpected AI response shape", async () => {
+ const mr = okJson({ unexpected: true });
+ await expect(generateEnrichmentSummary(mr, "Hello")).rejects.toThrow(
+ "Unexpected AI response shape",
+ );
+ });
+
+ it("throws on non-200 AI response", async () => {
+ const mr = failHttp(500, "Internal Server Error");
+ await expect(generateEnrichmentSummary(mr, "Hello")).rejects.toThrow(
+ "AI service error: 500",
+ );
+ });
+
+ it("uses DEFAULT_ENRICH_MODEL when model is omitted", async () => {
+ const mr = okJson({
+ choices: [{ message: { content: "summary" } }],
+ });
+ await generateEnrichmentSummary(mr, "text");
+ const [, init] = (mr as jest.Mock).mock.calls[0] as [string, RequestInit];
+ const body = JSON.parse(init.body as string);
+ expect(body.model).toBe(DEFAULT_ENRICH_MODEL);
+ });
+
+ it("sends conversation text in the prompt", async () => {
+ const mr = okJson({
+ choices: [{ message: { content: "ok" } }],
+ });
+ await generateEnrichmentSummary(mr, "The user asked about pricing.");
+ const [, init] = (mr as jest.Mock).mock.calls[0] as [string, RequestInit];
+ const body = JSON.parse(init.body as string);
+ const userMsg = body.messages.find(
+ (m: { role: string }) => m.role === "user",
+ );
+ expect(userMsg?.content).toContain("The user asked about pricing.");
+ });
+});
+
+// ── Plugin manifest ───────────────────────────────────────────────────────────
+
+describe("plugin manifest", () => {
+ it("has name hubspot-crm", () => {
+ expect(plugin.manifest.name).toBe("hubspot-crm");
+ });
+
+ it("has version 1.0.0", () => {
+ expect(plugin.manifest.version).toBe("1.0.0");
+ });
+
+ it("requires network:external and db:readwrite permissions", () => {
+ expect(plugin.manifest.permissions).toContain("network:external");
+ expect(plugin.manifest.permissions).toContain("db:readwrite");
+ });
+});
+
+// ── Plugin settings ───────────────────────────────────────────────────────────
+
+describe("plugin settings", () => {
+ const settings = plugin.definition.settings!;
+
+ it("has hubspotApiKey as encrypted string", () => {
+ expect(settings["hubspotApiKey"].type).toBe("string");
+ expect(settings["hubspotApiKey"].encrypted).toBe(true);
+ });
+
+ it("has autoLogConversations boolean defaulting to true", () => {
+ expect(settings["autoLogConversations"].type).toBe("boolean");
+ expect(settings["autoLogConversations"].default).toBe(true);
+ });
+
+ it("has enrichModel select with gpt-4o-mini as default", () => {
+ expect(settings["enrichModel"].type).toBe("select");
+ expect(settings["enrichModel"].default).toBe("gpt-4o-mini");
+ expect(settings["enrichModel"].options).toContain("gpt-4o");
+ expect(settings["enrichModel"].options).toContain("claude-3-5-sonnet");
+ });
+
+ it("has pipelineId string setting", () => {
+ expect(settings["pipelineId"].type).toBe("string");
+ });
+
+ it("defines exactly 4 settings", () => {
+ expect(Object.keys(settings)).toHaveLength(4);
+ });
+});
+
+// ── app:init — endpoint registration ─────────────────────────────────────────
+
+describe("app:init hook", () => {
+ it("registers exactly 6 endpoints", async () => {
+ const api = createMockAPI();
+ await initPlugin(api);
+ expect(api._endpoints).toHaveLength(6);
+ });
+
+ it("all endpoints require auth", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ expect(endpoints.every((e) => e.auth === true)).toBe(true);
+ });
+
+ it("registers POST /connect", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ expect(getEp(endpoints, "POST", "/connect")).toBeDefined();
+ });
+
+ it("registers GET /contacts", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ expect(getEp(endpoints, "GET", "/contacts")).toBeDefined();
+ });
+
+ it("registers GET /contacts/:id", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ expect(getEp(endpoints, "GET", "/contacts/:id")).toBeDefined();
+ });
+
+ it("registers POST /contacts/:id/enrich", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ expect(getEp(endpoints, "POST", "/contacts/:id/enrich")).toBeDefined();
+ });
+
+ it("registers GET /deals", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ expect(getEp(endpoints, "GET", "/deals")).toBeDefined();
+ });
+
+ it("registers POST /deals/:id/update-stage", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ expect(getEp(endpoints, "POST", "/deals/:id/update-stage")).toBeDefined();
+ });
+});
+
+// ── POST /connect ─────────────────────────────────────────────────────────────
+
+describe("POST /connect", () => {
+ it("validates key, stores config, returns connected: true", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: jest.fn().mockResolvedValue({ results: [] }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "POST", "/connect")!;
+ const req = makeReq({ body: { apiKey: "pat-live-123" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(200);
+ expect(res._body).toMatchObject({ connected: true });
+ expect(api.db.set).toHaveBeenCalledWith(
+ CONNECTION_KEY,
+ expect.objectContaining({ apiKey: "pat-live-123" }),
+ );
+ });
+
+ it("returns 400 when apiKey is missing", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "POST", "/connect")!;
+ const req = makeReq({ body: {} });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(400);
+ expect(res._body).toMatchObject({
+ error: expect.stringContaining("required"),
+ });
+ });
+
+ it("returns 400 when HubSpot rejects the token", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 401,
+ statusText: "Unauthorized",
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("Unauthorized"),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "POST", "/connect")!;
+ const req = makeReq({ body: { apiKey: "bad-key" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(400);
+ expect(res._body).toMatchObject({
+ error: expect.stringContaining("Invalid API key"),
+ });
+ });
+});
+
+// ── GET /contacts ─────────────────────────────────────────────────────────────
+
+describe("GET /contacts", () => {
+ it("returns contacts from HubSpot search", async () => {
+ const contacts: HubSpotContact[] = [
+ { id: "c1", properties: { email: "test@example.com" } },
+ ];
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: jest.fn().mockResolvedValue({ results: contacts, total: 1 }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/contacts")!;
+ const req = makeReq({ query: { q: "test" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(200);
+ expect(res._body).toMatchObject({
+ contacts: expect.arrayContaining([expect.objectContaining({ id: "c1" })]),
+ });
+ });
+
+ it("returns 400 when API key not configured", async () => {
+ const api = createMockAPI({ hubspotApiKey: "" });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/contacts")!;
+ const res = makeRes();
+
+ await ep.handler(makeReq(), res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(400);
+ expect(res._body).toMatchObject({
+ error: expect.stringContaining("not configured"),
+ });
+ });
+
+ it("caps limit at 100", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: jest.fn().mockResolvedValue({ results: [], total: 0 }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/contacts")!;
+ const req = makeReq({ query: { q: "", limit: "500" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ // makeRequest is called for the search; check the body's limit field
+ const allCalls = (api.makeRequest as jest.Mock).mock.calls as [
+ string,
+ RequestInit,
+ ][];
+ const searchCall = allCalls.find(([url]) =>
+ url.includes("/contacts/search"),
+ );
+ const body = JSON.parse(searchCall![1].body as string);
+ expect(body.limit).toBeLessThanOrEqual(100);
+ });
+
+ it("returns 502 on HubSpot network error", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 503,
+ statusText: "Service Unavailable",
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("Service Unavailable"),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/contacts")!;
+ const req = makeReq({ query: { q: "x" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(502);
+ });
+});
+
+// ── GET /contacts/:id ─────────────────────────────────────────────────────────
+
+describe("GET /contacts/:id", () => {
+ it("returns a single contact", async () => {
+ const contact: HubSpotContact = {
+ id: "c1",
+ properties: { firstname: "Alan", lastname: "Turing" },
+ };
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ status: 200,
+ json: jest.fn().mockResolvedValue(contact),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/contacts/:id")!;
+ const req = makeReq({ params: { id: "c1" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(200);
+ expect(res._body).toMatchObject({ contact: { id: "c1" } });
+ });
+
+ it("returns 400 when API key not configured", async () => {
+ const api = createMockAPI({ hubspotApiKey: "" });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/contacts/:id")!;
+ const res = makeRes();
+
+ await ep.handler(
+ makeReq({ params: { id: "x" } }),
+ res as unknown as EndpointResponse,
+ );
+
+ expect(res._status).toBe(400);
+ });
+
+ it("returns 404 when HubSpot returns 404", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 404,
+ statusText: "Not Found",
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("Not Found"),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/contacts/:id")!;
+ const req = makeReq({ params: { id: "missing" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(404);
+ expect(res._body).toMatchObject({ error: "Contact not found" });
+ });
+
+ it("returns 502 on other HubSpot errors", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ statusText: "Internal Server Error",
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("error"),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/contacts/:id")!;
+ const req = makeReq({ params: { id: "c1" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(502);
+ });
+});
+
+// ── POST /contacts/:id/enrich ─────────────────────────────────────────────────
+
+describe("POST /contacts/:id/enrich", () => {
+ async function callEnrich(
+ api: MockAPI,
+ params: Record,
+ body: Record,
+ ) {
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "POST", "/contacts/:id/enrich")!;
+ const req = makeReq({ params, body });
+ const res = makeRes();
+ await ep.handler(req, res as unknown as EndpointResponse);
+ return res;
+ }
+
+ it("generates summary, creates note, stores enrichment record", async () => {
+ const api = createMockAPI();
+ const makeReqMock = api.makeRequest as jest.Mock;
+
+ // Call 1: AI summary
+ makeReqMock.mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({
+ choices: [
+ { message: { content: "Contact is interested in Pro plan." } },
+ ],
+ }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ // Call 2: create note
+ makeReqMock.mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({
+ id: "n1",
+ properties: {
+ hs_note_body: "Contact is interested in Pro plan.",
+ hs_timestamp: "",
+ },
+ }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ // Call 3: associate note
+ makeReqMock.mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ results: [] }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+
+ const res = await callEnrich(
+ api,
+ { id: "c1" },
+ {
+ conversationText: "User asked about pricing.",
+ conversationId: "conv-1",
+ },
+ );
+
+ expect(res._status).toBe(200);
+ expect(res._body).toMatchObject({
+ summary: "Contact is interested in Pro plan.",
+ noteId: "n1",
+ });
+ expect(api.db.set).toHaveBeenCalledWith(
+ buildEnrichmentKey("c1"),
+ expect.objectContaining({ contactId: "c1", summary: expect.any(String) }),
+ );
+ expect(api.db.set).toHaveBeenCalledWith(
+ buildContactKey("c1", "conv-1"),
+ expect.objectContaining({
+ hubspotContactId: "c1",
+ conversationId: "conv-1",
+ }),
+ );
+ });
+
+ it("stores enrichment even without conversationId", async () => {
+ const api = createMockAPI();
+ const makeReqMock = api.makeRequest as jest.Mock;
+ makeReqMock
+ .mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({
+ choices: [{ message: { content: "A summary." } }],
+ }),
+ text: jest.fn().mockResolvedValue(""),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ id: "n2", properties: {} }),
+ text: jest.fn().mockResolvedValue(""),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ results: [] }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+
+ const res = await callEnrich(
+ api,
+ { id: "c2" },
+ { conversationText: "text" },
+ );
+
+ expect(res._status).toBe(200);
+ // enrichment record stored, but no contact-interaction record
+ expect(api.db.set).toHaveBeenCalledWith(
+ buildEnrichmentKey("c2"),
+ expect.objectContaining({ contactId: "c2" }),
+ );
+ const setCallKeys = (api.db.set as jest.Mock).mock.calls.map(
+ ([k]: [string]) => k,
+ );
+ expect(setCallKeys.some((k: string) => k.startsWith("contact:"))).toBe(
+ false,
+ );
+ });
+
+ it("returns 400 when API key not configured", async () => {
+ const api = createMockAPI({ hubspotApiKey: "" });
+ const res = await callEnrich(
+ api,
+ { id: "c1" },
+ { conversationText: "text" },
+ );
+ expect(res._status).toBe(400);
+ });
+
+ it("returns 400 when conversationText is missing", async () => {
+ const api = createMockAPI();
+ const res = await callEnrich(api, { id: "c1" }, {});
+ expect(res._status).toBe(400);
+ expect(res._body).toMatchObject({
+ error: expect.stringContaining("required"),
+ });
+ });
+
+ it("returns 502 on AI service error", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("error"),
+ });
+ const res = await callEnrich(
+ api,
+ { id: "c1" },
+ { conversationText: "text" },
+ );
+ expect(res._status).toBe(502);
+ });
+
+ it("returns 502 on HubSpot note creation error", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock)
+ .mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({
+ choices: [{ message: { content: "summary" } }],
+ }),
+ text: jest.fn().mockResolvedValue(""),
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 403,
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("Forbidden"),
+ });
+
+ const res = await callEnrich(
+ api,
+ { id: "c1" },
+ { conversationText: "text" },
+ );
+ expect(res._status).toBe(502);
+ });
+});
+
+// ── GET /deals ────────────────────────────────────────────────────────────────
+
+describe("GET /deals", () => {
+ const deals: HubSpotDeal[] = [
+ {
+ id: "d1",
+ properties: {
+ dealname: "Enterprise",
+ pipeline: "default",
+ dealstage: "closedwon",
+ },
+ },
+ {
+ id: "d2",
+ properties: {
+ dealname: "Starter",
+ pipeline: "pipe-2",
+ dealstage: "appointmentscheduled",
+ },
+ },
+ ];
+
+ it("returns all deals when no pipeline filter", async () => {
+ const api = createMockAPI({ pipelineId: "" });
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ results: deals }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/deals")!;
+ const res = makeRes();
+
+ await ep.handler(makeReq(), res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(200);
+ expect((res._body as { deals: unknown[] }).deals).toHaveLength(2);
+ });
+
+ it("filters by pipelineId in query param", async () => {
+ const api = createMockAPI({ pipelineId: "" });
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ results: deals }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/deals")!;
+ const req = makeReq({ query: { pipelineId: "pipe-2" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect((res._body as { deals: HubSpotDeal[] }).deals).toHaveLength(1);
+ expect((res._body as { deals: HubSpotDeal[] }).deals[0].id).toBe("d2");
+ });
+
+ it("uses pipelineId from plugin config when not in query", async () => {
+ const api = createMockAPI({ pipelineId: "default" });
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ results: deals }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/deals")!;
+ const res = makeRes();
+
+ await ep.handler(makeReq(), res as unknown as EndpointResponse);
+
+ expect((res._body as { deals: HubSpotDeal[] }).deals).toHaveLength(1);
+ expect((res._body as { deals: HubSpotDeal[] }).deals[0].id).toBe("d1");
+ });
+
+ it("returns 400 when API key not configured", async () => {
+ const api = createMockAPI({ hubspotApiKey: "" });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/deals")!;
+ const res = makeRes();
+
+ await ep.handler(makeReq(), res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(400);
+ });
+
+ it("returns 502 on HubSpot error", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 502,
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("Bad Gateway"),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "GET", "/deals")!;
+ const res = makeRes();
+
+ await ep.handler(makeReq(), res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(502);
+ });
+});
+
+// ── POST /deals/:id/update-stage ──────────────────────────────────────────────
+
+describe("POST /deals/:id/update-stage", () => {
+ it("updates the deal stage and returns deal", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({
+ id: "d1",
+ properties: { dealstage: "closedwon", pipeline: "default" },
+ }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "POST", "/deals/:id/update-stage")!;
+ const req = makeReq({ params: { id: "d1" }, body: { stage: "closedwon" } });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(200);
+ expect((res._body as { deal: HubSpotDeal }).deal.properties.dealstage).toBe(
+ "closedwon",
+ );
+ });
+
+ it("returns 400 when API key not configured", async () => {
+ const api = createMockAPI({ hubspotApiKey: "" });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "POST", "/deals/:id/update-stage")!;
+ const res = makeRes();
+
+ await ep.handler(
+ makeReq({ params: { id: "d1" }, body: { stage: "closedlost" } }),
+ res as unknown as EndpointResponse,
+ );
+
+ expect(res._status).toBe(400);
+ });
+
+ it("returns 400 when stage is missing", async () => {
+ const api = createMockAPI();
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "POST", "/deals/:id/update-stage")!;
+ const req = makeReq({ params: { id: "d1" }, body: {} });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(400);
+ expect(res._body).toMatchObject({
+ error: expect.stringContaining("required"),
+ });
+ });
+
+ it("returns 502 on HubSpot error", async () => {
+ const api = createMockAPI();
+ (api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 404,
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("Not Found"),
+ });
+ const endpoints = await initPlugin(api);
+ const ep = getEp(endpoints, "POST", "/deals/:id/update-stage")!;
+ const req = makeReq({
+ params: { id: "missing" },
+ body: { stage: "closedwon" },
+ });
+ const res = makeRes();
+
+ await ep.handler(req, res as unknown as EndpointResponse);
+
+ expect(res._status).toBe(502);
+ });
+});
+
+// ── conversation:end hook ─────────────────────────────────────────────────────
+
+describe("conversation:end hook", () => {
+ async function fireHook(ctx: MockCtx): Promise {
+ const hook = plugin.definition.hooks?.["conversation:end"];
+ await hook?.(ctx);
+ }
+
+ it("skips everything when autoLogConversations is false", async () => {
+ const ctx = makeCtx({ userId: "u1" }, { autoLogConversations: false });
+ (ctx as unknown as Record)["conversationId"] = "conv-1";
+
+ await fireHook(ctx);
+
+ expect(ctx.api.db.set).not.toHaveBeenCalled();
+ expect(ctx.api.makeRequest).not.toHaveBeenCalled();
+ });
+
+ it("skips when conversationId is not on context", async () => {
+ const ctx = makeCtx({ userId: "u1" });
+ // no conversationId set
+
+ await fireHook(ctx);
+
+ expect(ctx.api.db.set).not.toHaveBeenCalled();
+ });
+
+ it("stores a pending record when autoLogConversations is true", async () => {
+ const ctx = makeCtx({ userId: "u1" });
+ (ctx as unknown as Record)["conversationId"] = "conv-2";
+
+ await fireHook(ctx);
+
+ expect(ctx.api.db.set).toHaveBeenCalledWith(
+ buildPendingKey("conv-2"),
+ expect.objectContaining({ conversationId: "conv-2", userId: "u1" }),
+ );
+ });
+
+ it("only stores pending when API key is not configured", async () => {
+ const ctx = makeCtx({ userId: "u1" }, { hubspotApiKey: "" });
+ (ctx as unknown as Record)["conversationId"] = "conv-3";
+
+ await fireHook(ctx);
+
+ expect(ctx.api.db.set).toHaveBeenCalledTimes(1);
+ expect(ctx.api.makeRequest).not.toHaveBeenCalled();
+ });
+
+ it("creates a HubSpot note when API key and contact association exist", async () => {
+ const ctx = makeCtx({ userId: "u1" }, { hubspotApiKey: "pat-123" });
+ (ctx as unknown as Record)["conversationId"] = "conv-4";
+
+ // Pre-seed contact association
+ const db = ctx.api.db as PluginDatabaseAPI & {
+ _store?: Map;
+ };
+ await db.set(buildAssociationKey("conv-4"), { hubspotContactId: "hs-c1" });
+
+ // makeRequest: set call happened above; now mock the two HubSpot calls
+ const makeReqMock = ctx.api.makeRequest as jest.Mock;
+ makeReqMock
+ .mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ id: "note-99", properties: {} }),
+ text: jest.fn().mockResolvedValue(""),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ results: [] }),
+ text: jest.fn().mockResolvedValue(""),
+ });
+
+ await fireHook(ctx);
+
+ // pending record + enrichment interaction record
+ expect(ctx.api.db.set).toHaveBeenCalledWith(
+ buildPendingKey("conv-4"),
+ expect.objectContaining({ conversationId: "conv-4" }),
+ );
+ expect(ctx.api.db.set).toHaveBeenCalledWith(
+ buildContactKey("hs-c1", "conv-4"),
+ expect.objectContaining({ hubspotContactId: "hs-c1", noteId: "note-99" }),
+ );
+ });
+
+ it("silently ignores HubSpot errors during note creation", async () => {
+ const ctx = makeCtx({ userId: "u1" }, { hubspotApiKey: "pat-123" });
+ (ctx as unknown as Record)["conversationId"] = "conv-5";
+ await ctx.api.db.set(buildAssociationKey("conv-5"), {
+ hubspotContactId: "hs-c2",
+ });
+
+ (ctx.api.makeRequest as jest.Mock).mockResolvedValueOnce({
+ ok: false,
+ status: 503,
+ json: jest.fn().mockResolvedValue({}),
+ text: jest.fn().mockResolvedValue("Service Unavailable"),
+ });
+
+ // Should not throw
+ await expect(fireHook(ctx)).resolves.toBeUndefined();
+ // pending record still stored
+ expect(ctx.api.db.set).toHaveBeenCalledWith(
+ buildPendingKey("conv-5"),
+ expect.anything(),
+ );
+ });
+});
diff --git a/packages/plugins/official/hubspot-crm/__tests__/tsconfig.json b/packages/plugins/official/hubspot-crm/__tests__/tsconfig.json
new file mode 100644
index 0000000..a05feed
--- /dev/null
+++ b/packages/plugins/official/hubspot-crm/__tests__/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "../..",
+ "baseUrl": "..",
+ "lib": ["ES2022", "DOM"],
+ "types": ["jest", "node"],
+ "paths": {
+ "@agentbase/plugin-sdk": ["../../../sdk/src/index.ts"]
+ }
+ },
+ "include": ["../src/**/*", "./**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/plugins/official/hubspot-crm/manifest.json b/packages/plugins/official/hubspot-crm/manifest.json
new file mode 100644
index 0000000..fa8a640
--- /dev/null
+++ b/packages/plugins/official/hubspot-crm/manifest.json
@@ -0,0 +1,53 @@
+{
+ "name": "hubspot-crm",
+ "version": "1.0.0",
+ "description": "Enrich HubSpot CRM contacts with AI conversation summaries. Auto-log interactions, update deal stages, and generate lead scores from chat data.",
+ "author": "Agentbase",
+ "license": "GPL-3.0",
+ "main": "dist/index.js",
+ "agentbase": {
+ "type": "plugin",
+ "apiVersion": "1"
+ },
+ "hooks": ["app:init", "conversation:end"],
+ "endpoints": [
+ { "method": "POST", "path": "/connect" },
+ { "method": "GET", "path": "/contacts" },
+ { "method": "GET", "path": "/contacts/:id" },
+ { "method": "POST", "path": "/contacts/:id/enrich" },
+ { "method": "GET", "path": "/deals" },
+ { "method": "POST", "path": "/deals/:id/update-stage" }
+ ],
+ "settings": [
+ {
+ "key": "hubspotApiKey",
+ "type": "string",
+ "label": "HubSpot Private App Token",
+ "encrypted": true
+ },
+ {
+ "key": "autoLogConversations",
+ "type": "boolean",
+ "label": "Automatically log conversations to HubSpot contact timelines",
+ "default": true
+ },
+ {
+ "key": "enrichModel",
+ "type": "select",
+ "label": "AI Model for Contact Enrichment",
+ "options": [
+ "gpt-4o-mini",
+ "gpt-4o",
+ "claude-3-5-haiku",
+ "claude-3-5-sonnet"
+ ],
+ "default": "gpt-4o-mini"
+ },
+ {
+ "key": "pipelineId",
+ "type": "string",
+ "label": "HubSpot Pipeline ID (optional — used to filter deals)"
+ }
+ ],
+ "permissions": ["network:external", "db:readwrite"]
+}
diff --git a/packages/plugins/official/hubspot-crm/package.json b/packages/plugins/official/hubspot-crm/package.json
new file mode 100644
index 0000000..4008583
--- /dev/null
+++ b/packages/plugins/official/hubspot-crm/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@agentbase/plugin-hubspot-crm",
+ "version": "1.0.0",
+ "description": "Enrich HubSpot CRM contacts with AI conversation summaries. Auto-log interactions, update deal stages, and generate lead scores from chat data.",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "test": "jest",
+ "lint": "eslint src --ext .ts"
+ },
+ "dependencies": {
+ "@agentbase/plugin-sdk": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.0",
+ "@types/node": "^20.0.0",
+ "jest": "^29.5.0",
+ "ts-jest": "^29.1.0",
+ "typescript": "^5.0.0"
+ },
+ "jest": {
+ "preset": "ts-jest",
+ "testEnvironment": "node",
+ "roots": [
+ "/__tests__"
+ ],
+ "globals": {
+ "ts-jest": {
+ "tsconfig": "__tests__/tsconfig.json"
+ }
+ }
+ },
+ "license": "GPL-3.0"
+}
diff --git a/packages/plugins/official/hubspot-crm/src/index.ts b/packages/plugins/official/hubspot-crm/src/index.ts
new file mode 100644
index 0000000..985f9c7
--- /dev/null
+++ b/packages/plugins/official/hubspot-crm/src/index.ts
@@ -0,0 +1,680 @@
+/**
+ * HubSpot CRM
+ *
+ * Enrich HubSpot CRM contacts with AI conversation summaries. Auto-log
+ * interactions, update deal stages, and generate lead scores from chat data.
+ *
+ * All external HubSpot requests use `makeRequest` with Bearer-token auth
+ * (Private App Token). The AI enrichment summary is generated via the
+ * platform's internal AI service.
+ *
+ * Plugin DB keys:
+ * - `connection:config` — stored HubSpot API key + metadata
+ * - `contact:{hubspotId}:{conversationId}` — logged interaction record
+ * - `enrichment:{contactId}` — latest AI enrichment record
+ * - `pending:{conversationId}` — queued conversation awaiting link
+ * - `association:{conversationId}` — maps conversationId → hubspotContactId
+ *
+ * @package @agentbase/plugin-hubspot-crm
+ * @version 1.0.0
+ */
+import { createPlugin, PluginContext } from "@agentbase/plugin-sdk";
+
+// ── Constants ─────────────────────────────────────────────────────────────────
+
+export const HUBSPOT_API_BASE = "https://api.hubapi.com";
+export const AI_COMPLETIONS_PATH = "/api/v1/internal/ai/completions";
+export const CONNECTION_KEY = "connection:config";
+export const DEFAULT_ENRICH_MODEL = "gpt-4o-mini";
+
+// ── Types ─────────────────────────────────────────────────────────────────────
+
+export interface HubSpotContactProperties {
+ firstname?: string;
+ lastname?: string;
+ email?: string;
+ company?: string;
+ phone?: string;
+}
+
+export interface HubSpotContact {
+ id: string;
+ properties: HubSpotContactProperties;
+}
+
+export interface HubSpotDealProperties {
+ dealname?: string;
+ dealstage?: string;
+ pipeline?: string;
+ amount?: string;
+ closedate?: string;
+}
+
+export interface HubSpotDeal {
+ id: string;
+ properties: HubSpotDealProperties;
+}
+
+export interface HubSpotNote {
+ id: string;
+ properties: {
+ hs_note_body: string;
+ hs_timestamp: string;
+ };
+}
+
+export interface HubSpotListResult {
+ results: T[];
+ paging?: {
+ next?: { after: string };
+ };
+}
+
+export interface HubSpotSearchResult {
+ results: T[];
+ total: number;
+}
+
+export interface ContactInteractionRecord {
+ hubspotContactId: string;
+ conversationId: string;
+ userId: string;
+ noteId?: string;
+ loggedAt: number;
+}
+
+export interface EnrichmentRecord {
+ contactId: string;
+ summary: string;
+ model: string;
+ generatedAt: number;
+}
+
+export interface PendingInteractionRecord {
+ conversationId: string;
+ userId: string;
+ queuedAt: number;
+}
+
+export interface ConnectionConfig {
+ apiKey: string;
+ connectedAt: number;
+}
+
+// ── DB Key Helpers ────────────────────────────────────────────────────────────
+
+export function buildContactKey(
+ hubspotId: string,
+ conversationId: string,
+): string {
+ return `contact:${hubspotId}:${conversationId}`;
+}
+
+export function buildEnrichmentKey(contactId: string): string {
+ return `enrichment:${contactId}`;
+}
+
+export function buildPendingKey(conversationId: string): string {
+ return `pending:${conversationId}`;
+}
+
+export function buildAssociationKey(conversationId: string): string {
+ return `association:${conversationId}`;
+}
+
+// ── HubSpot API Helpers ───────────────────────────────────────────────────────
+
+/**
+ * Generic HubSpot REST API request with Bearer-token (Private App) auth.
+ * Throws on non-2xx responses.
+ */
+export async function hubspotRequest(
+ makeRequest: (url: string, opts?: RequestInit) => Promise,
+ apiKey: string,
+ method: string,
+ path: string,
+ body?: unknown,
+): Promise {
+ const init: RequestInit = {
+ method,
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ };
+
+ if (body !== undefined) {
+ init.body = JSON.stringify(body);
+ }
+
+ const response = await makeRequest(`${HUBSPOT_API_BASE}${path}`, init);
+
+ if (!response.ok) {
+ const text = await response.text().catch(() => "");
+ throw new Error(
+ `HubSpot API error ${response.status}: ${text || response.statusText}`,
+ );
+ }
+
+ return response.json() as Promise;
+}
+
+/**
+ * Search HubSpot contacts by a query string using the v3 search API.
+ */
+export async function searchContacts(
+ makeRequest: (url: string, opts?: RequestInit) => Promise,
+ apiKey: string,
+ query: string,
+ limit = 20,
+): Promise {
+ const result = await hubspotRequest>(
+ makeRequest,
+ apiKey,
+ "POST",
+ "/crm/v3/objects/contacts/search",
+ {
+ query,
+ limit,
+ properties: ["firstname", "lastname", "email", "company", "phone"],
+ },
+ );
+ return result.results;
+}
+
+/**
+ * Get a single HubSpot contact by ID with standard properties.
+ */
+export async function getContact(
+ makeRequest: (url: string, opts?: RequestInit) => Promise,
+ apiKey: string,
+ contactId: string,
+): Promise {
+ return hubspotRequest(
+ makeRequest,
+ apiKey,
+ "GET",
+ `/crm/v3/objects/contacts/${contactId}?properties=firstname,lastname,email,company,phone`,
+ );
+}
+
+/**
+ * List HubSpot deals, optionally filtered client-side by pipeline ID.
+ */
+export async function getDeals(
+ makeRequest: (url: string, opts?: RequestInit) => Promise,
+ apiKey: string,
+ pipelineId?: string,
+ limit = 20,
+): Promise {
+ const params = new URLSearchParams({
+ limit: String(limit),
+ properties: "dealname,dealstage,pipeline,amount,closedate",
+ });
+
+ const result = await hubspotRequest>(
+ makeRequest,
+ apiKey,
+ "GET",
+ `/crm/v3/objects/deals?${params.toString()}`,
+ );
+
+ if (pipelineId) {
+ return result.results.filter((d) => d.properties.pipeline === pipelineId);
+ }
+
+ return result.results;
+}
+
+/**
+ * Create a Note engagement object in HubSpot.
+ */
+export async function createNote(
+ makeRequest: (url: string, opts?: RequestInit) => Promise,
+ apiKey: string,
+ noteBody: string,
+): Promise {
+ return hubspotRequest(
+ makeRequest,
+ apiKey,
+ "POST",
+ "/crm/v3/objects/notes",
+ {
+ properties: {
+ hs_note_body: noteBody,
+ hs_timestamp: new Date().toISOString(),
+ },
+ },
+ );
+}
+
+/**
+ * Associate a Note with a Contact via the v3 associations batch API.
+ */
+export async function associateNoteWithContact(
+ makeRequest: (url: string, opts?: RequestInit) => Promise,
+ apiKey: string,
+ noteId: string,
+ contactId: string,
+): Promise {
+ await hubspotRequest(
+ makeRequest,
+ apiKey,
+ "POST",
+ "/crm/v3/associations/notes/contacts/batch/create",
+ {
+ inputs: [
+ {
+ from: { id: noteId },
+ to: { id: contactId },
+ type: "note_to_contact",
+ },
+ ],
+ },
+ );
+}
+
+/**
+ * Update the stage of a HubSpot deal via PATCH.
+ */
+export async function updateDealStage(
+ makeRequest: (url: string, opts?: RequestInit) => Promise,
+ apiKey: string,
+ dealId: string,
+ stage: string,
+): Promise {
+ return hubspotRequest(
+ makeRequest,
+ apiKey,
+ "PATCH",
+ `/crm/v3/objects/deals/${dealId}`,
+ { properties: { dealstage: stage } },
+ );
+}
+
+// ── AI Enrichment ─────────────────────────────────────────────────────────────
+
+/**
+ * Generate a one-paragraph CRM enrichment summary from conversation text
+ * using the platform's internal AI service.
+ */
+export async function generateEnrichmentSummary(
+ makeRequest: (url: string, opts?: RequestInit) => Promise,
+ conversationText: string,
+ model: string = DEFAULT_ENRICH_MODEL,
+): Promise {
+ const prompt = `You are a CRM assistant. Based on the following AI conversation, write a concise one-paragraph summary suitable for a HubSpot contact note. Focus on the user's intent, key information shared, and any required follow-up actions.\n\nConversation:\n${conversationText}`;
+
+ const response = await makeRequest(AI_COMPLETIONS_PATH, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ model,
+ messages: [{ role: "user", content: prompt }],
+ temperature: 0.3,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`AI service error: ${response.status}`);
+ }
+
+ const data = (await response.json()) as Record;
+
+ const choices = data["choices"] as
+ | Array<{ message: { content: string } }>
+ | undefined;
+ if (choices?.[0]?.message?.content) return choices[0].message.content;
+ if (typeof data["content"] === "string") return data["content"] as string;
+
+ throw new Error("Unexpected AI response shape");
+}
+
+// ── Plugin ────────────────────────────────────────────────────────────────────
+
+export default createPlugin({
+ name: "hubspot-crm",
+ version: "1.0.0",
+ description:
+ "Enrich HubSpot CRM contacts with AI conversation summaries. Auto-log interactions, update deal stages, and generate lead scores from chat data.",
+ permissions: ["network:external", "db:readwrite"],
+
+ settings: {
+ hubspotApiKey: {
+ type: "string",
+ label: "HubSpot Private App Token",
+ encrypted: true,
+ },
+ autoLogConversations: {
+ type: "boolean",
+ label: "Automatically log conversations to HubSpot contact timelines",
+ default: true,
+ },
+ enrichModel: {
+ type: "select",
+ label: "AI Model for Contact Enrichment",
+ default: DEFAULT_ENRICH_MODEL,
+ options: [
+ "gpt-4o-mini",
+ "gpt-4o",
+ "claude-3-5-haiku",
+ "claude-3-5-sonnet",
+ ],
+ },
+ pipelineId: {
+ type: "string",
+ label: "HubSpot Pipeline ID (optional — used to filter deals)",
+ },
+ },
+
+ hooks: {
+ // ── app:init ─────────────────────────────────────────────────────────────
+ "app:init": async (context: PluginContext) => {
+ const { api } = context;
+
+ // ── POST /connect ───────────────────────────────────────────────────────
+ api.registerEndpoint({
+ method: "POST",
+ path: "/connect",
+ auth: true,
+ description: "Validate and store HubSpot Private App Token",
+ handler: async (req, res) => {
+ const { apiKey } = req.body as { apiKey?: string };
+
+ if (!apiKey) {
+ res.status(400).json({ error: "apiKey is required" });
+ return;
+ }
+
+ // Validate by fetching one contact — cheap read operation
+ try {
+ await hubspotRequest(
+ api.makeRequest,
+ apiKey,
+ "GET",
+ "/crm/v3/objects/contacts?limit=1",
+ );
+ } catch (err) {
+ res.status(400).json({
+ error: `Invalid API key: ${(err as Error).message}`,
+ });
+ return;
+ }
+
+ const config: ConnectionConfig = { apiKey, connectedAt: Date.now() };
+ await api.db.set(CONNECTION_KEY, config);
+ res.status(200).json({ connected: true });
+ },
+ });
+
+ // ── GET /contacts ───────────────────────────────────────────────────────
+ api.registerEndpoint({
+ method: "GET",
+ path: "/contacts",
+ auth: true,
+ description: "Search HubSpot contacts by query string",
+ handler: async (req, res) => {
+ const apiKey = (api.getConfig("hubspotApiKey") as string) ?? "";
+ if (!apiKey) {
+ res.status(400).json({ error: "HubSpot API key not configured" });
+ return;
+ }
+
+ const q = req.query["q"] ?? "";
+ const limit = Math.min(
+ parseInt(req.query["limit"] ?? "20", 10) || 20,
+ 100,
+ );
+
+ try {
+ const contacts = await searchContacts(
+ api.makeRequest,
+ apiKey,
+ q,
+ limit,
+ );
+ res.status(200).json({ contacts });
+ } catch (err) {
+ res.status(502).json({ error: (err as Error).message });
+ }
+ },
+ });
+
+ // ── GET /contacts/:id ───────────────────────────────────────────────────
+ api.registerEndpoint({
+ method: "GET",
+ path: "/contacts/:id",
+ auth: true,
+ description: "Get a single HubSpot contact by ID",
+ handler: async (req, res) => {
+ const apiKey = (api.getConfig("hubspotApiKey") as string) ?? "";
+ if (!apiKey) {
+ res.status(400).json({ error: "HubSpot API key not configured" });
+ return;
+ }
+
+ const { id } = req.params;
+
+ try {
+ const contact = await getContact(api.makeRequest, apiKey, id);
+ res.status(200).json({ contact });
+ } catch (err) {
+ const msg = (err as Error).message;
+ if (msg.includes("404")) {
+ res.status(404).json({ error: "Contact not found" });
+ } else {
+ res.status(502).json({ error: msg });
+ }
+ }
+ },
+ });
+
+ // ── POST /contacts/:id/enrich ───────────────────────────────────────────
+ api.registerEndpoint({
+ method: "POST",
+ path: "/contacts/:id/enrich",
+ auth: true,
+ description:
+ "Generate an AI summary from conversation text and add it as a HubSpot note",
+ handler: async (req, res) => {
+ const apiKey = (api.getConfig("hubspotApiKey") as string) ?? "";
+ if (!apiKey) {
+ res.status(400).json({ error: "HubSpot API key not configured" });
+ return;
+ }
+
+ const { id: contactId } = req.params;
+ const { conversationText, conversationId } = req.body as {
+ conversationText?: string;
+ conversationId?: string;
+ };
+
+ if (!conversationText) {
+ res.status(400).json({ error: "conversationText is required" });
+ return;
+ }
+
+ const model =
+ (api.getConfig("enrichModel") as string) ?? DEFAULT_ENRICH_MODEL;
+
+ try {
+ const summary = await generateEnrichmentSummary(
+ api.makeRequest,
+ conversationText,
+ model,
+ );
+
+ const note = await createNote(api.makeRequest, apiKey, summary);
+ await associateNoteWithContact(
+ api.makeRequest,
+ apiKey,
+ note.id,
+ contactId,
+ );
+
+ const enrichmentRecord: EnrichmentRecord = {
+ contactId,
+ summary,
+ model,
+ generatedAt: Date.now(),
+ };
+ await api.db.set(buildEnrichmentKey(contactId), enrichmentRecord);
+
+ if (conversationId) {
+ const interaction: ContactInteractionRecord = {
+ hubspotContactId: contactId,
+ conversationId,
+ userId: "",
+ noteId: note.id,
+ loggedAt: Date.now(),
+ };
+ await api.db.set(
+ buildContactKey(contactId, conversationId),
+ interaction,
+ );
+ }
+
+ res.status(200).json({ summary, noteId: note.id });
+ } catch (err) {
+ res.status(502).json({ error: (err as Error).message });
+ }
+ },
+ });
+
+ // ── GET /deals ──────────────────────────────────────────────────────────
+ api.registerEndpoint({
+ method: "GET",
+ path: "/deals",
+ auth: true,
+ description: "List HubSpot deals, optionally filtered by pipeline",
+ handler: async (req, res) => {
+ const apiKey = (api.getConfig("hubspotApiKey") as string) ?? "";
+ if (!apiKey) {
+ res.status(400).json({ error: "HubSpot API key not configured" });
+ return;
+ }
+
+ const limit = Math.min(
+ parseInt(req.query["limit"] ?? "20", 10) || 20,
+ 100,
+ );
+ const pipelineId =
+ req.query["pipelineId"] ||
+ (api.getConfig("pipelineId") as string) ||
+ undefined;
+
+ try {
+ const deals = await getDeals(
+ api.makeRequest,
+ apiKey,
+ pipelineId,
+ limit,
+ );
+ res.status(200).json({ deals });
+ } catch (err) {
+ res.status(502).json({ error: (err as Error).message });
+ }
+ },
+ });
+
+ // ── POST /deals/:id/update-stage ────────────────────────────────────────
+ api.registerEndpoint({
+ method: "POST",
+ path: "/deals/:id/update-stage",
+ auth: true,
+ description: "Update the stage of a HubSpot deal",
+ handler: async (req, res) => {
+ const apiKey = (api.getConfig("hubspotApiKey") as string) ?? "";
+ if (!apiKey) {
+ res.status(400).json({ error: "HubSpot API key not configured" });
+ return;
+ }
+
+ const { id: dealId } = req.params;
+ const { stage } = req.body as { stage?: string };
+
+ if (!stage) {
+ res.status(400).json({ error: "stage is required" });
+ return;
+ }
+
+ try {
+ const deal = await updateDealStage(
+ api.makeRequest,
+ apiKey,
+ dealId,
+ stage,
+ );
+ res.status(200).json({ deal });
+ } catch (err) {
+ res.status(502).json({ error: (err as Error).message });
+ }
+ },
+ });
+ },
+
+ // ── conversation:end ──────────────────────────────────────────────────────
+ "conversation:end": async (context: PluginContext) => {
+ const autoLog =
+ (context.api.getConfig("autoLogConversations") as boolean) ?? true;
+ if (!autoLog) return;
+
+ const conversationId = (context as unknown as Record)[
+ "conversationId"
+ ] as string | undefined;
+ if (!conversationId) return;
+
+ // Always queue a pending record for later manual linking
+ const pending: PendingInteractionRecord = {
+ conversationId,
+ userId: context.userId,
+ queuedAt: Date.now(),
+ };
+ await context.api.db.set(buildPendingKey(conversationId), pending);
+
+ // If API key is configured, check for a stored contact association
+ const apiKey = (context.api.getConfig("hubspotApiKey") as string) ?? "";
+ if (!apiKey) return;
+
+ const association = (await context.api.db.get(
+ buildAssociationKey(conversationId),
+ )) as { hubspotContactId: string } | null;
+ if (!association?.hubspotContactId) return;
+
+ try {
+ const noteBody = [
+ `Conversation logged by Agentbase`,
+ `Conversation ID: ${conversationId}`,
+ `User ID: ${context.userId}`,
+ `Timestamp: ${new Date().toISOString()}`,
+ ].join("\n");
+
+ const note = await createNote(
+ context.api.makeRequest,
+ apiKey,
+ noteBody,
+ );
+ await associateNoteWithContact(
+ context.api.makeRequest,
+ apiKey,
+ note.id,
+ association.hubspotContactId,
+ );
+
+ const record: ContactInteractionRecord = {
+ hubspotContactId: association.hubspotContactId,
+ conversationId,
+ userId: context.userId,
+ noteId: note.id,
+ loggedAt: Date.now(),
+ };
+ await context.api.db.set(
+ buildContactKey(association.hubspotContactId, conversationId),
+ record,
+ );
+ } catch {
+ // Silent failure — logging must not interrupt the conversation flow
+ }
+ },
+ },
+});
diff --git a/packages/plugins/official/hubspot-crm/tsconfig.json b/packages/plugins/official/hubspot-crm/tsconfig.json
new file mode 100644
index 0000000..5628f37
--- /dev/null
+++ b/packages/plugins/official/hubspot-crm/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "commonjs",
+ "lib": ["ES2022", "DOM"],
+ "outDir": "./dist",
+ "rootDir": "../..",
+ "baseUrl": ".",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "ignoreDeprecations": "6.0",
+ "types": ["node"],
+ "paths": {
+ "@agentbase/plugin-sdk": ["../../sdk/src/index.ts"]
+ }
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "__tests__"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0104670..6cdd37f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -269,7 +269,7 @@ importers:
version: 10.4.24(postcss@8.5.6)
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@22.19.11)
+ version: 29.7.0(@types/node@22.19.11)(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
jest-environment-jsdom:
specifier: ^30.2.0
version: 30.2.0
@@ -284,7 +284,7 @@ importers:
version: 1.0.7(tailwindcss@3.4.19)
ts-jest:
specifier: ^29.4.6
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@22.19.11))(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@22.19.11)(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -373,10 +373,10 @@ importers:
version: 29.5.14
jest:
specifier: ^29.7.0
- version: 29.7.0
+ version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0)(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -392,10 +392,10 @@ importers:
version: 29.5.14
jest:
specifier: ^29.7.0
- version: 29.7.0
+ version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0)(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -414,10 +414,10 @@ importers:
version: 25.5.2
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@25.5.2)
+ version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2))(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -436,14 +436,36 @@ importers:
version: 25.5.2
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@25.5.2)
+ version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2))(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
+ packages/plugins/official/hubspot-crm:
+ dependencies:
+ '@agentbase/plugin-sdk':
+ specifier: workspace:*
+ version: link:../..
+ devDependencies:
+ '@types/jest':
+ specifier: ^29.5.0
+ version: 29.5.14
+ '@types/node':
+ specifier: ^20.0.0
+ version: 20.19.33
+ jest:
+ specifier: ^29.5.0
+ version: 29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
+ ts-jest:
+ specifier: ^29.1.0
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3)
+ typescript:
+ specifier: ^5.0.0
+ version: 5.9.3
+
packages/plugins/official/openrouter-gateway:
dependencies:
'@agentbase/plugin-sdk':
@@ -458,10 +480,10 @@ importers:
version: 20.19.33
jest:
specifier: ^29.5.0
- version: 29.7.0(@types/node@20.19.33)
+ version: 29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
ts-jest:
specifier: ^29.1.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@20.19.33))(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.0.0
version: 5.9.3
@@ -480,10 +502,10 @@ importers:
version: 25.5.2
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@25.5.2)
+ version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2))(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -502,10 +524,10 @@ importers:
version: 20.19.33
jest:
specifier: ^29.5.0
- version: 29.7.0(@types/node@20.19.33)
+ version: 29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
ts-jest:
specifier: ^29.1.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@20.19.33))(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.0.0
version: 5.9.3
@@ -524,10 +546,10 @@ importers:
version: 25.5.2
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@25.5.2)
+ version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2))(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -543,10 +565,10 @@ importers:
version: 29.5.14
jest:
specifier: ^29.7.0
- version: 29.7.0
+ version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0)(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -564,10 +586,10 @@ importers:
version: 29.5.14
jest:
specifier: ^29.7.0
- version: 29.7.0
+ version: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.0
- version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0)(typescript@5.9.3)
+ version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.3.0
version: 5.9.3
@@ -6386,6 +6408,41 @@ snapshots:
jest-util: 29.7.0
slash: 3.0.0
+ '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))':
+ dependencies:
+ '@jest/console': 29.7.0
+ '@jest/reporters': 29.7.0
+ '@jest/test-result': 29.7.0
+ '@jest/transform': 29.7.0
+ '@jest/types': 29.6.3
+ '@types/node': 22.19.11
+ ansi-escapes: 4.3.2
+ chalk: 4.1.2
+ ci-info: 3.9.0
+ exit: 0.1.2
+ graceful-fs: 4.2.11
+ jest-changed-files: 29.7.0
+ jest-config: 29.7.0(@types/node@22.19.11)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
+ jest-haste-map: 29.7.0
+ jest-message-util: 29.7.0
+ jest-regex-util: 29.6.3
+ jest-resolve: 29.7.0
+ jest-resolve-dependencies: 29.7.0
+ jest-runner: 29.7.0
+ jest-runtime: 29.7.0
+ jest-snapshot: 29.7.0
+ jest-util: 29.7.0
+ jest-validate: 29.7.0
+ jest-watcher: 29.7.0
+ micromatch: 4.0.8
+ pretty-format: 29.7.0
+ slash: 3.0.0
+ strip-ansi: 6.0.1
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))':
dependencies:
'@jest/console': 29.7.0
@@ -8380,13 +8437,13 @@ snapshots:
optionalDependencies:
typescript: 5.7.2
- create-jest@29.7.0(@types/node@20.19.33):
+ create-jest@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
- jest-config: 29.7.0(@types/node@20.19.33)
+ jest-config: 29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@@ -9602,54 +9659,16 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-cli@29.7.0:
+ jest-cli@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)):
dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
+ '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
- create-jest: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
+ create-jest: 29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
exit: 0.1.2
import-local: 3.2.0
- jest-config: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
- jest-util: 29.7.0
- jest-validate: 29.7.0
- yargs: 17.7.2
- transitivePeerDependencies:
- - '@types/node'
- - babel-plugin-macros
- - supports-color
- - ts-node
-
- jest-cli@29.7.0(@types/node@20.19.33):
- dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
- '@jest/test-result': 29.7.0
- '@jest/types': 29.6.3
- chalk: 4.1.2
- create-jest: 29.7.0(@types/node@20.19.33)
- exit: 0.1.2
- import-local: 3.2.0
- jest-config: 29.7.0(@types/node@20.19.33)
- jest-util: 29.7.0
- jest-validate: 29.7.0
- yargs: 17.7.2
- transitivePeerDependencies:
- - '@types/node'
- - babel-plugin-macros
- - supports-color
- - ts-node
-
- jest-cli@29.7.0(@types/node@22.19.11):
- dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
- '@jest/test-result': 29.7.0
- '@jest/types': 29.6.3
- chalk: 4.1.2
- create-jest: 29.7.0(@types/node@22.19.11)(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
- exit: 0.1.2
- import-local: 3.2.0
- jest-config: 29.7.0(@types/node@22.19.11)(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
+ jest-config: 29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@@ -9678,9 +9697,9 @@ snapshots:
- supports-color
- ts-node
- jest-cli@29.7.0(@types/node@25.5.2):
+ jest-cli@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)):
dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
+ '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
@@ -9697,26 +9716,38 @@ snapshots:
- supports-color
- ts-node
- jest-cli@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)):
+ jest-config@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)):
dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
- '@jest/test-result': 29.7.0
+ '@babel/core': 7.29.0
+ '@jest/test-sequencer': 29.7.0
'@jest/types': 29.6.3
+ babel-jest: 29.7.0(@babel/core@7.29.0)
chalk: 4.1.2
- create-jest: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
- exit: 0.1.2
- import-local: 3.2.0
- jest-config: 29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
+ ci-info: 3.9.0
+ deepmerge: 4.3.1
+ glob: 7.2.3
+ graceful-fs: 4.2.11
+ jest-circus: 29.7.0
+ jest-environment-node: 29.7.0
+ jest-get-type: 29.6.3
+ jest-regex-util: 29.6.3
+ jest-resolve: 29.7.0
+ jest-runner: 29.7.0
jest-util: 29.7.0
jest-validate: 29.7.0
- yargs: 17.7.2
+ micromatch: 4.0.8
+ parse-json: 5.2.0
+ pretty-format: 29.7.0
+ slash: 3.0.0
+ strip-json-comments: 3.1.1
+ optionalDependencies:
+ '@types/node': 20.19.33
+ ts-node: 10.9.2(@types/node@20.19.33)(typescript@5.9.3)
transitivePeerDependencies:
- - '@types/node'
- babel-plugin-macros
- supports-color
- - ts-node
- jest-config@29.7.0(@types/node@20.19.33):
+ jest-config@29.7.0(@types/node@22.19.11)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.29.0
'@jest/test-sequencer': 29.7.0
@@ -9741,7 +9772,8 @@ snapshots:
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
- '@types/node': 20.19.33
+ '@types/node': 22.19.11
+ ts-node: 10.9.2(@types/node@20.19.33)(typescript@5.9.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@@ -10142,36 +10174,12 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
- jest@29.7.0:
+ jest@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)):
dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
- '@jest/types': 29.6.3
- import-local: 3.2.0
- jest-cli: 29.7.0
- transitivePeerDependencies:
- - '@types/node'
- - babel-plugin-macros
- - supports-color
- - ts-node
-
- jest@29.7.0(@types/node@20.19.33):
- dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
- '@jest/types': 29.6.3
- import-local: 3.2.0
- jest-cli: 29.7.0(@types/node@20.19.33)
- transitivePeerDependencies:
- - '@types/node'
- - babel-plugin-macros
- - supports-color
- - ts-node
-
- jest@29.7.0(@types/node@22.19.11):
- dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
+ '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
'@jest/types': 29.6.3
import-local: 3.2.0
- jest-cli: 29.7.0(@types/node@22.19.11)
+ jest-cli: 29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -10190,18 +10198,6 @@ snapshots:
- supports-color
- ts-node
- jest@29.7.0(@types/node@25.5.2):
- dependencies:
- '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3))
- '@jest/types': 29.6.3
- import-local: 3.2.0
- jest-cli: 29.7.0(@types/node@25.5.2)
- transitivePeerDependencies:
- - '@types/node'
- - babel-plugin-macros
- - supports-color
- - ts-node
-
jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3))
@@ -12279,12 +12275,12 @@ snapshots:
ts-interface-checker@0.1.13: {}
- ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@20.19.33))(typescript@5.9.3):
+ ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
handlebars: 4.7.8
- jest: 29.7.0(@types/node@20.19.33)
+ jest: 29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3))
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
@@ -12319,26 +12315,6 @@ snapshots:
babel-jest: 29.7.0(@babel/core@7.29.0)
jest-util: 30.3.0
- ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@22.19.11))(typescript@5.9.3):
- dependencies:
- bs-logger: 0.2.6
- fast-json-stable-stringify: 2.1.0
- handlebars: 4.7.8
- jest: 29.7.0(@types/node@22.19.11)
- json5: 2.2.3
- lodash.memoize: 4.1.2
- make-error: 1.3.6
- semver: 7.7.4
- type-fest: 4.41.0
- typescript: 5.9.3
- yargs-parser: 21.1.1
- optionalDependencies:
- '@babel/core': 7.29.0
- '@jest/transform': 29.7.0
- '@jest/types': 30.3.0
- babel-jest: 29.7.0(@babel/core@7.29.0)
- jest-util: 30.3.0
-
ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(typescript@5.9.3):
dependencies:
bs-logger: 0.2.6
@@ -12359,46 +12335,6 @@ snapshots:
babel-jest: 29.7.0(@babel/core@7.29.0)
jest-util: 30.3.0
- ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@25.5.2))(typescript@5.9.3):
- dependencies:
- bs-logger: 0.2.6
- fast-json-stable-stringify: 2.1.0
- handlebars: 4.7.8
- jest: 29.7.0(@types/node@25.5.2)
- json5: 2.2.3
- lodash.memoize: 4.1.2
- make-error: 1.3.6
- semver: 7.7.4
- type-fest: 4.41.0
- typescript: 5.9.3
- yargs-parser: 21.1.1
- optionalDependencies:
- '@babel/core': 7.29.0
- '@jest/transform': 29.7.0
- '@jest/types': 30.3.0
- babel-jest: 29.7.0(@babel/core@7.29.0)
- jest-util: 30.3.0
-
- ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@30.3.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0)(typescript@5.9.3):
- dependencies:
- bs-logger: 0.2.6
- fast-json-stable-stringify: 2.1.0
- handlebars: 4.7.8
- jest: 29.7.0
- json5: 2.2.3
- lodash.memoize: 4.1.2
- make-error: 1.3.6
- semver: 7.7.4
- type-fest: 4.41.0
- typescript: 5.9.3
- yargs-parser: 21.1.1
- optionalDependencies:
- '@babel/core': 7.29.0
- '@jest/transform': 29.7.0
- '@jest/types': 30.3.0
- babel-jest: 29.7.0(@babel/core@7.29.0)
- jest-util: 30.3.0
-
ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1