From d94727cca912a88a1c0c39ff195f7d91677c6ca1 Mon Sep 17 00:00:00 2001 From: DeWitt Gibson Date: Mon, 6 Apr 2026 15:17:22 -0700 Subject: [PATCH] Add user-feedback-collector plugin Introduce a new user-feedback-collector plugin that captures thumbs/stars ratings (with optional comments), stores per-message feedback, maintains daily model stats, and exposes endpoints for submitting, listing, aggregating, and exporting feedback. Adds POST /feedback, GET /feedback, GET /stats and GET /export endpoints, a response:modify filter to signal the frontend, CSV export utilities, DB key helpers, and rating/aggregation logic. Includes comprehensive unit tests, plugin manifest, package.json, and TypeScript configuration for the plugin. --- .../__tests__/index.test.ts | 973 ++++++++++++++++++ .../__tests__/tsconfig.json | 14 + .../user-feedback-collector/manifest.json | 42 + .../user-feedback-collector/package.json | 35 + .../user-feedback-collector/src/index.ts | 436 ++++++++ .../user-feedback-collector/tsconfig.json | 24 + .../tsconfig.test.json | 12 + pnpm-lock.yaml | 294 ++++-- 8 files changed, 1737 insertions(+), 93 deletions(-) create mode 100644 packages/plugins/official/user-feedback-collector/__tests__/index.test.ts create mode 100644 packages/plugins/official/user-feedback-collector/__tests__/tsconfig.json create mode 100644 packages/plugins/official/user-feedback-collector/manifest.json create mode 100644 packages/plugins/official/user-feedback-collector/package.json create mode 100644 packages/plugins/official/user-feedback-collector/src/index.ts create mode 100644 packages/plugins/official/user-feedback-collector/tsconfig.json create mode 100644 packages/plugins/official/user-feedback-collector/tsconfig.test.json diff --git a/packages/plugins/official/user-feedback-collector/__tests__/index.test.ts b/packages/plugins/official/user-feedback-collector/__tests__/index.test.ts new file mode 100644 index 0000000..05dd191 --- /dev/null +++ b/packages/plugins/official/user-feedback-collector/__tests__/index.test.ts @@ -0,0 +1,973 @@ +/// +/** + * User Feedback Collector — Unit Tests + * + * Covers: DB key helpers, todayUtc, isoWeek, yearMonth, normaliseRating, + * updateStats, buildCsvRow, plugin manifest/settings, app:init (4 endpoints), + * POST /feedback (success, missing fields, bad rating, requireComment), + * GET /feedback (no filter, conversationId filter, model filter, date filter), + * GET /stats (group by day / week / month, model filter), + * GET /export (empty, with records, CSV format), + * response:modify filter. + */ +import plugin, { + buildFeedbackKey, + buildStatsKey, + todayUtc, + isoWeek, + yearMonth, + normaliseRating, + updateStats, + buildCsvRow, + CSV_HEADER, + FEEDBACK_KEY_PREFIX, + STATS_KEY_PREFIX, + FeedbackRecord, + DailyModelStats, +} 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([ + ["enableAutoPrompt", true], + ["feedbackTypes", "thumbs"], + ["requireComment", false], + ...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 runInit(ctx: MockCtx): Promise { + const hook = plugin.definition.hooks?.["app:init"]; + if (!hook) throw new Error("app:init hook not registered"); + await hook(ctx); +} + +function getEndpoint( + api: MockAPI, + method: string, + path: string, +): EndpointDefinition { + const ep = api._endpoints.find((e) => e.method === method && e.path === path); + if (!ep) throw new Error(`Endpoint ${method} ${path} not found`); + return ep; +} + +// ── DB Key Helpers ──────────────────────────────────────────────────────────── + +describe("buildFeedbackKey", () => { + it("formats as feedback:conversationId:messageIndex", () => { + expect(buildFeedbackKey("conv-1", 0)).toBe("feedback:conv-1:0"); + expect(buildFeedbackKey("conv-abc", 42)).toBe("feedback:conv-abc:42"); + }); +}); + +describe("buildStatsKey", () => { + it("formats as stats:daily:date:model", () => { + expect(buildStatsKey("2025-06-01", "openai/gpt-4o")).toBe( + "stats:daily:2025-06-01:openai/gpt-4o", + ); + }); +}); + +// ── todayUtc ────────────────────────────────────────────────────────────────── + +describe("todayUtc", () => { + it("returns YYYY-MM-DD format", () => { + expect(todayUtc(new Date("2025-06-15T10:00:00Z").getTime())).toBe( + "2025-06-15", + ); + }); + + it("returns current date when no arg given", () => { + expect(todayUtc()).toMatch(/^\d{4}-\d{2}-\d{2}$/); + }); +}); + +// ── isoWeek ─────────────────────────────────────────────────────────────────── + +describe("isoWeek", () => { + it("returns YYYY-WNN format", () => { + expect(isoWeek("2025-01-01")).toMatch(/^\d{4}-W\d{2}$/); + }); + + it("groups two days in same week", () => { + // 2025-06-04 (day 155) and 2025-06-10 (day 161) both yield ceil(n/7) = 23 + const w1 = isoWeek("2025-06-04"); + const w2 = isoWeek("2025-06-10"); + expect(w1).toBe(w2); + }); + + it("gives different weeks for days in different weeks", () => { + const w1 = isoWeek("2025-06-01"); + const w2 = isoWeek("2025-06-09"); + expect(w1).not.toBe(w2); + }); +}); + +// ── yearMonth ───────────────────────────────────────────────────────────────── + +describe("yearMonth", () => { + it("returns YYYY-MM", () => { + expect(yearMonth("2025-06-15")).toBe("2025-06"); + expect(yearMonth("2024-12-31")).toBe("2024-12"); + }); +}); + +// ── normaliseRating ─────────────────────────────────────────────────────────── + +describe("normaliseRating", () => { + describe("thumbs type", () => { + it("maps 'up' → numeric 2", () => { + const r = normaliseRating("up", "thumbs"); + expect(r).not.toBeNull(); + expect(r!.numeric).toBe(2); + expect(r!.raw).toBe("up"); + }); + + it("maps 'down' → numeric 1", () => { + const r = normaliseRating("down", "thumbs"); + expect(r!.numeric).toBe(1); + expect(r!.raw).toBe("down"); + }); + + it("returns null for invalid value", () => { + expect(normaliseRating("maybe", "thumbs")).toBeNull(); + expect(normaliseRating(3, "thumbs")).toBeNull(); + expect(normaliseRating(null, "thumbs")).toBeNull(); + }); + }); + + describe("stars type", () => { + it("maps integer 1–5 through as-is", () => { + for (let i = 1; i <= 5; i++) { + const r = normaliseRating(i, "stars"); + expect(r).not.toBeNull(); + expect(r!.numeric).toBe(i); + expect(r!.raw).toBe(i); + } + }); + + it("returns null for out-of-range values", () => { + expect(normaliseRating(0, "stars")).toBeNull(); + expect(normaliseRating(6, "stars")).toBeNull(); + }); + + it("returns null for non-integers", () => { + expect(normaliseRating(2.5, "stars")).toBeNull(); + }); + }); + + describe("both type", () => { + it("accepts thumbs values", () => { + expect(normaliseRating("up", "both")).not.toBeNull(); + expect(normaliseRating("down", "both")).not.toBeNull(); + }); + + it("accepts star values", () => { + expect(normaliseRating(4, "both")).not.toBeNull(); + }); + }); +}); + +// ── updateStats ─────────────────────────────────────────────────────────────── + +describe("updateStats", () => { + it("creates a new record when none exists", () => { + const rec = updateStats(null, "gpt-4o", "2025-06-01", 2); + expect(rec.count).toBe(1); + expect(rec.ratingSum).toBe(2); + expect(rec.avgRating).toBe(2); + expect(rec.model).toBe("gpt-4o"); + expect(rec.date).toBe("2025-06-01"); + }); + + it("increments existing record", () => { + const existing: DailyModelStats = { + model: "gpt-4o", + date: "2025-06-01", + ratingSum: 4, + count: 2, + avgRating: 2, + }; + const rec = updateStats(existing, "gpt-4o", "2025-06-01", 1); + expect(rec.count).toBe(3); + expect(rec.ratingSum).toBe(5); + expect(rec.avgRating).toBeCloseTo(5 / 3); + }); + + it("computes correct average with multiple updates", () => { + let rec: DailyModelStats | null = null; + rec = updateStats(rec, "m", "2025-01-01", 2); + rec = updateStats(rec, "m", "2025-01-01", 2); + rec = updateStats(rec, "m", "2025-01-01", 1); + expect(rec.count).toBe(3); + expect(rec.avgRating).toBeCloseTo(5 / 3); + }); +}); + +// ── buildCsvRow ─────────────────────────────────────────────────────────────── + +describe("buildCsvRow", () => { + it("produces a comma-separated row with 8 fields", () => { + const record: FeedbackRecord = { + conversationId: "conv-1", + messageIndex: 0, + rating: 2, + rawRating: "up", + comment: "Great!", + model: "gpt-4o", + userId: "u1", + timestamp: new Date("2025-06-01T12:00:00Z").getTime(), + }; + const row = buildCsvRow(record); + const cols = row.split(","); + expect(cols).toHaveLength(8); + expect(cols[0]).toBe("conv-1"); + expect(cols[2]).toBe("up"); + }); + + it("escapes commas in comment field", () => { + const record: FeedbackRecord = { + conversationId: "c", + messageIndex: 0, + rating: 1, + rawRating: "down", + comment: "bad, really bad", + model: "m", + userId: "u", + timestamp: Date.now(), + }; + const row = buildCsvRow(record); + expect(row).toContain('"bad, really bad"'); + }); + + it("escapes double-quotes in comment", () => { + const record: FeedbackRecord = { + conversationId: "c", + messageIndex: 0, + rating: 2, + rawRating: "up", + comment: 'She said "hello"', + model: "m", + userId: "u", + timestamp: Date.now(), + }; + const row = buildCsvRow(record); + expect(row).toContain('"She said ""hello"""'); + }); + + it("handles missing optional fields gracefully", () => { + const record: FeedbackRecord = { + conversationId: "c", + messageIndex: 1, + rating: 2, + rawRating: "up", + timestamp: 0, + }; + // Should not throw + expect(() => buildCsvRow(record)).not.toThrow(); + }); +}); + +describe("CSV_HEADER", () => { + it("has 8 columns", () => { + expect(CSV_HEADER.split(",")).toHaveLength(8); + }); +}); + +// ── Plugin manifest ─────────────────────────────────────────────────────────── + +describe("plugin manifest", () => { + it("has correct name", () => { + expect(plugin.manifest.name).toBe("user-feedback-collector"); + }); + + it("has correct version", () => { + expect(plugin.manifest.version).toBe("1.0.0"); + }); + + it("has a description", () => { + expect((plugin.manifest.description ?? "").length).toBeGreaterThan(10); + }); +}); + +// ── Plugin settings ─────────────────────────────────────────────────────────── + +describe("plugin settings", () => { + const settings = plugin.definition.settings!; + + it("defines exactly 3 settings", () => { + expect(Object.keys(settings)).toHaveLength(3); + }); + + it("has enableAutoPrompt as boolean", () => { + expect((settings["enableAutoPrompt"] as { type: string }).type).toBe( + "boolean", + ); + }); + + it("has feedbackTypes as select", () => { + expect((settings["feedbackTypes"] as { type: string }).type).toBe("select"); + }); + + it("has requireComment as boolean with default false", () => { + const s = settings["requireComment"] as { type: string; default: unknown }; + expect(s.type).toBe("boolean"); + expect(s.default).toBe(false); + }); +}); + +// ── app:init — endpoint registration ───────────────────────────────────────── + +describe("app:init", () => { + it("registers exactly 4 endpoints", async () => { + const ctx = makeCtx(); + await runInit(ctx); + expect(ctx.api._endpoints).toHaveLength(4); + }); + + it("registers POST /feedback", async () => { + const ctx = makeCtx(); + await runInit(ctx); + expect(() => getEndpoint(ctx.api, "POST", "/feedback")).not.toThrow(); + }); + + it("registers GET /feedback", async () => { + const ctx = makeCtx(); + await runInit(ctx); + expect(() => getEndpoint(ctx.api, "GET", "/feedback")).not.toThrow(); + }); + + it("registers GET /stats", async () => { + const ctx = makeCtx(); + await runInit(ctx); + expect(() => getEndpoint(ctx.api, "GET", "/stats")).not.toThrow(); + }); + + it("registers GET /export", async () => { + const ctx = makeCtx(); + await runInit(ctx); + expect(() => getEndpoint(ctx.api, "GET", "/export")).not.toThrow(); + }); + + it("all endpoints require auth", async () => { + const ctx = makeCtx(); + await runInit(ctx); + for (const ep of ctx.api._endpoints) { + expect(ep.auth).toBe(true); + } + }); +}); + +// ── POST /feedback ──────────────────────────────────────────────────────────── + +describe("POST /feedback", () => { + it("returns 400 when conversationId missing", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ body: { messageIndex: 0, rating: "up" } }), + res as unknown as EndpointResponse, + ); + expect(res._status).toBe(400); + }); + + it("returns 400 when messageIndex missing", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ body: { conversationId: "c1", rating: "up" } }), + res as unknown as EndpointResponse, + ); + expect(res._status).toBe(400); + }); + + it("returns 400 for invalid rating", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ + body: { conversationId: "c1", messageIndex: 0, rating: "maybe" }, + }), + res as unknown as EndpointResponse, + ); + expect(res._status).toBe(400); + }); + + it("saves feedback record and returns 201 on success", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ + body: { + conversationId: "conv-1", + messageIndex: 2, + rating: "up", + model: "gpt-4o", + }, + }), + res as unknown as EndpointResponse, + ); + expect(res._status).toBe(201); + const body = res._body as { ok: boolean; feedback: FeedbackRecord }; + expect(body.ok).toBe(true); + expect(body.feedback.rawRating).toBe("up"); + expect(body.feedback.rating).toBe(2); + + const saved = (await ctx.api.db.get( + buildFeedbackKey("conv-1", 2), + )) as FeedbackRecord; + expect(saved).not.toBeNull(); + }); + + it("updates daily stats on submission", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ + body: { + conversationId: "c", + messageIndex: 0, + rating: "up", + model: "gpt-4o", + }, + }), + res as unknown as EndpointResponse, + ); + + const today = todayUtc(); + const statsKey = buildStatsKey(today, "gpt-4o"); + const stats = (await ctx.api.db.get(statsKey)) as DailyModelStats; + expect(stats).not.toBeNull(); + expect(stats.count).toBe(1); + expect(stats.ratingSum).toBe(2); + }); + + it("returns 400 when requireComment=true and no comment given", async () => { + const ctx = makeCtx({}, { requireComment: true }); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ + body: { conversationId: "c", messageIndex: 0, rating: "up" }, + }), + res as unknown as EndpointResponse, + ); + expect(res._status).toBe(400); + }); + + it("accepts comment when requireComment=true", async () => { + const ctx = makeCtx({}, { requireComment: true }); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ + body: { + conversationId: "c", + messageIndex: 0, + rating: "up", + comment: "Great response!", + }, + }), + res as unknown as EndpointResponse, + ); + expect(res._status).toBe(201); + }); + + it("accepts star ratings when feedbackTypes=stars", async () => { + const ctx = makeCtx({}, { feedbackTypes: "stars" }); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ + body: { conversationId: "c", messageIndex: 0, rating: 4 }, + }), + res as unknown as EndpointResponse, + ); + expect(res._status).toBe(201); + const body = res._body as { feedback: FeedbackRecord }; + expect(body.feedback.rating).toBe(4); + }); + + it("stores userId from context", async () => { + const ctx = makeCtx({ userId: "user-99" }); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "POST", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ + body: { conversationId: "c", messageIndex: 0, rating: "down" }, + }), + res as unknown as EndpointResponse, + ); + const saved = (await ctx.api.db.get( + buildFeedbackKey("c", 0), + )) as FeedbackRecord; + expect(saved.userId).toBe("user-99"); + }); +}); + +// ── GET /feedback ───────────────────────────────────────────────────────────── + +async function seedFeedback( + ctx: MockCtx, + records: Partial[], +): Promise { + for (const r of records) { + const full: FeedbackRecord = { + conversationId: r.conversationId ?? "conv-1", + messageIndex: r.messageIndex ?? 0, + rating: r.rating ?? 2, + rawRating: r.rawRating ?? "up", + comment: r.comment, + model: r.model, + userId: r.userId, + timestamp: r.timestamp ?? Date.now(), + }; + await ctx.api.db.set( + buildFeedbackKey(full.conversationId, full.messageIndex), + full, + ); + } +} + +describe("GET /feedback", () => { + it("returns empty array when no records", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/feedback"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + expect(res._status).toBe(200); + expect((res._body as { feedback: unknown[] }).feedback).toEqual([]); + }); + + it("returns all records when no filter", async () => { + const ctx = makeCtx(); + await seedFeedback(ctx, [ + { conversationId: "c1", messageIndex: 0 }, + { conversationId: "c2", messageIndex: 0 }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/feedback"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + const body = res._body as { feedback: FeedbackRecord[]; total: number }; + expect(body.feedback).toHaveLength(2); + expect(body.total).toBe(2); + }); + + it("filters by conversationId", async () => { + const ctx = makeCtx(); + await seedFeedback(ctx, [ + { conversationId: "c1", messageIndex: 0 }, + { conversationId: "c2", messageIndex: 0 }, + { conversationId: "c1", messageIndex: 1 }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ query: { conversationId: "c1" } }), + res as unknown as EndpointResponse, + ); + const body = res._body as { feedback: FeedbackRecord[] }; + expect(body.feedback).toHaveLength(2); + expect(body.feedback.every((r) => r.conversationId === "c1")).toBe(true); + }); + + it("filters by model", async () => { + const ctx = makeCtx(); + await seedFeedback(ctx, [ + { conversationId: "c1", messageIndex: 0, model: "gpt-4o" }, + { conversationId: "c1", messageIndex: 1, model: "claude-3-5-sonnet" }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ query: { model: "gpt-4o" } }), + res as unknown as EndpointResponse, + ); + const body = res._body as { feedback: FeedbackRecord[] }; + expect(body.feedback).toHaveLength(1); + expect(body.feedback[0]!.model).toBe("gpt-4o"); + }); + + it("filters by from date", async () => { + const now = Date.now(); + const ctx = makeCtx(); + await seedFeedback(ctx, [ + { conversationId: "c1", messageIndex: 0, timestamp: now - 10_000 }, + { conversationId: "c1", messageIndex: 1, timestamp: now }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ + query: { from: new Date(now - 5_000).toISOString() }, + }), + res as unknown as EndpointResponse, + ); + const body = res._body as { feedback: FeedbackRecord[] }; + expect(body.feedback).toHaveLength(1); + }); + + it("respects limit parameter", async () => { + const ctx = makeCtx(); + await seedFeedback(ctx, [ + { conversationId: "c1", messageIndex: 0 }, + { conversationId: "c2", messageIndex: 0 }, + { conversationId: "c3", messageIndex: 0 }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/feedback"); + const res = makeRes(); + await ep.handler( + makeReq({ query: { limit: "2" } }), + res as unknown as EndpointResponse, + ); + const body = res._body as { feedback: FeedbackRecord[]; total: number }; + expect(body.feedback).toHaveLength(2); + expect(body.total).toBe(3); // total reflects unsliced count + }); +}); + +// ── GET /stats ──────────────────────────────────────────────────────────────── + +async function seedStats( + ctx: MockCtx, + records: Partial[], +): Promise { + for (const r of records) { + const full: DailyModelStats = { + model: r.model ?? "gpt-4o", + date: r.date ?? "2025-06-01", + ratingSum: r.ratingSum ?? 4, + count: r.count ?? 2, + avgRating: r.avgRating ?? 2, + }; + await ctx.api.db.set(buildStatsKey(full.date, full.model), full); + } +} + +describe("GET /stats", () => { + it("returns empty stats when no data", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/stats"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + expect(res._status).toBe(200); + expect((res._body as { stats: unknown[] }).stats).toEqual([]); + }); + + it("groups by day by default", async () => { + const ctx = makeCtx(); + await seedStats(ctx, [ + { model: "gpt-4o", date: "2025-06-01", ratingSum: 4, count: 2 }, + { model: "gpt-4o", date: "2025-06-02", ratingSum: 3, count: 2 }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/stats"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + const body = res._body as { stats: { period: string }[]; groupBy: string }; + expect(body.groupBy).toBe("day"); + expect(body.stats).toHaveLength(2); + // Sorted descending by period + expect(body.stats[0]!.period).toBe("2025-06-02"); + }); + + it("groups by week", async () => { + const ctx = makeCtx(); + // Same calendar week + await seedStats(ctx, [ + { model: "gpt-4o", date: "2025-06-02", ratingSum: 4, count: 2 }, + { model: "gpt-4o", date: "2025-06-03", ratingSum: 2, count: 1 }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/stats"); + const res = makeRes(); + await ep.handler( + makeReq({ query: { groupBy: "week" } }), + res as unknown as EndpointResponse, + ); + const body = res._body as { stats: { count: number }[] }; + // Both days folded into one week entry + expect(body.stats).toHaveLength(1); + expect(body.stats[0]!.count).toBe(3); + }); + + it("groups by month", async () => { + const ctx = makeCtx(); + await seedStats(ctx, [ + { model: "m", date: "2025-06-01", ratingSum: 4, count: 2 }, + { model: "m", date: "2025-06-15", ratingSum: 2, count: 1 }, + { model: "m", date: "2025-07-01", ratingSum: 3, count: 1 }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/stats"); + const res = makeRes(); + await ep.handler( + makeReq({ query: { groupBy: "month" } }), + res as unknown as EndpointResponse, + ); + const body = res._body as { stats: { period: string; count: number }[] }; + expect(body.stats).toHaveLength(2); + const june = body.stats.find((s) => s.period === "2025-06"); + expect(june!.count).toBe(3); + }); + + it("filters by model", async () => { + const ctx = makeCtx(); + await seedStats(ctx, [ + { model: "gpt-4o", date: "2025-06-01" }, + { model: "claude", date: "2025-06-01" }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/stats"); + const res = makeRes(); + await ep.handler( + makeReq({ query: { model: "gpt-4o" } }), + res as unknown as EndpointResponse, + ); + const body = res._body as { stats: { model: string }[] }; + expect(body.stats).toHaveLength(1); + expect(body.stats[0]!.model).toBe("gpt-4o"); + }); + + it("computes correct avgRating in aggregated buckets", async () => { + const ctx = makeCtx(); + await seedStats(ctx, [ + { model: "m", date: "2025-06-01", ratingSum: 6, count: 3, avgRating: 2 }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/stats"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + const body = res._body as { stats: { avgRating: number }[] }; + expect(body.stats[0]!.avgRating).toBeCloseTo(2); + }); +}); + +// ── GET /export ─────────────────────────────────────────────────────────────── + +describe("GET /export", () => { + it("returns 200 with CSV wrapper object", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/export"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + expect(res._status).toBe(200); + const body = res._body as Record; + expect(body._csv).toBe(true); + expect(body.contentType).toBe("text/csv"); + }); + + it("returns only header row when no feedback exists", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/export"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + const body = res._body as { data: string }; + expect(body.data.trim()).toBe(CSV_HEADER); + }); + + it("includes one data row per feedback record", async () => { + const ctx = makeCtx(); + await seedFeedback(ctx, [ + { conversationId: "c1", messageIndex: 0 }, + { conversationId: "c2", messageIndex: 0 }, + ]); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/export"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + const body = res._body as { data: string }; + const lines = body.data.trim().split("\n"); + expect(lines).toHaveLength(3); // header + 2 records + }); + + it("filename contains today's date", async () => { + const ctx = makeCtx(); + await runInit(ctx); + const ep = getEndpoint(ctx.api, "GET", "/export"); + const res = makeRes(); + await ep.handler(makeReq(), res as unknown as EndpointResponse); + const body = res._body as { filename: string }; + expect(body.filename).toMatch(/feedback-export-\d{4}-\d{2}-\d{2}\.csv/); + }); +}); + +// ── response:modify filter ──────────────────────────────────────────────────── + +describe("response:modify filter", () => { + it("injects _feedbackEnabled: true into response", async () => { + const filter = plugin.definition.filters?.["response:modify"]; + if (!filter) throw new Error("response:modify filter not registered"); + + const ctx = makeCtx(); + const result = await filter(ctx, { text: "Hello" }); + expect((result as Record)["_feedbackEnabled"]).toBe(true); + }); + + it("preserves existing response properties", async () => { + const filter = plugin.definition.filters?.["response:modify"]; + const ctx = makeCtx(); + const result = await filter!(ctx, { text: "Hello", model: "gpt-4o" }); + const r = result as Record; + expect(r["text"]).toBe("Hello"); + expect(r["model"]).toBe("gpt-4o"); + }); +}); + +// ── Key prefix constants ────────────────────────────────────────────────────── + +describe("constants", () => { + it("FEEDBACK_KEY_PREFIX is 'feedback:'", () => { + expect(FEEDBACK_KEY_PREFIX).toBe("feedback:"); + }); + + it("STATS_KEY_PREFIX is 'stats:daily:'", () => { + expect(STATS_KEY_PREFIX).toBe("stats:daily:"); + }); +}); diff --git a/packages/plugins/official/user-feedback-collector/__tests__/tsconfig.json b/packages/plugins/official/user-feedback-collector/__tests__/tsconfig.json new file mode 100644 index 0000000..a05feed --- /dev/null +++ b/packages/plugins/official/user-feedback-collector/__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/user-feedback-collector/manifest.json b/packages/plugins/official/user-feedback-collector/manifest.json new file mode 100644 index 0000000..8b240b8 --- /dev/null +++ b/packages/plugins/official/user-feedback-collector/manifest.json @@ -0,0 +1,42 @@ +{ + "name": "user-feedback-collector", + "version": "1.0.0", + "description": "Thumbs up/down rating after each AI response with optional comments. Aggregated quality scores per model and time period.", + "author": "Agentbase", + "license": "GPL-3.0", + "main": "dist/index.js", + "agentbase": { + "type": "plugin", + "apiVersion": "1" + }, + "hooks": ["app:init"], + "filters": ["response:modify"], + "endpoints": [ + { "method": "POST", "path": "/feedback" }, + { "method": "GET", "path": "/feedback" }, + { "method": "GET", "path": "/stats" }, + { "method": "GET", "path": "/export" } + ], + "settings": [ + { + "key": "enableAutoPrompt", + "type": "boolean", + "label": "Automatically prompt for feedback after each response", + "default": true + }, + { + "key": "feedbackTypes", + "type": "select", + "label": "Rating style", + "options": ["thumbs", "stars", "both"], + "default": "thumbs" + }, + { + "key": "requireComment", + "type": "boolean", + "label": "Require a comment with every rating", + "default": false + } + ], + "permissions": ["db:readwrite"] +} diff --git a/packages/plugins/official/user-feedback-collector/package.json b/packages/plugins/official/user-feedback-collector/package.json new file mode 100644 index 0000000..c1e14c3 --- /dev/null +++ b/packages/plugins/official/user-feedback-collector/package.json @@ -0,0 +1,35 @@ +{ + "name": "@agentbase/plugin-user-feedback-collector", + "version": "1.0.0", + "description": "Thumbs up/down rating after each AI response with optional comments. Aggregated quality scores per model and time period.", + "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/user-feedback-collector/src/index.ts b/packages/plugins/official/user-feedback-collector/src/index.ts new file mode 100644 index 0000000..ce930d6 --- /dev/null +++ b/packages/plugins/official/user-feedback-collector/src/index.ts @@ -0,0 +1,436 @@ +/** + * User Feedback Collector + * + * Captures thumbs-up / thumbs-down (or star) ratings after every AI response + * and stores them for quality monitoring. No AI calls are needed — this plugin + * is pure data collection. + * + * Key design decisions: + * - `response:modify` injects `_feedbackEnabled: true` into every response so + * the frontend knows to render the rating widget. + * - Ratings are stored per message: `feedback:{conversationId}:{messageIndex}` + * - Daily stats keyed by `stats:daily:{date}:{model}` track avg rating + count + * with an incremental running-average update on every submission. + * - `GET /export` streams CSV (no temp files, built inline). + * - Stats `GET /stats` supports grouping by model, day, week, or month. + * + * @package @agentbase/plugin-user-feedback-collector + * @version 1.0.0 + */ +import { createPlugin } from "@agentbase/plugin-sdk"; + +// ── Constants ───────────────────────────────────────────────────────────────── + +export const FEEDBACK_KEY_PREFIX = "feedback:"; +export const STATS_KEY_PREFIX = "stats:daily:"; + +// ── Types ───────────────────────────────────────────────────────────────────── + +export type RatingType = "thumbs" | "stars" | "both"; +export type ThumbsRating = "up" | "down"; +export type StarsRating = 1 | 2 | 3 | 4 | 5; + +export interface FeedbackRecord { + conversationId: string; + messageIndex: number; + /** Numeric rating: 1 (thumbs-down) / 2 (thumbs-up) or 1–5 for stars. */ + rating: number; + /** Raw value as submitted by user: "up" | "down" | 1-5. */ + rawRating: ThumbsRating | StarsRating; + comment?: string; + model?: string; + userId?: string; + timestamp: number; +} + +export interface DailyModelStats { + model: string; + date: string; + /** Running sum of all numeric ratings for the day. */ + ratingSum: number; + /** Total feedback submissions. */ + count: number; + /** Precomputed average: ratingSum / count. Updates on each submission. */ + avgRating: number; +} + +export interface StatsGroupEntry { + period: string; + model: string; + avgRating: number; + count: number; +} + +// ── DB Key Helpers ──────────────────────────────────────────────────────────── + +export function buildFeedbackKey( + conversationId: string, + messageIndex: number, +): string { + return `${FEEDBACK_KEY_PREFIX}${conversationId}:${messageIndex}`; +} + +export function buildStatsKey(date: string, model: string): string { + return `${STATS_KEY_PREFIX}${date}:${model}`; +} + +export function todayUtc(nowMs: number = Date.now()): string { + return new Date(nowMs).toISOString().slice(0, 10); +} + +/** ISO week number (1-53) for a date string "YYYY-MM-DD". */ +export function isoWeek(dateStr: string): string { + const d = new Date(`${dateStr}T00:00:00Z`); + const jan1 = new Date(`${d.getUTCFullYear()}-01-01T00:00:00Z`); + const dayOfYear = Math.floor((d.getTime() - jan1.getTime()) / 86_400_000) + 1; + const week = Math.ceil(dayOfYear / 7); + return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`; +} + +/** "YYYY-MM" month string. */ +export function yearMonth(dateStr: string): string { + return dateStr.slice(0, 7); +} + +// ── Rating Normalisation ────────────────────────────────────────────────────── + +/** + * Convert a raw user-supplied rating into a normalised numeric value. + * - thumbs: "up" → 2, "down" → 1 + * - stars: 1–5 passed through as-is + * Returns `null` when the value is invalid for the given type. + */ +export function normaliseRating( + raw: unknown, + feedbackType: RatingType, +): { numeric: number; raw: ThumbsRating | StarsRating } | null { + if (feedbackType === "thumbs" || feedbackType === "both") { + if (raw === "up") return { numeric: 2, raw: "up" as ThumbsRating }; + if (raw === "down") return { numeric: 1, raw: "down" as ThumbsRating }; + } + if (feedbackType === "stars" || feedbackType === "both") { + const n = Number(raw); + if (Number.isInteger(n) && n >= 1 && n <= 5) + return { numeric: n, raw: n as StarsRating }; + } + return null; +} + +// ── Stats Helpers ───────────────────────────────────────────────────────────── + +/** + * Incrementally update a DailyModelStats record with a new rating. + * Returns the updated record (mutates in place for efficiency). + */ +export function updateStats( + existing: DailyModelStats | null, + model: string, + date: string, + numericRating: number, +): DailyModelStats { + const rec: DailyModelStats = existing ?? { + model, + date, + ratingSum: 0, + count: 0, + avgRating: 0, + }; + rec.ratingSum += numericRating; + rec.count += 1; + rec.avgRating = rec.ratingSum / rec.count; + return rec; +} + +// ── CSV Builder ─────────────────────────────────────────────────────────────── + +export function buildCsvRow(record: FeedbackRecord): string { + const escape = (v: unknown): string => { + const s = String(v ?? ""); + return s.includes(",") || s.includes('"') || s.includes("\n") + ? `"${s.replace(/"/g, '""')}"` + : s; + }; + return [ + escape(record.conversationId), + escape(record.messageIndex), + escape(record.rawRating), + escape(record.rating), + escape(record.comment ?? ""), + escape(record.model ?? ""), + escape(record.userId ?? ""), + escape(new Date(record.timestamp).toISOString()), + ].join(","); +} + +export const CSV_HEADER = + "conversationId,messageIndex,rawRating,numericRating,comment,model,userId,submittedAt"; + +// ── Plugin Definition ───────────────────────────────────────────────────────── + +export default createPlugin({ + name: "user-feedback-collector", + version: "1.0.0", + description: + "Thumbs up/down rating after each AI response with optional comments. Aggregated quality scores per model and time period.", + permissions: ["db:readwrite"], + settings: { + enableAutoPrompt: { + type: "boolean", + label: "Automatically prompt for feedback after each response", + default: true, + }, + feedbackTypes: { + type: "select", + label: "Rating style", + options: ["thumbs", "stars", "both"], + default: "thumbs", + }, + requireComment: { + type: "boolean", + label: "Require a comment with every rating", + default: false, + }, + }, + + hooks: { + "app:init": async (context) => { + const { api } = context; + + // ── POST /feedback ────────────────────────────────────────────────────── + api.registerEndpoint({ + method: "POST", + path: "/feedback", + auth: true, + description: "Submit a rating (and optional comment) for a message", + handler: async (req, res) => { + const { + conversationId, + messageIndex, + rating: rawRating, + comment, + model, + } = req.body as { + conversationId?: string; + messageIndex?: number; + rating?: unknown; + comment?: string; + model?: string; + }; + + if (!conversationId) { + res.status(400).json({ error: "conversationId is required" }); + return; + } + if (messageIndex === undefined || messageIndex === null) { + res.status(400).json({ error: "messageIndex is required" }); + return; + } + + const feedbackType = + (api.getConfig("feedbackTypes") as RatingType) ?? "thumbs"; + const normalized = normaliseRating(rawRating, feedbackType); + if (!normalized) { + res.status(400).json({ + error: `Invalid rating for feedback type '${feedbackType}'`, + }); + return; + } + + const requireComment = + (api.getConfig("requireComment") as boolean) ?? false; + if (requireComment && !comment?.trim()) { + res.status(400).json({ error: "A comment is required" }); + return; + } + + const record: FeedbackRecord = { + conversationId, + messageIndex, + rating: normalized.numeric, + rawRating: normalized.raw, + comment: comment?.trim() ?? undefined, + model: model ?? undefined, + userId: context.userId ?? undefined, + timestamp: Date.now(), + }; + + await api.db.set( + buildFeedbackKey(conversationId, messageIndex), + record, + ); + + // Update daily stats + const date = todayUtc(); + const modelKey = model ?? "unknown"; + const statsKey = buildStatsKey(date, modelKey); + const existing = (await api.db.get( + statsKey, + )) as DailyModelStats | null; + const updated = updateStats( + existing, + modelKey, + date, + normalized.numeric, + ); + await api.db.set(statsKey, updated); + + res.status(201).json({ ok: true, feedback: record }); + }, + }); + + // ── GET /feedback ─────────────────────────────────────────────────────── + api.registerEndpoint({ + method: "GET", + path: "/feedback", + auth: true, + description: "List feedback records with optional filters", + handler: async (req, res) => { + const { + conversationId, + model, + from, + to, + limit: limitParam, + } = req.query as Record; + const limit = Math.min(parseInt(limitParam ?? "50", 10) || 50, 200); + + const allKeys = await api.db.keys(FEEDBACK_KEY_PREFIX); + const records = ( + await Promise.all(allKeys.map((k) => api.db.get(k))) + ).filter((r): r is FeedbackRecord => r !== null); + + let filtered = records; + if (conversationId) { + filtered = filtered.filter( + (r) => r.conversationId === conversationId, + ); + } + if (model) { + filtered = filtered.filter((r) => r.model === model); + } + if (from) { + const fromMs = new Date(from).getTime(); + if (!isNaN(fromMs)) { + filtered = filtered.filter((r) => r.timestamp >= fromMs); + } + } + if (to) { + const toMs = new Date(to).getTime(); + if (!isNaN(toMs)) { + filtered = filtered.filter((r) => r.timestamp <= toMs); + } + } + + filtered.sort((a, b) => b.timestamp - a.timestamp); + res.status(200).json({ + feedback: filtered.slice(0, limit), + total: filtered.length, + }); + }, + }); + + // ── GET /stats ────────────────────────────────────────────────────────── + api.registerEndpoint({ + method: "GET", + path: "/stats", + auth: true, + description: + "Aggregate quality scores — group by model, day, week, or month", + handler: async (req, res) => { + const { groupBy = "day", model: filterModel } = req.query as Record< + string, + string | undefined + >; + + const allKeys = await api.db.keys(STATS_KEY_PREFIX); + const records = ( + await Promise.all(allKeys.map((k) => api.db.get(k))) + ).filter((r): r is DailyModelStats => r !== null); + + let filtered = records; + if (filterModel) { + filtered = filtered.filter((r) => r.model === filterModel); + } + + // Re-aggregate by the requested grouping + const buckets = new Map< + string, + { ratingSum: number; count: number; model: string } + >(); + + for (const rec of filtered) { + let period: string; + if (groupBy === "week") { + period = isoWeek(rec.date); + } else if (groupBy === "month") { + period = yearMonth(rec.date); + } else { + period = rec.date; // default: day + } + + const key = `${period}::${rec.model}`; + const existing = buckets.get(key) ?? { + ratingSum: 0, + count: 0, + model: rec.model, + }; + existing.ratingSum += rec.ratingSum; + existing.count += rec.count; + buckets.set(key, existing); + } + + const result: StatsGroupEntry[] = [...buckets.entries()].map( + ([key, v]) => ({ + period: key.split("::")[0]!, + model: v.model, + avgRating: v.count > 0 ? v.ratingSum / v.count : 0, + count: v.count, + }), + ); + + result.sort((a, b) => b.period.localeCompare(a.period)); + res.status(200).json({ stats: result, groupBy }); + }, + }); + + // ── GET /export ───────────────────────────────────────────────────────── + api.registerEndpoint({ + method: "GET", + path: "/export", + auth: true, + description: "Download all feedback as a CSV file", + handler: async (_req, res) => { + const allKeys = await api.db.keys(FEEDBACK_KEY_PREFIX); + const records = ( + await Promise.all(allKeys.map((k) => api.db.get(k))) + ).filter((r): r is FeedbackRecord => r !== null); + + records.sort((a, b) => a.timestamp - b.timestamp); + + const lines = [CSV_HEADER, ...records.map((r) => buildCsvRow(r))]; + const csv = lines.join("\n"); + + // Return as text/csv; handler sets headers via res.status().json() convention. + // Well-behaved SDK implementations pass `headers` through — we use json-as-text + // for SDK compatibility, wrapping in an object the frontend/SDK can detect. + res.status(200).json({ + _csv: true, + filename: `feedback-export-${todayUtc()}.csv`, + contentType: "text/csv", + data: csv, + }); + }, + }); + }, + }, + + filters: { + "response:modify": async (_context, response) => { + // Signal the frontend to render the feedback widget for this response. + return { + ...response, + _feedbackEnabled: true, + }; + }, + }, +}); diff --git a/packages/plugins/official/user-feedback-collector/tsconfig.json b/packages/plugins/official/user-feedback-collector/tsconfig.json new file mode 100644 index 0000000..5628f37 --- /dev/null +++ b/packages/plugins/official/user-feedback-collector/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/packages/plugins/official/user-feedback-collector/tsconfig.test.json b/packages/plugins/official/user-feedback-collector/tsconfig.test.json new file mode 100644 index 0000000..68f8f76 --- /dev/null +++ b/packages/plugins/official/user-feedback-collector/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "outDir": "dist-test", + "paths": { + "@agentbase/plugin-sdk": ["../../../sdk/src/index.ts"] + } + }, + "include": ["src/**/*", "__tests__/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 546e128..0104670 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)(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3)) + version: 29.7.0(@types/node@22.19.11) 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)(ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3)))(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))(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(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)) + version: 29.7.0 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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(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)(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(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)) + version: 29.7.0 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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(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)(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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)) + version: 29.7.0(@types/node@25.5.2) 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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(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))(typescript@5.9.3) typescript: specifier: ^5.7.0 version: 5.9.3 @@ -436,10 +436,10 @@ importers: version: 25.5.2 jest: specifier: ^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)) + version: 29.7.0(@types/node@25.5.2) 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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(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))(typescript@5.9.3) typescript: specifier: ^5.7.0 version: 5.9.3 @@ -458,10 +458,10 @@ importers: 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)) + version: 29.7.0(@types/node@20.19.33) 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) + 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) typescript: specifier: ^5.0.0 version: 5.9.3 @@ -480,14 +480,36 @@ importers: version: 25.5.2 jest: specifier: ^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)) + version: 29.7.0(@types/node@25.5.2) 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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(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))(typescript@5.9.3) typescript: specifier: ^5.7.0 version: 5.9.3 + packages/plugins/official/user-feedback-collector: + 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-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) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/plugins/official/zapier-bridge: dependencies: '@agentbase/plugin-sdk': @@ -502,10 +524,10 @@ importers: version: 25.5.2 jest: specifier: ^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)) + version: 29.7.0(@types/node@25.5.2) 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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(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))(typescript@5.9.3) typescript: specifier: ^5.7.0 version: 5.9.3 @@ -521,10 +543,10 @@ importers: version: 29.5.14 jest: specifier: ^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)) + version: 29.7.0 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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(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)(typescript@5.9.3) typescript: specifier: ^5.7.0 version: 5.9.3 @@ -542,10 +564,10 @@ importers: version: 29.5.14 jest: specifier: ^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)) + version: 29.7.0 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)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)))(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)(typescript@5.9.3) typescript: specifier: ^5.3.0 version: 5.9.3 @@ -6364,41 +6386,6 @@ 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 @@ -8393,13 +8380,13 @@ snapshots: optionalDependencies: typescript: 5.7.2 - create-jest@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)): + create-jest@29.7.0(@types/node@20.19.33): 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)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@20.19.33) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -9615,16 +9602,54 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)): + jest-cli@29.7.0: dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)) + '@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)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)) + 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@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)) + 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-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -9653,9 +9678,9 @@ 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-cli@29.7.0(@types/node@25.5.2): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)) + '@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 @@ -9672,38 +9697,26 @@ snapshots: - supports-color - ts-node - 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-cli@29.7.0(@types/node@25.5.2)(ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3)): dependencies: - '@babel/core': 7.29.0 - '@jest/test-sequencer': 29.7.0 + '@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 - babel-jest: 29.7.0(@babel/core@7.29.0) chalk: 4.1.2 - 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 + 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)) jest-util: 29.7.0 jest-validate: 29.7.0 - 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) + yargs: 17.7.2 transitivePeerDependencies: + - '@types/node' - babel-plugin-macros - supports-color + - ts-node - 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-config@29.7.0(@types/node@20.19.33): dependencies: '@babel/core': 7.29.0 '@jest/test-sequencer': 29.7.0 @@ -9728,8 +9741,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.19.11 - ts-node: 10.9.2(@types/node@20.19.33)(typescript@5.9.3) + '@types/node': 20.19.33 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -10130,12 +10142,36 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)): + jest@29.7.0: dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)) + '@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/types': 29.6.3 import-local: 3.2.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)) + jest-cli: 29.7.0(@types/node@22.19.11) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -10154,6 +10190,18 @@ 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)) @@ -12231,12 +12279,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)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(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))(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)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)) + jest: 29.7.0(@types/node@20.19.33) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -12271,6 +12319,26 @@ 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 @@ -12291,6 +12359,46 @@ 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