From 8df3b0eb6c92cae4316daef4ae7bb770db2ee002 Mon Sep 17 00:00:00 2001 From: Disviel Date: Mon, 26 Jan 2026 11:48:28 +0800 Subject: [PATCH 01/16] feat: implement Agenda-backed job scheduling system --- packages/ema/package.json | 1 + packages/ema/src/db/mongo.ts | 6 + packages/ema/src/db/mongo/memory.ts | 12 + packages/ema/src/db/mongo/remote.ts | 31 + packages/ema/src/scheduler/base.ts | 107 ++ packages/ema/src/scheduler/index.ts | 3 + packages/ema/src/scheduler/jobs/index.ts | 23 + packages/ema/src/scheduler/jobs/test.job.ts | 18 + packages/ema/src/scheduler/scheduler.ts | 172 +++ packages/ema/src/server.ts | 8 +- packages/ema/src/test_scheduler.ts | 46 + packages/ema/src/tests/scheduler.spec.ts | 143 ++ pnpm-lock.yaml | 1292 ++++++++++++++++++- 13 files changed, 1836 insertions(+), 26 deletions(-) create mode 100644 packages/ema/src/scheduler/base.ts create mode 100644 packages/ema/src/scheduler/index.ts create mode 100644 packages/ema/src/scheduler/jobs/index.ts create mode 100644 packages/ema/src/scheduler/jobs/test.job.ts create mode 100644 packages/ema/src/scheduler/scheduler.ts create mode 100644 packages/ema/src/test_scheduler.ts create mode 100644 packages/ema/src/tests/scheduler.spec.ts diff --git a/packages/ema/package.json b/packages/ema/package.json index 64c80715..b2abe3af 100644 --- a/packages/ema/package.json +++ b/packages/ema/package.json @@ -28,6 +28,7 @@ ], "dependencies": { "@google/genai": "^1.34.0", + "@hokify/agenda": "^6.3.0", "@lancedb/lancedb": "^0.23.0", "@modelcontextprotocol/sdk": "^1.25.1", "apache-arrow": "^18.1.0", diff --git a/packages/ema/src/db/mongo.ts b/packages/ema/src/db/mongo.ts index 67be2e68..ae5431e3 100644 --- a/packages/ema/src/db/mongo.ts +++ b/packages/ema/src/db/mongo.ts @@ -66,6 +66,12 @@ export abstract class Mongo { */ abstract getClient(): MongoClient; + /** + * Gets the MongoDB connection URI. + * @returns The MongoDB connection URI + */ + abstract getUri(): string; + /** * Connects to the MongoDB database * @returns Promise resolving when connection is established diff --git a/packages/ema/src/db/mongo/memory.ts b/packages/ema/src/db/mongo/memory.ts index fc0504e6..33ba51a8 100644 --- a/packages/ema/src/db/mongo/memory.ts +++ b/packages/ema/src/db/mongo/memory.ts @@ -83,6 +83,18 @@ export class MemoryMongo extends Mongo { return this.client; } + /** + * Gets the MongoDB connection URI. + * @returns The MongoDB connection URI + * @throws Error if not connected + */ + getUri(): string { + if (!this.mongoServer) { + throw new Error("MongoDB not connected. Call connect() first."); + } + return this.mongoServer.getUri(this.dbName); + } + /** * Closes the MongoDB connection and stops the in-memory server * @returns Promise resolving when connection is closed and server is stopped diff --git a/packages/ema/src/db/mongo/remote.ts b/packages/ema/src/db/mongo/remote.ts index f6a3a33d..93aefd59 100644 --- a/packages/ema/src/db/mongo/remote.ts +++ b/packages/ema/src/db/mongo/remote.ts @@ -62,6 +62,14 @@ export class RemoteMongo extends Mongo { return this.client; } + /** + * Gets the MongoDB connection URI. + * @returns The MongoDB connection URI + */ + getUri(): string { + return this.buildUriWithDb(); + } + /** * Closes the MongoDB connection * @returns Promise resolving when connection is closed @@ -72,4 +80,27 @@ export class RemoteMongo extends Mongo { this.client = undefined; } } + + private buildUriWithDb(): string { + const [base, query] = this.uri.split("?"); + const withQuery = (value: string) => (query ? `${value}?${query}` : value); + + const schemeIndex = base.indexOf("://"); + if (schemeIndex === -1) { + return withQuery(base); + } + + const rest = base.slice(schemeIndex + 3); + const slashIndex = rest.indexOf("/"); + if (slashIndex === -1) { + return withQuery(`${base}/${this.dbName}`); + } + + const path = rest.slice(slashIndex + 1); + if (!path) { + return withQuery(`${base}${this.dbName}`); + } + + return withQuery(base); + } } diff --git a/packages/ema/src/scheduler/base.ts b/packages/ema/src/scheduler/base.ts new file mode 100644 index 00000000..a9dfbbfd --- /dev/null +++ b/packages/ema/src/scheduler/base.ts @@ -0,0 +1,107 @@ +/** + * Scheduler domain types and interfaces for Agenda-backed scheduling. + * + * The scheduler API remains stable while relying on Agenda's job runtime model. + */ +import type { JobDataMap } from "./jobs"; +import type { Job as AgendaJob } from "@hokify/agenda"; + +/** + * Union of all job names. + */ +export type JobName = keyof JobDataMap; + +/** + * Data type for a specific job name. + * @typeParam K - The job name. + */ +export type JobData = JobDataMap[K]; + +/** + * Union of all job data payloads. + */ +export type JobDataUnion = JobData; + +/** + * Agenda job type with EMA data typing. + */ +export type Job = AgendaJob>; + +/** + * Scheduler job identifier. + */ +export type JobId = string; + +/** + * Input data for scheduling a job. + */ +export interface JobSpec { + /** + * The job name used to resolve a handler. + */ + name: K; + /** + * When the job should run (Unix timestamp in milliseconds). + */ + runAt: number; + /** + * Handler-specific data. + */ + payload: JobData; +} + +/** + * Agenda-backed job handler signature. + */ +export type JobHandler = ( + job: Job, + done?: (error?: Error) => void, +) => Promise | void; + +/** + * Scheduler interface for managing job lifecycle. + */ +export interface Scheduler { + /** + * Starts the scheduler loop. + * @param handlers - Mapping of job names to their handlers. + * @returns Promise resolving when the scheduler is started. + */ + start(handlers: JobHandlerMap): Promise; + /** + * Stops the scheduler loop. + * @returns Promise resolving when the scheduler is stopped. + */ + stop(): Promise; + /** + * Schedules a job for execution. + * @param job - The job to schedule. + * @returns Promise resolving to the job id. + */ + schedule(job: JobSpec): Promise; + /** + * Reschedules an existing queued job with new runAt/data. + * @param id - The job identifier. + * @param job - The new job data. + * @returns Promise resolving to true if rescheduled, false otherwise. + */ + reschedule(id: JobId, job: JobSpec): Promise; + /** + * Cancels a pending job by id. + * @param id - The job identifier. + * @returns Promise resolving to true if canceled, false otherwise. + */ + cancel(id: JobId): Promise; +} + +/** + * Mapping of job names to their handlers. + */ +export type JobHandlerMap = { + [K in JobName]: JobHandler; +}; + +/** + * Runtime status of the scheduler. + */ +export type SchedulerStatus = "idle" | "running" | "stopping"; diff --git a/packages/ema/src/scheduler/index.ts b/packages/ema/src/scheduler/index.ts new file mode 100644 index 00000000..1483a8c2 --- /dev/null +++ b/packages/ema/src/scheduler/index.ts @@ -0,0 +1,3 @@ +export * from "./base"; +export * from "./jobs"; +export * from "./scheduler"; diff --git a/packages/ema/src/scheduler/jobs/index.ts b/packages/ema/src/scheduler/jobs/index.ts new file mode 100644 index 00000000..87ab7e3f --- /dev/null +++ b/packages/ema/src/scheduler/jobs/index.ts @@ -0,0 +1,23 @@ +/** + * Job data definitions and mappings. + */ + +import type { JobHandlerMap } from "../base"; +import { TestJobHandler, type TestJobData } from "./test.job"; + +/** + * Mapping from job name to its data schema. + */ +export interface JobDataMap { + /** + * Demo job data mapping. + */ + test: TestJobData; +} + +/** + * Mapping from job name to its handler implementation. + */ +export const jobHandlers = { + test: TestJobHandler, +} satisfies JobHandlerMap; diff --git a/packages/ema/src/scheduler/jobs/test.job.ts b/packages/ema/src/scheduler/jobs/test.job.ts new file mode 100644 index 00000000..374b97ba --- /dev/null +++ b/packages/ema/src/scheduler/jobs/test.job.ts @@ -0,0 +1,18 @@ +import type { JobHandler } from "../base"; + +/** + * Data shape for the demo job. + */ +export interface TestJobData { + /** + * The test message. + */ + message: string; +} + +/** + * Demo job handler implementation. + */ +export const TestJobHandler: JobHandler<"test"> = async (job) => { + console.log(`[scheduler:test] ${job.attrs.data.message}`); +}; diff --git a/packages/ema/src/scheduler/scheduler.ts b/packages/ema/src/scheduler/scheduler.ts new file mode 100644 index 00000000..e6de8ff5 --- /dev/null +++ b/packages/ema/src/scheduler/scheduler.ts @@ -0,0 +1,172 @@ +import { Agenda, type IAgendaConfig } from "@hokify/agenda"; +import type { Mongo } from "../db/mongo"; +import type { + Job, + JobHandler, + JobHandlerMap, + JobId, + JobName, + JobSpec, + Scheduler, + SchedulerStatus, +} from "./base"; + +/** + * Scheduler implementation backed by Agenda. + */ +export class AgendaScheduler implements Scheduler { + /** + * Collection name used by Agenda. + */ + readonly collectionName = "agenda"; + private status: SchedulerStatus = "idle"; + private readonly agenda: Agenda; + private readonly mongo: Mongo; + + /** + * Creates a new AgendaScheduler instance. + * @param mongo - MongoDB instance used to resolve the Agenda connection URI. + * @param config - Agenda configuration overrides. + */ + constructor(mongo: Mongo, config?: Partial) { + this.agenda = new Agenda(config); + this.mongo = mongo; + } + + /** + * Starts the scheduler loop. + * @param handlers - Mapping of job names to their handlers. + * @returns Promise resolving when the scheduler is started. + */ + async start(handlers: JobHandlerMap): Promise { + if (this.status !== "idle") { + return; + } + this.registerHandlers(handlers); + this.status = "running"; + + try { + await this.agenda.database(this.mongo.getUri(), this.collectionName); + await this.agenda.start(); + } catch (error) { + this.status = "idle"; + throw error; + } + } + + /** + * Stops the scheduler loop. + * @returns Promise resolving when the scheduler is stopped. + */ + async stop(): Promise { + if (this.status === "idle") { + return; + } + this.status = "stopping"; + + try { + await this.agenda.stop(); + } finally { + this.status = "idle"; + } + } + + /** + * Schedules a job for execution. + * @param job - The job to schedule. + * @returns Promise resolving to the job id. + */ + async schedule(job: JobSpec): Promise { + await this.ensureReady(); + const scheduled = await this.agenda.schedule( + new Date(job.runAt), + job.name, + job.payload, + ); + const id = scheduled.attrs._id?.toString(); + if (!id) { + throw new Error("Agenda job id is missing."); + } + return id; + } + + /** + * Reschedules an existing queued job with new runAt/payload. + * @param id - The job identifier. + * @param job - The new job data. + * @returns Promise resolving to true if rescheduled, false otherwise. + */ + async reschedule(id: JobId, job: JobSpec): Promise { + await this.ensureReady(); + const agendaJob = await this.loadJob(id); + if (!agendaJob) { + return false; + } + + const running = await this.isRunning(agendaJob); + if (running) { + return false; + } + + agendaJob.attrs.name = job.name; + agendaJob.attrs.data = job.payload; + agendaJob.schedule(new Date(job.runAt)); + await agendaJob.save(); + return true; + } + + /** + * Cancels a pending job by id. + * @param id - The job identifier. + * @returns Promise resolving to true if canceled, false otherwise. + */ + async cancel(id: JobId): Promise { + await this.ensureReady(); + const agendaJob = await this.loadJob(id); + if (!agendaJob) { + return false; + } + + const running = await this.isRunning(agendaJob); + if (running) { + return false; + } + + const removed = await agendaJob.remove(); + return removed > 0; + } + + private async ensureReady(): Promise { + if (this.status !== "running") { + throw new Error("Scheduler is not running."); + } + await this.agenda.ready; + } + + private registerHandlers(handlers: JobHandlerMap): void { + for (const name of Object.keys(handlers) as JobName[]) { + this.register(name, handlers[name]); + } + } + + private register(name: K, handler: JobHandler): void { + this.agenda.define(name, handler as (job: Job) => Promise | void); + } + + private async loadJob(id: JobId): Promise { + try { + const job = await this.agenda.getForkedJob(id); + return job as Job; + } catch { + return null; + } + } + + private async isRunning(job: Job): Promise { + try { + return await job.isRunning(); + } catch { + return false; + } + } +} diff --git a/packages/ema/src/server.ts b/packages/ema/src/server.ts index 683db45d..e4db2118 100644 --- a/packages/ema/src/server.ts +++ b/packages/ema/src/server.ts @@ -34,6 +34,8 @@ import type { Fs } from "./fs"; import { RealFs } from "./fs"; import * as path from "node:path"; import { ActorWorker } from "./actor"; +import { AgendaScheduler } from "./scheduler"; +import { jobHandlers } from "./scheduler/jobs"; /** * The server class for the EverMemoryArchive. @@ -60,6 +62,7 @@ export class Server { longTermMemoryDB!: LongTermMemoryDB & MongoCollectionGetter & IndexableDB; longTermMemoryVectorSearcher!: MongoMemorySearchAdaptor & MongoCollectionGetter; + scheduler!: AgendaScheduler; private constructor( private readonly fs: Fs, @@ -112,6 +115,8 @@ export class Server { server.longTermMemoryVectorSearcher.createIndices(), ]); + await server.scheduler.start(jobHandlers); + return server; } @@ -144,6 +149,7 @@ export class Server { server.longTermMemoryDB = new MongoLongTermMemoryDB(mongo, [ server.longTermMemoryVectorSearcher, ]); + server.scheduler = new AgendaScheduler(mongo); return server; } @@ -179,7 +185,7 @@ export class Server { this.longTermMemoryVectorSearcher, ]; const collections = new Set(dbs.flatMap((db) => db.collections)); - + collections.add(this.scheduler.collectionName); const snapshot = await this.mongo.snapshot(Array.from(collections)); await this.fs.write(fileName, JSON.stringify(snapshot, null, 1)); return { diff --git a/packages/ema/src/test_scheduler.ts b/packages/ema/src/test_scheduler.ts new file mode 100644 index 00000000..2a258914 --- /dev/null +++ b/packages/ema/src/test_scheduler.ts @@ -0,0 +1,46 @@ +import { createMongo } from "./db"; +import { AgendaScheduler, type JobHandlerMap } from "./scheduler"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function main(): Promise { + const mongo = await createMongo("", "ema_scheduler_demo", "memory"); + await mongo.connect(); + + const scheduler = new AgendaScheduler(mongo, { + processEvery: 100, + defaultConcurrency: 1, + maxConcurrency: 1, + defaultLockLimit: 1, + lockLimit: 1, + defaultLockLifetime: 10000, + }); + + const handlers: JobHandlerMap = { + test: async (job) => { + console.log(`[scheduler:test] ${job.attrs.data.message}`); + }, + }; + + try { + await scheduler.start(handlers); + + const jobId = await scheduler.schedule({ + name: "test", + runAt: Date.now() + 500, + payload: { message: "hello from test_scheduler" }, + }); + + console.log(`[scheduler] scheduled job ${jobId}`); + + await sleep(2000); + } finally { + await scheduler.stop(); + await mongo.close(); + } +} + +main().catch((error) => { + console.error("[scheduler] failed:", error); + process.exitCode = 1; +}); diff --git a/packages/ema/src/tests/scheduler.spec.ts b/packages/ema/src/tests/scheduler.spec.ts new file mode 100644 index 00000000..b79bbd64 --- /dev/null +++ b/packages/ema/src/tests/scheduler.spec.ts @@ -0,0 +1,143 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { ObjectId } from "mongodb"; + +import { createMongo } from "../db"; +import type { Mongo } from "../db"; +import { AgendaScheduler, type JobHandlerMap } from "../scheduler"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe("AgendaScheduler", () => { + let mongo: Mongo; + let scheduler: AgendaScheduler; + + beforeEach(async () => { + mongo = await createMongo("", "ema_scheduler_test", "memory"); + await mongo.connect(); + scheduler = new AgendaScheduler(mongo, { + processEvery: 20, + defaultConcurrency: 1, + maxConcurrency: 1, + defaultLockLimit: 1, + lockLimit: 1, + defaultLockLifetime: 1000, + }); + }); + + afterEach(async () => { + await scheduler.stop(); + await mongo.close(); + }); + + test("throws when scheduling before start", async () => { + await expect( + scheduler.schedule({ + name: "test", + runAt: Date.now() + 50, + payload: { message: "not-started" }, + }), + ).rejects.toThrow("Scheduler is not running."); + }); + + test("executes a scheduled job", async () => { + let resolveDone!: () => void; + const donePromise = new Promise((resolve) => { + resolveDone = resolve; + }); + let received: string | null = null; + + const handler = vi.fn(async (job) => { + received = job.attrs.data?.message ?? null; + resolveDone(); + }); + const handlers: JobHandlerMap = { test: handler }; + await scheduler.start(handlers); + + await scheduler.schedule({ + name: "test", + runAt: Date.now(), + payload: { message: "hello" }, + }); + + await Promise.race([ + donePromise, + sleep(5000).then(() => { + throw new Error("timeout"); + }), + ]); + + expect(received).toBe("hello"); + expect(handler).toHaveBeenCalledTimes(1); + }); + + test("cancels a pending job and removes it from the database", async () => { + const handler = vi.fn(async () => {}); + const handlers: JobHandlerMap = { test: handler }; + await scheduler.start(handlers); + + const jobId = await scheduler.schedule({ + name: "test", + runAt: Date.now() + 500, + payload: { message: "cancel" }, + }); + + const canceled = await scheduler.cancel(jobId); + expect(canceled).toBe(true); + + await sleep(200); + expect(handler).not.toHaveBeenCalled(); + + const collection = mongo.getDb().collection(scheduler.collectionName); + const doc = await collection.findOne({ _id: new ObjectId(jobId) }); + expect(doc).toBeNull(); + }); + + test("reschedules a job and updates its data", async () => { + let resolveDone!: () => void; + const donePromise = new Promise((resolve) => { + resolveDone = resolve; + }); + let received: string | null = null; + + const handler = vi.fn(async (job) => { + received = job.attrs.data.message; + resolveDone(); + }); + const handlers: JobHandlerMap = { test: handler }; + await scheduler.start(handlers); + + const jobId = await scheduler.schedule({ + name: "test", + runAt: Date.now() + 800, + payload: { message: "old" }, + }); + + const updated = await scheduler.reschedule(jobId, { + name: "test", + runAt: Date.now() + 50, + payload: { message: "new" }, + }); + expect(updated).toBe(true); + + await Promise.race([ + donePromise, + sleep(1000).then(() => { + throw new Error("timeout"); + }), + ]); + + expect(received).toBe("new"); + }); + + test("returns false when rescheduling a missing job", async () => { + const handlers: JobHandlerMap = { test: async () => {} }; + await scheduler.start(handlers); + + const updated = await scheduler.reschedule(new ObjectId().toString(), { + name: "test", + runAt: Date.now() + 50, + payload: { message: "missing" }, + }); + expect(updated).toBe(false); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7fd26b7..418545be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,9 @@ importers: '@google/genai': specifier: ^1.34.0 version: 1.34.0(@modelcontextprotocol/sdk@1.25.1(hono@4.11.1)(zod@4.2.1)) + '@hokify/agenda': + specifier: ^6.3.0 + version: 6.3.0 '@lancedb/lancedb': specifier: ^0.23.0 version: 0.23.0(apache-arrow@18.1.0) @@ -81,10 +84,10 @@ importers: version: 4.1.1 mongodb: specifier: ^7.0.0 - version: 7.0.0 + version: 7.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7) mongodb-memory-server: specifier: ^11.0.0 - version: 11.0.0 + version: 11.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7) openai: specifier: ^6.13.0 version: 6.13.0(ws@8.18.3)(zod@4.2.1) @@ -139,7 +142,7 @@ importers: version: link:../ema mongodb: specifier: ^7.0.0 - version: 7.0.0 + version: 7.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7) next: specifier: 16.0.10 version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -265,6 +268,135 @@ packages: '@ark/util@0.56.0': resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-cognito-identity@3.974.0': + resolution: {integrity: sha512-V3anrX+U5XgEaLVwZOJIDSXMY0Tk/i/KpjEENB70ovIiTYb0uNnSHWUMlepxVxAIhcvN8egGTEq6vgULIYBJFA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-sso@3.974.0': + resolution: {integrity: sha512-ci+GiM0c4ULo4D79UMcY06LcOLcfvUfiyt8PzNY0vbt5O8BfCPYf4QomwVgkNcLLCYmroO4ge2Yy1EsLUlcD6g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.0': + resolution: {integrity: sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-cognito-identity@3.972.1': + resolution: {integrity: sha512-/qAxlqkzW7UX9SUN+kFX1ZMHy0pFgwrfCjQYbVSmTOQvSprmzpNNj5RvVY/2WX78s5GMc6VQH/pwrOAxXbTiiA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.1': + resolution: {integrity: sha512-/etNHqnx96phy/SjI0HRC588o4vKH5F0xfkZ13yAATV7aNrb+5gYGNE6ePWafP+FuZ3HkULSSlJFj0AxgrAqYw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.1': + resolution: {integrity: sha512-AeopObGW5lpWbDRZ+t4EAtS7wdfSrHPLeFts7jaBzgIaCCD7TL7jAyAB9Y5bCLOPF+17+GL54djCCsjePljUAw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.1': + resolution: {integrity: sha512-OdbJA3v+XlNDsrYzNPRUwr8l7gw1r/nR8l4r96MDzSBDU8WEo8T6C06SvwaXR8SpzsjO3sq5KMP86wXWg7Rj4g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.1': + resolution: {integrity: sha512-CccqDGL6ZrF3/EFWZefvKW7QwwRdxlHUO8NVBKNVcNq6womrPDvqB6xc9icACtE0XB0a7PLoSTkAg8bQVkTO2w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.1': + resolution: {integrity: sha512-DwXPk9GfuU/xG9tmCyXFVkCr6X3W8ZCoL5Ptb0pbltEx1/LCcg7T+PBqDlPiiinNCD6ilIoMJDWsnJ8ikzZA7Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.1': + resolution: {integrity: sha512-bi47Zigu3692SJwdBvo8y1dEwE6B61stCwCFnuRWJVTfiM84B+VTSCV661CSWJmIZzmcy7J5J3kWyxL02iHj0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.1': + resolution: {integrity: sha512-dLZVNhM7wSgVUFsgVYgI5hb5Z/9PUkT46pk/SHrSmUqfx6YDvoV4YcPtaiRqviPpEGGiRtdQMEadyOKIRqulUQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.1': + resolution: {integrity: sha512-YMDeYgi0u687Ay0dAq/pFPKuijrlKTgsaB/UATbxCs/FzZfMiG4If5ksywHmmW7MiYUF8VVv+uou3TczvLrN4w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-providers@3.974.0': + resolution: {integrity: sha512-yVaoEcji7H0aXffvf5c50tqnXtaqR8n4DHsaH/hoNIbPplfYqdqLRtg106Jc7ir84YIy0SwTmyttwnzEAtGJsg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.1': + resolution: {integrity: sha512-/R82lXLPmZ9JaUGSUdKtBp2k/5xQxvBT3zZWyKiBOhyulFotlfvdlrO8TnqstBimsl4lYEYySDL+W6ldFh6ALg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.1': + resolution: {integrity: sha512-JGgFl6cHg9G2FHu4lyFIzmFN8KESBiRr84gLC3Aeni0Gt1nKm+KxWLBuha/RPcXxJygGXCcMM4AykkIwxor8RA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.1': + resolution: {integrity: sha512-taGzNRe8vPHjnliqXIHp9kBgIemLE/xCaRTMH1NH0cncHeaPcjxtnCroAAM9aOlPuKvBe2CpZESyvM1+D8oI7Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.1': + resolution: {integrity: sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.974.0': + resolution: {integrity: sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.1': + resolution: {integrity: sha512-voIY8RORpxLAEgEkYaTFnkaIuRwVBEc+RjVZYcSSllPV+ZEKAacai6kNhJeE3D70Le+JCfvRb52tng/AVHY+jQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.974.0': + resolution: {integrity: sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.972.0': + resolution: {integrity: sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.0': + resolution: {integrity: sha512-jYIdB7a7jhRTvyb378nsjyvJh1Si+zVduJ6urMNGpz8RjkmHZ+9vM2H07XaIB2Cfq0GhJRZYOfUCH8uqQhqBkQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.972.0': + resolution: {integrity: sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.3': + resolution: {integrity: sha512-FNUqAjlKAGA7GM05kywE99q8wiPHPZqrzhq3wXRga6PRD6A0kzT85Pb0AzYBVTBRpSrKyyr6M92Y6bnSBVp2BA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.1': + resolution: {integrity: sha512-IgF55NFmJX8d9Wql9M0nEpk2eYbuD8G4781FN4/fFgwTXBn86DvlZJuRWDCMcMqZymnBVX7HW9r+3r9ylqfW0w==} + + '@aws-sdk/util-user-agent-node@3.972.1': + resolution: {integrity: sha512-oIs4JFcADzoZ0c915R83XvK2HltWupxNsXUIuZse2rgk7b97zTpkxaqXiH0h9ylh31qtgo/t8hp4tIqcsMrEbQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.1': + resolution: {integrity: sha512-6zZGlPOqn7Xb+25MAXGb1JhgvaC5HjZj6GzszuVrnEgbhvzBRFGKYemuHBV4bho+dtqeYKPgaZUv7/e80hIGNg==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -712,6 +844,10 @@ packages: '@modelcontextprotocol/sdk': optional: true + '@hokify/agenda@6.3.0': + resolution: {integrity: sha512-fWrKMDe/8QHJXLOdEsMogb6cb213Z82iNsnU7nFrSIMFifEXSkXNTyCZ99FV3KLf+Du1gS/M9/8uTC6FHyWRZQ==} + engines: {node: '>=14.0.0'} + '@hono/node-server@1.19.7': resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==} engines: {node: '>=18.14.1'} @@ -1198,6 +1334,178 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@smithy/abort-controller@4.2.8': + resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.6': + resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.21.1': + resolution: {integrity: sha512-NUH8R4O6FkN8HKMojzbGg/5pNjsfTjlMmeFclyPfPaXXUrbr5TzhWgbf7t92wfrpCHRgpjyz7ffASIS3wX28aA==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.8': + resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.9': + resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.8': + resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.8': + resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.8': + resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.11': + resolution: {integrity: sha512-/WqsrycweGGfb9sSzME4CrsuayjJF6BueBmkKlcbeU5q18OhxRrvvKlmfw3tpDsK5ilx2XUJvoukwxHB0nHs/Q==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.27': + resolution: {integrity: sha512-xFUYCGRVsfgiN5EjsJJSzih9+yjStgMTCLANPlf0LVQkPDYCe0hz97qbdTZosFOiYlGBlHYityGRxrQ/hxhfVQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.9': + resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.8': + resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.8': + resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.8': + resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.8': + resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.8': + resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.8': + resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.8': + resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.8': + resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.3': + resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.8': + resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.10.12': + resolution: {integrity: sha512-VKO/HKoQ5OrSHW6AJUmEnUKeXI1/5LfCwO9cwyao7CmLvGnZeM1i36Lyful3LK1XU7HwTVieTqO1y2C/6t3qtA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.12.0': + resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.8': + resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.26': + resolution: {integrity: sha512-vva0dzYUTgn7DdE0uaha10uEdAgmdLnNFowKFjpMm6p2R0XDk5FHPX3CBJLzWQkQXuEprsb0hGz9YwbicNWhjw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.29': + resolution: {integrity: sha512-c6D7IUBsZt/aNnTBHMTf+OVh+h/JcxUUgfTcIJaWRe6zhOum1X+pNKSZtZ+7fbOn5I99XVFtmrnXKv8yHHErTQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.2.8': + resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.8': + resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.8': + resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.10': + resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} + engines: {node: '>=18.0.0'} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1285,6 +1593,9 @@ packages: '@types/whatwg-url@13.0.0': resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} + '@types/whatwg-url@8.2.2': + resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} + '@typescript-eslint/eslint-plugin@8.50.0': resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1771,6 +2082,9 @@ packages: resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} engines: {node: '>=18'} + bowser@2.13.1: + resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1789,6 +2103,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bson@4.7.2: + resolution: {integrity: sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==} + engines: {node: '>=6.9.0'} + bson@7.0.0: resolution: {integrity: sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==} engines: {node: '>=20.19.0'} @@ -1802,6 +2120,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1930,6 +2251,10 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1970,12 +2295,23 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date.js@0.3.3: + resolution: {integrity: sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2335,6 +2671,10 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -2587,6 +2927,9 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-interval@2.0.1: + resolution: {integrity: sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2595,6 +2938,9 @@ packages: resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2618,6 +2964,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2881,6 +3231,10 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2978,6 +3332,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mongodb-connection-string-url@2.6.0: + resolution: {integrity: sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==} + mongodb-connection-string-url@7.0.0: resolution: {integrity: sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==} engines: {node: '>=20.19.0'} @@ -2990,6 +3347,10 @@ packages: resolution: {integrity: sha512-hx43BuFV4G48ghJBsS7BYqtCZ/UGQgEqu6V0/rUCfdYKYV2WpcYGHcXtFBYR0HxmlinpBvZAnRFS8IrqsU0L3Q==} engines: {node: '>=20.19.0'} + mongodb@4.17.2: + resolution: {integrity: sha512-mLV7SEiov2LHleRJPMPrK2PMyhXFZt2UQLC4VD4pnth3jMjYKHhtqfwwkkvS/NXuo/Fp3vbhaNcXrIDaLRb9Tg==} + engines: {node: '>=12.9.0'} + mongodb@7.0.0: resolution: {integrity: sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==} engines: {node: '>=20.19.0'} @@ -3017,6 +3378,9 @@ packages: socks: optional: true + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3074,6 +3438,9 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + numbered@1.1.0: + resolution: {integrity: sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==} + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -3503,6 +3870,14 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} @@ -3612,6 +3987,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.1.2: + resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -3707,6 +4085,10 @@ packages: resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} engines: {node: '>=8'} + tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} @@ -4030,6 +4412,10 @@ packages: whatwg-mimetype@2.3.0: resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} @@ -4245,30 +4631,469 @@ snapshots: '@algolia/requester-fetch': 5.46.2 '@algolia/requester-node-http': 5.46.2 - '@algolia/recommend@5.46.2': + '@algolia/recommend@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + '@algolia/requester-browser-xhr': 5.46.2 + '@algolia/requester-fetch': 5.46.2 + '@algolia/requester-node-http': 5.46.2 + + '@algolia/requester-browser-xhr@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@algolia/requester-fetch@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@algolia/requester-node-http@5.46.2': + dependencies: + '@algolia/client-common': 5.46.2 + + '@ark/schema@0.56.0': + dependencies: + '@ark/util': 0.56.0 + + '@ark/util@0.56.0': {} + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.0 + '@aws-sdk/util-locate-window': 3.965.3 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + optional: true + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.0 + tslib: 2.8.1 + optional: true + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/client-cognito-identity@3.974.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.0 + '@aws-sdk/credential-provider-node': 3.972.1 + '@aws-sdk/middleware-host-header': 3.972.1 + '@aws-sdk/middleware-logger': 3.972.1 + '@aws-sdk/middleware-recursion-detection': 3.972.1 + '@aws-sdk/middleware-user-agent': 3.972.1 + '@aws-sdk/region-config-resolver': 3.972.1 + '@aws-sdk/types': 3.973.0 + '@aws-sdk/util-endpoints': 3.972.0 + '@aws-sdk/util-user-agent-browser': 3.972.1 + '@aws-sdk/util-user-agent-node': 3.972.1 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.21.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.11 + '@smithy/middleware-retry': 4.4.27 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.12 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.26 + '@smithy/util-defaults-mode-node': 4.2.29 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/client-sso@3.974.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.0 + '@aws-sdk/middleware-host-header': 3.972.1 + '@aws-sdk/middleware-logger': 3.972.1 + '@aws-sdk/middleware-recursion-detection': 3.972.1 + '@aws-sdk/middleware-user-agent': 3.972.1 + '@aws-sdk/region-config-resolver': 3.972.1 + '@aws-sdk/types': 3.973.0 + '@aws-sdk/util-endpoints': 3.972.0 + '@aws-sdk/util-user-agent-browser': 3.972.1 + '@aws-sdk/util-user-agent-node': 3.972.1 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.21.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.11 + '@smithy/middleware-retry': 4.4.27 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.12 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.26 + '@smithy/util-defaults-mode-node': 4.2.29 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/core@3.973.0': + dependencies: + '@aws-sdk/types': 3.973.0 + '@aws-sdk/xml-builder': 3.972.1 + '@smithy/core': 3.21.1 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.12 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-cognito-identity@3.972.1': + dependencies: + '@aws-sdk/client-cognito-identity': 3.974.0 + '@aws-sdk/types': 3.973.0 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/credential-provider-env@3.972.1': + dependencies: + '@aws-sdk/core': 3.973.0 + '@aws-sdk/types': 3.973.0 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-http@3.972.1': + dependencies: + '@aws-sdk/core': 3.973.0 + '@aws-sdk/types': 3.973.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.12 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-ini@3.972.1': + dependencies: + '@aws-sdk/core': 3.973.0 + '@aws-sdk/credential-provider-env': 3.972.1 + '@aws-sdk/credential-provider-http': 3.972.1 + '@aws-sdk/credential-provider-login': 3.972.1 + '@aws-sdk/credential-provider-process': 3.972.1 + '@aws-sdk/credential-provider-sso': 3.972.1 + '@aws-sdk/credential-provider-web-identity': 3.972.1 + '@aws-sdk/nested-clients': 3.974.0 + '@aws-sdk/types': 3.973.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/credential-provider-login@3.972.1': + dependencies: + '@aws-sdk/core': 3.973.0 + '@aws-sdk/nested-clients': 3.974.0 + '@aws-sdk/types': 3.973.0 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/credential-provider-node@3.972.1': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.1 + '@aws-sdk/credential-provider-http': 3.972.1 + '@aws-sdk/credential-provider-ini': 3.972.1 + '@aws-sdk/credential-provider-process': 3.972.1 + '@aws-sdk/credential-provider-sso': 3.972.1 + '@aws-sdk/credential-provider-web-identity': 3.972.1 + '@aws-sdk/types': 3.973.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/credential-provider-process@3.972.1': + dependencies: + '@aws-sdk/core': 3.973.0 + '@aws-sdk/types': 3.973.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/credential-provider-sso@3.972.1': + dependencies: + '@aws-sdk/client-sso': 3.974.0 + '@aws-sdk/core': 3.973.0 + '@aws-sdk/token-providers': 3.974.0 + '@aws-sdk/types': 3.973.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/credential-provider-web-identity@3.972.1': + dependencies: + '@aws-sdk/core': 3.973.0 + '@aws-sdk/nested-clients': 3.974.0 + '@aws-sdk/types': 3.973.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/credential-providers@3.974.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.974.0 + '@aws-sdk/core': 3.973.0 + '@aws-sdk/credential-provider-cognito-identity': 3.972.1 + '@aws-sdk/credential-provider-env': 3.972.1 + '@aws-sdk/credential-provider-http': 3.972.1 + '@aws-sdk/credential-provider-ini': 3.972.1 + '@aws-sdk/credential-provider-login': 3.972.1 + '@aws-sdk/credential-provider-node': 3.972.1 + '@aws-sdk/credential-provider-process': 3.972.1 + '@aws-sdk/credential-provider-sso': 3.972.1 + '@aws-sdk/credential-provider-web-identity': 3.972.1 + '@aws-sdk/nested-clients': 3.974.0 + '@aws-sdk/types': 3.973.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.21.1 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/middleware-host-header@3.972.1': + dependencies: + '@aws-sdk/types': 3.973.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/middleware-logger@3.972.1': + dependencies: + '@aws-sdk/types': 3.973.0 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/middleware-recursion-detection@3.972.1': + dependencies: + '@aws-sdk/types': 3.973.0 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/middleware-user-agent@3.972.1': + dependencies: + '@aws-sdk/core': 3.973.0 + '@aws-sdk/types': 3.973.0 + '@aws-sdk/util-endpoints': 3.972.0 + '@smithy/core': 3.21.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/nested-clients@3.974.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.0 + '@aws-sdk/middleware-host-header': 3.972.1 + '@aws-sdk/middleware-logger': 3.972.1 + '@aws-sdk/middleware-recursion-detection': 3.972.1 + '@aws-sdk/middleware-user-agent': 3.972.1 + '@aws-sdk/region-config-resolver': 3.972.1 + '@aws-sdk/types': 3.973.0 + '@aws-sdk/util-endpoints': 3.972.0 + '@aws-sdk/util-user-agent-browser': 3.972.1 + '@aws-sdk/util-user-agent-node': 3.972.1 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.21.1 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.11 + '@smithy/middleware-retry': 4.4.27 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.12 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.26 + '@smithy/util-defaults-mode-node': 4.2.29 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/region-config-resolver@3.972.1': + dependencies: + '@aws-sdk/types': 3.973.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/token-providers@3.974.0': + dependencies: + '@aws-sdk/core': 3.973.0 + '@aws-sdk/nested-clients': 3.974.0 + '@aws-sdk/types': 3.973.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + optional: true + + '@aws-sdk/types@3.972.0': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@aws-sdk/types@3.973.0': dependencies: - '@algolia/client-common': 5.46.2 - '@algolia/requester-browser-xhr': 5.46.2 - '@algolia/requester-fetch': 5.46.2 - '@algolia/requester-node-http': 5.46.2 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true - '@algolia/requester-browser-xhr@5.46.2': + '@aws-sdk/util-endpoints@3.972.0': dependencies: - '@algolia/client-common': 5.46.2 + '@aws-sdk/types': 3.972.0 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 + tslib: 2.8.1 + optional: true - '@algolia/requester-fetch@5.46.2': + '@aws-sdk/util-locate-window@3.965.3': dependencies: - '@algolia/client-common': 5.46.2 + tslib: 2.8.1 + optional: true - '@algolia/requester-node-http@5.46.2': + '@aws-sdk/util-user-agent-browser@3.972.1': dependencies: - '@algolia/client-common': 5.46.2 + '@aws-sdk/types': 3.973.0 + '@smithy/types': 4.12.0 + bowser: 2.13.1 + tslib: 2.8.1 + optional: true - '@ark/schema@0.56.0': + '@aws-sdk/util-user-agent-node@3.972.1': dependencies: - '@ark/util': 0.56.0 + '@aws-sdk/middleware-user-agent': 3.972.1 + '@aws-sdk/types': 3.973.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true - '@ark/util@0.56.0': {} + '@aws-sdk/xml-builder@3.972.1': + dependencies: + '@smithy/types': 4.12.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + optional: true + + '@aws/lambda-invoke-store@0.2.3': + optional: true '@babel/code-frame@7.27.1': dependencies: @@ -4626,6 +5451,18 @@ snapshots: - supports-color - utf-8-validate + '@hokify/agenda@6.3.0': + dependencies: + cron-parser: 4.9.0 + date.js: 0.3.3 + debug: 4.4.3 + human-interval: 2.0.1 + luxon: 3.7.2 + mongodb: 4.17.2 + transitivePeerDependencies: + - aws-crt + - supports-color + '@hono/node-server@1.19.7(hono@4.11.1)': dependencies: hono: 4.11.1 @@ -5027,6 +5864,323 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@smithy/abort-controller@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/config-resolver@4.4.6': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + optional: true + + '@smithy/core@3.21.1': + dependencies: + '@smithy/middleware-serde': 4.2.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + optional: true + + '@smithy/credential-provider-imds@4.2.8': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + tslib: 2.8.1 + optional: true + + '@smithy/fetch-http-handler@5.3.9': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + tslib: 2.8.1 + optional: true + + '@smithy/hash-node@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/invalid-dependency@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/is-array-buffer@4.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/middleware-content-length@4.2.8': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/middleware-endpoint@4.4.11': + dependencies: + '@smithy/core': 3.21.1 + '@smithy/middleware-serde': 4.2.9 + '@smithy/node-config-provider': 4.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-middleware': 4.2.8 + tslib: 2.8.1 + optional: true + + '@smithy/middleware-retry@4.4.27': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/service-error-classification': 4.2.8 + '@smithy/smithy-client': 4.10.12 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/uuid': 1.1.0 + tslib: 2.8.1 + optional: true + + '@smithy/middleware-serde@4.2.9': + dependencies: + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/middleware-stack@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/node-config-provider@4.3.8': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/node-http-handler@4.4.8': + dependencies: + '@smithy/abort-controller': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/property-provider@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/protocol-http@5.3.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/querystring-builder@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + '@smithy/util-uri-escape': 4.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/querystring-parser@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/service-error-classification@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + optional: true + + '@smithy/shared-ini-file-loader@4.4.3': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/signature-v4@5.3.8': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/smithy-client@4.10.12': + dependencies: + '@smithy/core': 3.21.1 + '@smithy/middleware-endpoint': 4.4.11 + '@smithy/middleware-stack': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 + tslib: 2.8.1 + optional: true + + '@smithy/types@4.12.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/url-parser@4.2.8': + dependencies: + '@smithy/querystring-parser': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-base64@4.3.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-body-length-browser@4.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-body-length-node@4.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-buffer-from@4.2.0': + dependencies: + '@smithy/is-array-buffer': 4.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-config-provider@4.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-defaults-mode-browser@4.3.26': + dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.10.12 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-defaults-mode-node@4.2.29': + dependencies: + '@smithy/config-resolver': 4.4.6 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.10.12 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-endpoints@3.2.8': + dependencies: + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-hex-encoding@4.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-middleware@4.2.8': + dependencies: + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-retry@4.2.8': + dependencies: + '@smithy/service-error-classification': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-stream@4.5.10': + dependencies: + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-uri-escape@4.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/util-utf8@4.2.0': + dependencies: + '@smithy/util-buffer-from': 4.2.0 + tslib: 2.8.1 + optional: true + + '@smithy/uuid@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -5107,6 +6261,11 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 + '@types/whatwg-url@8.2.2': + dependencies: + '@types/node': 20.19.27 + '@types/webidl-conversions': 7.0.3 + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5639,6 +6798,9 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@2.13.1: + optional: true + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5663,6 +6825,10 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.2(browserslist@4.28.1) + bson@4.7.2: + dependencies: + buffer: 5.7.1 + bson@7.0.0: {} buffer-crc32@0.2.13: {} @@ -5672,6 +6838,11 @@ snapshots: buffer-from@1.1.2: optional: true + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bytes@3.1.2: {} cac@6.7.14: {} @@ -5787,6 +6958,10 @@ snapshots: create-require@1.1.1: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5835,10 +7010,20 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date.js@0.3.3: + dependencies: + debug: 3.1.0 + transitivePeerDependencies: + - supports-color + dateformat@4.6.3: {} dayjs@1.11.19: {} + debug@3.1.0: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -6106,7 +7291,7 @@ snapshots: eslint: 9.39.2(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@1.21.7)) @@ -6139,7 +7324,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -6154,7 +7339,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -6389,6 +7574,11 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.2 + optional: true + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -6691,6 +7881,10 @@ snapshots: transitivePeerDependencies: - supports-color + human-interval@2.0.1: + dependencies: + numbered: 1.1.0 + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -6700,6 +7894,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -6719,6 +7915,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.1.0: {} + ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: @@ -7005,6 +8203,8 @@ snapshots: lunr@2.3.9: {} + luxon@3.7.2: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7102,12 +8302,17 @@ snapshots: mitt@3.0.1: {} + mongodb-connection-string-url@2.6.0: + dependencies: + '@types/whatwg-url': 8.2.2 + whatwg-url: 11.0.0 + mongodb-connection-string-url@7.0.0: dependencies: '@types/whatwg-url': 13.0.0 whatwg-url: 14.2.0 - mongodb-memory-server-core@11.0.0: + mongodb-memory-server-core@11.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7): dependencies: async-mutex: 0.5.0 camelcase: 6.3.0 @@ -7115,7 +8320,7 @@ snapshots: find-cache-dir: 3.3.2 follow-redirects: 1.15.11(debug@4.4.3) https-proxy-agent: 7.0.6 - mongodb: 7.0.0 + mongodb: 7.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7) new-find-package-json: 2.0.0 semver: 7.7.3 tar-stream: 3.1.7 @@ -7133,9 +8338,9 @@ snapshots: - socks - supports-color - mongodb-memory-server@11.0.0: + mongodb-memory-server@11.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7): dependencies: - mongodb-memory-server-core: 11.0.0 + mongodb-memory-server-core: 11.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7) tslib: 2.8.1 transitivePeerDependencies: - '@aws-sdk/credential-providers' @@ -7149,11 +8354,27 @@ snapshots: - socks - supports-color - mongodb@7.0.0: + mongodb@4.17.2: + dependencies: + bson: 4.7.2 + mongodb-connection-string-url: 2.6.0 + socks: 2.8.7 + optionalDependencies: + '@aws-sdk/credential-providers': 3.974.0 + '@mongodb-js/saslprep': 1.4.4 + transitivePeerDependencies: + - aws-crt + + mongodb@7.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7): dependencies: '@mongodb-js/saslprep': 1.4.4 bson: 7.0.0 mongodb-connection-string-url: 7.0.0 + optionalDependencies: + '@aws-sdk/credential-providers': 3.974.0 + socks: 2.8.7 + + ms@2.0.0: {} ms@2.1.3: {} @@ -7204,6 +8425,8 @@ snapshots: node-releases@2.0.27: {} + numbered@1.1.0: {} + nwsapi@2.2.23: optional: true @@ -7740,6 +8963,13 @@ snapshots: signal-exit@4.1.0: {} + smart-buffer@4.2.0: {} + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 @@ -7878,6 +9108,9 @@ snapshots: dependencies: js-tokens: 9.0.1 + strnum@2.1.2: + optional: true + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.1): dependencies: client-only: 0.0.1 @@ -7972,6 +9205,10 @@ snapshots: punycode: 2.3.1 optional: true + tr46@3.0.0: + dependencies: + punycode: 2.3.1 + tr46@5.1.1: dependencies: punycode: 2.3.1 @@ -8366,6 +9603,11 @@ snapshots: whatwg-mimetype@2.3.0: optional: true + whatwg-url@11.0.0: + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + whatwg-url@14.2.0: dependencies: tr46: 5.1.1 From c1d75e30b3f95dd832f979b876cb2f5f29851861 Mon Sep 17 00:00:00 2001 From: Disviel Date: Mon, 26 Jan 2026 17:13:18 +0800 Subject: [PATCH 02/16] feat: modified the tool call interface to achieve context injection --- packages/ema/src/actor.ts | 14 + packages/ema/src/agent.ts | 14 +- packages/ema/src/skills/base.ts | 6 +- packages/ema/src/skills/demo-skill/index.ts | 8 +- .../src/tests/tools/ema_reply_tool.spec.ts | 33 +- .../src/tests/tools/exec_skill_tool.spec.ts | 17 +- .../src/tests/tools/get_skill_tool.spec.ts | 10 +- packages/ema/src/tools/base.ts | 14 +- packages/ema/src/tools/ema_reply_tool.ts | 22 +- packages/ema/src/tools/exec_skill_tool.ts | 16 +- packages/ema/src/tools/get_skill_tool.ts | 11 +- .../ema/src/tools/mini_agent/bash_tool.ts | 688 ------------------ .../ema/src/tools/mini_agent/file_tools.ts | 336 --------- .../ema/src/tools/mini_agent/mcp_loader.ts | 273 ------- .../ema/src/tools/mini_agent/note_tool.ts | 237 ------ .../ema/src/tools/mini_agent/skill_loader.ts | 292 -------- .../ema/src/tools/mini_agent/skill_tool.ts | 87 --- 17 files changed, 103 insertions(+), 1975 deletions(-) delete mode 100644 packages/ema/src/tools/mini_agent/bash_tool.ts delete mode 100644 packages/ema/src/tools/mini_agent/file_tools.ts delete mode 100644 packages/ema/src/tools/mini_agent/mcp_loader.ts delete mode 100644 packages/ema/src/tools/mini_agent/note_tool.ts delete mode 100644 packages/ema/src/tools/mini_agent/skill_loader.ts delete mode 100644 packages/ema/src/tools/mini_agent/skill_tool.ts diff --git a/packages/ema/src/actor.ts b/packages/ema/src/actor.ts index 9bde35fd..383783f7 100644 --- a/packages/ema/src/actor.ts +++ b/packages/ema/src/actor.ts @@ -29,6 +29,13 @@ import type { Content } from "./schema"; import { LLMClient } from "./llm"; import { type AgentState } from "./agent"; +/** The scope information for the actor. */ +export interface ActorScope { + actorId: number; + userId: number; + conversationId?: number; +} + /** * A facade of the actor functionalities between the server (system) and the agent (actor). */ @@ -293,6 +300,13 @@ export class ActorWorker implements ActorStateStorage, ActorMemory { ), messages: batches.map((item) => bufferMessageToUserMessage(item)), tools: this.config.baseTools, + toolContext: { + actorScope: { + actorId: this.actorId, + userId: this.userId, + conversationId: this.conversationId, + }, + }, }; } this.resumeStateAfterAbort = false; diff --git a/packages/ema/src/agent.ts b/packages/ema/src/agent.ts index 4740f9f7..816cf939 100644 --- a/packages/ema/src/agent.ts +++ b/packages/ema/src/agent.ts @@ -11,7 +11,7 @@ import { isToolMessage, isUserMessage, } from "./schema"; -import type { Tool, ToolResult } from "./tools/base"; +import type { Tool, ToolResult, ToolContext } from "./tools/base"; import type { EmaReply } from "./tools/ema_reply_tool"; /** Event emitted when the agent finishes a run. */ @@ -71,6 +71,7 @@ export type AgentState = { systemPrompt: string; messages: Message[]; tools: Tool[]; + toolContext?: ToolContext; }; /** Callback type for running the agent with a given state. */ @@ -314,13 +315,10 @@ export class Agent { }; } else { try { - const props = ( - tool.parameters as { properties?: Record } - ).properties; - const positionalArgs = props - ? Object.keys(props).map((key) => callArgs[key]) - : Object.values(callArgs); - result = await tool.execute(...positionalArgs); + result = await tool.execute( + callArgs, + this.contextManager.state.toolContext, + ); } catch (err) { const errorDetail = `${(err as Error).name}: ${(err as Error).message}`; const errorTrace = (err as Error).stack ?? ""; diff --git a/packages/ema/src/skills/base.ts b/packages/ema/src/skills/base.ts index 84e032f8..3e93b35d 100644 --- a/packages/ema/src/skills/base.ts +++ b/packages/ema/src/skills/base.ts @@ -1,7 +1,8 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import type { ToolResult } from "../tools/base"; +import type { ToolResult, ToolContext } from "../tools/base"; +import type { ActorScope } from "../actor"; /** Skill name -> Skill instance registry. */ export type SkillRegistry = Record; @@ -31,8 +32,9 @@ export abstract class Skill { /** * Executes the skill. * @param args - Arguments object that should satisfy `parameters`. + * @param context - Optional tool context (e.g. actor scope). */ - abstract execute(...args: any[]): Promise; + abstract execute(args: any, context?: ToolContext): Promise; /** Returns minimal metadata used for listing in prompts/UI. */ get metadata(): Record { diff --git a/packages/ema/src/skills/demo-skill/index.ts b/packages/ema/src/skills/demo-skill/index.ts index 72aceb7f..01767e53 100644 --- a/packages/ema/src/skills/demo-skill/index.ts +++ b/packages/ema/src/skills/demo-skill/index.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import dayjs from "dayjs"; import { Skill } from "../base"; -import type { ToolResult } from "../../tools/base"; +import type { ToolResult, ToolContext } from "../../tools/base"; //TODO: Use arktype in future const DemoSkillSchema = z @@ -37,9 +37,11 @@ export default class DemoSkill extends Skill { * - Validates args with zod * - Supports #time and #echo commands * - Returns localized error messages for invalid inputs + * @param args - Skill arguments. + * @param context - Optional tool context (unused). */ - async execute(args: { input: string }): Promise { - let payload: { input: string }; + async execute(args: any, context?: ToolContext): Promise { + let payload: z.infer; try { payload = DemoSkillSchema.parse(args); } catch (err) { diff --git a/packages/ema/src/tests/tools/ema_reply_tool.spec.ts b/packages/ema/src/tests/tools/ema_reply_tool.spec.ts index 473c68f7..44ddc825 100644 --- a/packages/ema/src/tests/tools/ema_reply_tool.spec.ts +++ b/packages/ema/src/tests/tools/ema_reply_tool.spec.ts @@ -27,12 +27,12 @@ describe("EmaReplyTool", () => { }); it("should execute successfully with valid inputs", async () => { - const result = await tool.execute( - " 我应该回复用户 ", - "微笑", - "点头", - " 你好,很高兴见到你 ", - ); + const result = await tool.execute({ + think: " 我应该回复用户 ", + expression: "微笑", + action: "点头", + response: " 你好,很高兴见到你 ", + }); expect(result.success).toBe(true); expect(result.content).toBeTruthy(); @@ -45,21 +45,36 @@ describe("EmaReplyTool", () => { }); it("should reject invalid expression enum values", async () => { - const result = await tool.execute("想法", "生气", "无", "回复"); + const result = await tool.execute({ + think: "想法", + expression: "生气", + action: "无", + response: "回复", + }); expect(result.success).toBe(false); expect(result.error).toContain("Invalid structured reply"); }); it("should reject invalid action enum values", async () => { - const result = await tool.execute("想法", "普通", "跳舞", "回复"); + const result = await tool.execute({ + think: "想法", + expression: "普通", + action: "跳舞", + response: "回复", + }); expect(result.success).toBe(false); expect(result.error).toContain("Invalid structured reply"); }); it("should reject empty strings", async () => { - const result = await tool.execute("", "普通", "无", ""); + const result = await tool.execute({ + think: "", + expression: "普通", + action: "无", + response: "", + }); expect(result.success).toBe(false); expect(result.error).toContain("Invalid structured reply"); diff --git a/packages/ema/src/tests/tools/exec_skill_tool.spec.ts b/packages/ema/src/tests/tools/exec_skill_tool.spec.ts index fb02683c..4679a4f5 100644 --- a/packages/ema/src/tests/tools/exec_skill_tool.spec.ts +++ b/packages/ema/src/tests/tools/exec_skill_tool.spec.ts @@ -23,14 +23,14 @@ class StubSkill extends Skill { describe("ExeSkillTool", () => { it("fails when skill missing", async () => { const tool = new ExecSkillTool({}); - const res = await tool.execute("missing", {}); + const res = await tool.execute({ skill_name: "missing" }); expect(res.success).toBe(false); expect(res.error).toMatch(/does not exist/); }); it("validates input schema", async () => { const tool = new ExecSkillTool({}); - const res = await tool.execute("", {}); + const res = await tool.execute({ skill_name: "" }); expect(res.success).toBe(false); expect(res.error).toMatch(/Invalid exe_skill_tool input/); }); @@ -42,7 +42,7 @@ describe("ExeSkillTool", () => { const registry = { stub: new StubSkill(execSpy) }; const tool = new ExecSkillTool(registry); const payload = { a: 1, b: "x" }; - const res = await tool.execute("stub", payload); + const res = await tool.execute({ skill_name: "stub", skill_args: payload }); expect(execSpy).toHaveBeenCalledWith(payload); expect(res.success).toBe(true); expect(res.content).toBe(JSON.stringify(payload)); @@ -57,7 +57,7 @@ describe("ExeSkillTool", () => { }); const registry = { stub: new StubSkill(execSpy) }; const tool = new ExecSkillTool(registry); - const res = await tool.execute("stub"); + const res = await tool.execute({ skill_name: "stub" }); expect(execSpy).toHaveBeenCalledWith(undefined); expect(res.success).toBe(true); expect(res.content).toBe("no-args"); @@ -69,7 +69,10 @@ describe("ExeSkillTool", () => { }); const registry = { stub: new StubSkill(execSpy) }; const tool = new ExecSkillTool(registry); - const res = await tool.execute("stub", { any: "thing" }); + const res = await tool.execute({ + skill_name: "stub", + skill_args: { any: "thing" }, + }); expect(execSpy).toHaveBeenCalled(); expect(res.success).toBe(false); expect(res.error).toBe("boom"); @@ -81,6 +84,8 @@ describe("ExeSkillTool", () => { }); const registry = { stub: new StubSkill(execSpy) }; const tool = new ExecSkillTool(registry); - await expect(tool.execute("stub")).rejects.toThrow("explode"); + await expect(tool.execute({ skill_name: "stub" })).rejects.toThrow( + "explode", + ); }); }); diff --git a/packages/ema/src/tests/tools/get_skill_tool.spec.ts b/packages/ema/src/tests/tools/get_skill_tool.spec.ts index ec511740..8705cd42 100644 --- a/packages/ema/src/tests/tools/get_skill_tool.spec.ts +++ b/packages/ema/src/tests/tools/get_skill_tool.spec.ts @@ -27,14 +27,14 @@ describe("GetSkillTool", () => { it("returns playbook when skill exists", async () => { const registry = { stub: new StubSkill("playbook") }; const tool = new GetSkillTool(registry); - const res = await tool.execute("stub"); + const res = await tool.execute({ skill_name: "stub" }); expect(res.success).toBe(true); expect(res.content).toBe("playbook"); }); it("fails when skill missing", async () => { const tool = new GetSkillTool({}); - const res = await tool.execute("missing"); + const res = await tool.execute({ skill_name: "missing" }); expect(res.success).toBe(false); expect(res.error).toMatch(/does not exist/); }); @@ -42,7 +42,7 @@ describe("GetSkillTool", () => { it("validates input schema", async () => { const registry = { stub: new StubSkill("playbook") }; const tool = new GetSkillTool(registry); - const res = await tool.execute(""); + const res = await tool.execute({ skill_name: "" }); expect(res.success).toBe(false); expect(res.error).toMatch(/Invalid get_skill_tool input/); }); @@ -50,7 +50,7 @@ describe("GetSkillTool", () => { it("validates non-string input", async () => { const registry = { stub: new StubSkill("playbook") }; const tool = new GetSkillTool(registry); - const res = await tool.execute(123 as any); + const res = await tool.execute({ skill_name: 123 as any }); expect(res.success).toBe(false); expect(res.error).toMatch(/Invalid get_skill_tool input/); }); @@ -64,7 +64,7 @@ describe("GetSkillTool", () => { } const registry = { spy: new SpySkill("doc", "spy") }; const tool = new GetSkillTool(registry); - const res = await tool.execute("spy"); + const res = await tool.execute({ skill_name: "spy" }); expect(res.success).toBe(true); expect(res.content).toBe("doc"); expect(getPlaybook).toHaveBeenCalledTimes(1); diff --git a/packages/ema/src/tools/base.ts b/packages/ema/src/tools/base.ts index 997184a9..c5c37236 100644 --- a/packages/ema/src/tools/base.ts +++ b/packages/ema/src/tools/base.ts @@ -1,3 +1,5 @@ +import type { ActorScope } from "../actor"; + /** Tool execution result. */ export interface ToolResult { success: boolean; @@ -5,6 +7,10 @@ export interface ToolResult { error?: string; } +export interface ToolContext { + actorScope?: ActorScope; +} + /** Base class for all tools. */ export abstract class Tool { /** Returns the tool name. */ @@ -16,6 +22,10 @@ export abstract class Tool { /** Returns the tool parameters schema (JSON Schema format). */ abstract parameters: Record; - /** Executes the tool with arbitrary arguments. */ - abstract execute(...args: any[]): Promise; + /** + * Executes the tool with arbitrary arguments. + * @param args - Tool-specific arguments. + * @param context - Optional tool context (e.g. actor scope). + */ + abstract execute(args: any, context?: ToolContext): Promise; } diff --git a/packages/ema/src/tools/ema_reply_tool.ts b/packages/ema/src/tools/ema_reply_tool.ts index 6661ddce..cdef7b26 100644 --- a/packages/ema/src/tools/ema_reply_tool.ts +++ b/packages/ema/src/tools/ema_reply_tool.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { Tool } from "./base"; -import type { ToolResult } from "./base"; +import type { ToolResult, ToolContext } from "./base"; const EmaReplySchema = z .object({ @@ -37,20 +37,14 @@ export class EmaReplyTool extends Tool { /** Returns the JSON Schema specifying the expected arguments. */ parameters = EmaReplySchema.toJSONSchema(); - /** Validates and emits a structured reply payload. */ - async execute( - think: string, - expression: string, - action: string, - response: string, - ): Promise { + /** + * Validates and emits a structured reply payload. + * @param args - Tool arguments matching the reply schema. + * @param context - Optional tool context (unused). + */ + async execute(args: any, context?: ToolContext): Promise { try { - const payload = EmaReplySchema.parse({ - think, - expression, - action, - response, - }); + const payload = EmaReplySchema.parse(args); return { success: true, content: JSON.stringify(payload), diff --git a/packages/ema/src/tools/exec_skill_tool.ts b/packages/ema/src/tools/exec_skill_tool.ts index 40dc66f3..e5fc53c6 100644 --- a/packages/ema/src/tools/exec_skill_tool.ts +++ b/packages/ema/src/tools/exec_skill_tool.ts @@ -1,12 +1,12 @@ import { z } from "zod"; import { Tool } from "./base"; -import type { ToolResult } from "./base"; +import type { ToolResult, ToolContext } from "./base"; import { type SkillRegistry } from "../skills"; const ExeSkillSchema = z .object({ skill_name: z.string().min(1).describe("需要执行的 skill 名称"), - args: z.any().optional().describe("传给 skill.execute 的参数对象"), + skill_args: z.any().optional().describe("传给 skill.execute 的参数对象"), }) .strict(); @@ -30,13 +30,13 @@ export class ExecSkillTool extends Tool { /** * Executes a registered skill by name. - * @param skill_name - Name of the skill to invoke. - * @param args - Arguments forwarded to the skill's execute method. + * @param args - Arguments containing skill name and payload. + * @param context - Optional tool context forwarded to the skill. */ - async execute(skill_name: string, args?: unknown): Promise { - let payload: { skill_name: string; args?: Record }; + async execute(args: any, context?: ToolContext): Promise { + let payload: z.infer; try { - payload = ExeSkillSchema.parse({ skill_name, args }); + payload = ExeSkillSchema.parse(args); } catch (err) { return { success: false, @@ -51,6 +51,6 @@ export class ExecSkillTool extends Tool { error: `Skill '${payload.skill_name}' does not exist.`, }; } - return await skill.execute(payload.args); + return await skill.execute(payload.skill_args, context); } } diff --git a/packages/ema/src/tools/get_skill_tool.ts b/packages/ema/src/tools/get_skill_tool.ts index 2011d40c..4e5c9f25 100644 --- a/packages/ema/src/tools/get_skill_tool.ts +++ b/packages/ema/src/tools/get_skill_tool.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { Tool } from "./base"; -import type { ToolResult } from "./base"; +import type { ToolResult, ToolContext } from "./base"; import { type SkillRegistry } from "../skills"; const GetSkillSchema = z @@ -29,12 +29,13 @@ export class GetSkillTool extends Tool { /** * Fetches the SKILL.md playbook for a given skill. - * @param skill_name - Name of the skill to fetch. + * @param args - Arguments containing the skill name. + * @param context - Optional tool context (unused). */ - async execute(skill_name: string): Promise { - let payload: { skill_name: string }; + async execute(args: any, context?: ToolContext): Promise { + let payload: z.infer; try { - payload = GetSkillSchema.parse({ skill_name }); + payload = GetSkillSchema.parse(args); } catch (err) { return { success: false, diff --git a/packages/ema/src/tools/mini_agent/bash_tool.ts b/packages/ema/src/tools/mini_agent/bash_tool.ts deleted file mode 100644 index 4a0fd005..00000000 --- a/packages/ema/src/tools/mini_agent/bash_tool.ts +++ /dev/null @@ -1,688 +0,0 @@ -/** Shell command execution tool with background process management. - * - * Supports both bash (Unix/Linux/macOS) and PowerShell (Windows). - */ - -import os from "node:os"; -import { ChildProcess, spawn, type StdioOptions } from "node:child_process"; -import { randomUUID } from "node:crypto"; -import { EOL } from "node:os"; - -import { Tool } from "../base"; -import type { ToolResult } from "../base"; - -class BashOutputResult implements ToolResult { - success: boolean; - content?: string; - error?: string; - /** Bash command execution result with separated stdout and stderr. - * - * Implements ToolResult fields plus: - * - stdout: str (raw stdout) - * - stderr: str (raw stderr) - * - exitCode: int (process exit code) - * - bashId: str | null (background session id) - */ - stdout: string; - stderr: string; - exitCode: number; - bashId: string | null; - - constructor(options: { - success: boolean; - stdout: string; - stderr: string; - exitCode: number; - bashId?: string | null; - content?: string; - error?: string | null; - }) { - this.success = options.success; - this.content = options.content ?? ""; - this.error = options.error ?? undefined; - this.stdout = options.stdout; - this.stderr = options.stderr; - this.exitCode = options.exitCode; - this.bashId = options.bashId ?? null; - - // Auto-format content from stdout and stderr if content is empty. - if (!this.content) { - let output = ""; - if (this.stdout) { - output += this.stdout; - } - if (this.stderr) { - output += `${EOL}[stderr]:${EOL}${this.stderr}`; - } - if (this.bashId) { - output += `${EOL}[bash_id]:${EOL}${this.bashId}`; - } - if (this.exitCode) { - output += `${EOL}[exit_code]:${EOL}${this.exitCode}`; - } - - if (!output) { - output = "(no output)"; - } - this.content = output; - } - } -} - -class BackgroundShell { - /** Background shell data container. - * - * Pure data class that only stores state and output. - * IO operations are managed externally by BackgroundShellManager. - */ - bashId: string; - command: string; - process: ChildProcess; - startTime: number; - stdoutLines: string[]; - stderrLines: string[]; - stdoutLastReadIndex: number; - stderrLastReadIndex: number; - status: "running" | "completed" | "failed" | "terminated" | "error"; - exitCode: number | null; - - constructor(options: { - bashId: string; - command: string; - process: ChildProcess; - startTime: number; - }) { - this.bashId = options.bashId; - this.command = options.command; - this.process = options.process; - this.startTime = options.startTime; - this.stdoutLines = []; - this.stderrLines = []; - this.stdoutLastReadIndex = 0; - this.stderrLastReadIndex = 0; - this.status = "running"; - this.exitCode = null; - } - - addStdout(line: string): void { - /** Adds a new stdout line. */ - this.stdoutLines.push(line); - } - - addStderr(line: string): void { - /** Adds a new stderr line. */ - this.stderrLines.push(line); - } - - getNewOutput(filterPattern?: string | null): { - stdoutLines: string[]; - stderrLines: string[]; - } { - /** Gets new output since last check, optionally filtered by regex. */ - const stdoutNewLines = this.stdoutLines.slice(this.stdoutLastReadIndex); - const stderrNewLines = this.stderrLines.slice(this.stderrLastReadIndex); - this.stdoutLastReadIndex = this.stdoutLines.length; - this.stderrLastReadIndex = this.stderrLines.length; - - if (filterPattern) { - try { - const pattern = new RegExp(filterPattern); - return { - stdoutLines: stdoutNewLines.filter((line) => pattern.test(line)), - stderrLines: stderrNewLines.filter((line) => pattern.test(line)), - }; - } catch { - // Invalid regex, return all lines - } - } - - return { stdoutLines: stdoutNewLines, stderrLines: stderrNewLines }; - } - - updateStatus(isAlive: boolean, exitCode: number | null = null): void { - /** Updates process status. */ - if (!isAlive) { - this.status = exitCode === 0 ? "completed" : "failed"; - this.exitCode = exitCode; - } else { - this.status = "running"; - } - } - - async terminate(): Promise { - /** Terminates the background process. */ - if (this.process.exitCode === null) { - const gracefulTimeoutMs = 5000; - const exitedGracefully = await new Promise((resolve) => { - let timer: ReturnType | undefined; - const onExit = () => { - if (timer) clearTimeout(timer); - resolve(true); - }; - - this.process.once("exit", onExit); - - try { - this.process.kill("SIGTERM"); - } catch { - // Process may have already exited. - } - - timer = setTimeout(() => { - this.process.removeListener("exit", onExit); - resolve(false); - }, gracefulTimeoutMs); - - if (this.process.exitCode !== null) { - this.process.removeListener("exit", onExit); - if (timer) clearTimeout(timer); - resolve(true); - } - }); - - if (!exitedGracefully && this.process.exitCode === null) { - this.process.kill("SIGKILL"); - } - } - this.status = "terminated"; - this.exitCode = this.process.exitCode; - } -} - -class BackgroundShellManager { - /** Manager for all background shell processes. */ - static _shells: Map = new Map(); - - static add(shell: BackgroundShell): void { - /** Adds a background shell to management. */ - this._shells.set(shell.bashId, shell); - } - - static get(bashId: string): BackgroundShell | undefined { - /** Gets a background shell by ID. */ - return this._shells.get(bashId); - } - - static getAvailableIds(): string[] { - /** Gets all available bash IDs. */ - return Array.from(this._shells.keys()); - } - - static _remove(bashId: string): void { - /** Removes a background shell from management (internal use only). */ - this._shells.delete(bashId); - } - - static startMonitor(bashId: string): void { - /** Starts monitoring a background shell's output. */ - const shell = this.get(bashId); - if (!shell) return; - - const { process } = shell; - const handleStdoutData = (chunk: Buffer) => { - const text = chunk.toString("utf-8"); - const lines = text.split(/\r?\n/); - lines.forEach((line) => { - if (line !== "") { - shell.addStdout(line); - } - }); - }; - const handleStderrData = (chunk: Buffer) => { - const text = chunk.toString("utf-8"); - const lines = text.split(/\r?\n/); - lines.forEach((line) => { - if (line !== "") { - shell.addStderr(line); - } - }); - }; - - process.stdout?.on("data", handleStdoutData); - process.stderr?.on("data", handleStderrData); - - process.on("close", (code: number | null) => { - shell.updateStatus(false, code ?? null); - }); - - process.on("error", (err: Error) => { - shell.status = "error"; - shell.addStderr(`Monitor error: ${err.message}`); - }); - } - - static async terminate(bashId: string): Promise { - /** Terminates a background shell and cleans up all resources. - * - * Args: - * bashId: The unique identifier of the background shell - * - * Returns: - * The terminated BackgroundShell object - * - * Raises: - * ValueError: If shell not found - */ - const shell = this.get(bashId); - if (!shell) { - throw new Error(`Shell not found: ${bashId}`); - } - - await shell.terminate(); - this._remove(bashId); - return shell; - } -} - -export class BashTool extends Tool { - /** Executes shell commands in foreground or background. - * - * Automatically detects OS and uses appropriate shell: - * - Windows: PowerShell - * - Unix/Linux/macOS: bash - */ - - isWindows: boolean; - shellName: string; - - constructor() { - /** Initializes BashTool with OS-specific shell detection. */ - super(); - this.isWindows = os.platform() === "win32"; - this.shellName = this.isWindows ? "PowerShell" : "bash"; - } - - get name(): string { - return "bash"; - } - - get description(): string { - const shellExamples: Record = { - Windows: `Execute PowerShell commands in foreground or background. - -For terminal operations like git, npm, docker, etc. DO NOT use for file operations - use specialized tools. - -Parameters: - - command (required): PowerShell command to execute - - timeout (optional): Timeout in seconds (default: 120, max: 600) for foreground commands - - run_in_background (optional): Set true for long-running commands (servers, etc.) - -Tips: - - Quote file paths with spaces: cd "My Documents" - - Chain dependent commands with semicolon: git add . ; git commit -m "msg" - - Use absolute paths instead of cd when possible - - For background commands, monitor with bash_output and terminate with bash_kill - -Examples: - - git status - - npm test - - python -m http.server 8080 (with run_in_background=true)`, - Unix: `Execute bash commands in foreground or background. - -For terminal operations like git, npm, docker, etc. DO NOT use for file operations - use specialized tools. - -Parameters: - - command (required): Bash command to execute - - timeout (optional): Timeout in seconds (default: 120, max: 600) for foreground commands - - run_in_background (optional): Set true for long-running commands (servers, etc.) - -Tips: - - Quote file paths with spaces: cd "My Documents" - - Chain dependent commands with &&: git add . && git commit -m "msg" - - Use absolute paths instead of cd when possible - - For background commands, monitor with bash_output and terminate with bash_kill - -Examples: - - git status - - npm test - - python3 -m http.server 8080 (with run_in_background=true)`, - }; - return this.isWindows ? shellExamples.Windows : shellExamples.Unix; - } - - get parameters(): Record { - const cmdDesc = `The ${this.shellName} command to execute. Quote file paths with spaces using double quotes.`; - return { - type: "object", - properties: { - command: { - type: "string", - description: cmdDesc, - }, - timeout: { - type: "integer", - description: - "Optional: Timeout in seconds (default: 120, max: 600). Only applies to foreground commands.", - default: 120, - }, - run_in_background: { - type: "boolean", - description: - "Optional: Set to true to run the command in the background. Use this for long-running commands like servers. You can monitor output using bash_output tool.", - default: false, - }, - }, - required: ["command"], - }; - } - - async execute( - command: string, - timeout: number = 120, - runInBackground: boolean = false, - ): Promise { - /** Executes a shell command with optional background execution. - * - * Args: - * command: The shell command to execute - * timeout: Timeout in seconds (default: 120, max: 600) - * runInBackground: Set true to run command in background - * - * Returns: - * BashExecutionResult with command output and status - */ - try { - // Validate timeout - if (timeout > 600) { - timeout = 600; - } else if (timeout < 1) { - timeout = 120; - } - - // Prepare shell-specific command execution - const stdioOptions: StdioOptions = ["ignore", "pipe", "pipe"]; - const spawnArgs = this.isWindows - ? { - cmd: "powershell.exe", - args: ["-NoProfile", "-Command", command], - options: { stdio: stdioOptions }, - } - : { - cmd: command, - args: [], - options: { shell: true, stdio: stdioOptions }, - }; - - if (runInBackground) { - // Background execution: Create isolated process - const bashId = randomUUID().slice(0, 8); - const process = spawn(spawnArgs.cmd, spawnArgs.args, spawnArgs.options); - - const bgShell = new BackgroundShell({ - bashId, - command, - process, - startTime: Date.now(), - }); - BackgroundShellManager.add(bgShell); - BackgroundShellManager.startMonitor(bashId); - - const message = `Command started in background. Use bash_output to monitor (bash_id='${bashId}').`; - const formattedContent = `${message}\n\nCommand: ${command}\nBash ID: ${bashId}`; - - return new BashOutputResult({ - success: true, - content: formattedContent, - stdout: `Background command started with ID: ${bashId}`, - stderr: "", - exitCode: 0, - bashId, - }); - } - - // Foreground execution: Create isolated process - const process = spawn(spawnArgs.cmd, spawnArgs.args, spawnArgs.options); - - let stdout = ""; - let stderr = ""; - - const stdoutPromise = new Promise((resolve) => { - process.stdout?.on("data", (data: Buffer) => { - stdout += data.toString(); - }); - process.stdout?.on("end", () => resolve()); - }); - - const stderrPromise = new Promise((resolve) => { - process.stderr?.on("data", (data: Buffer) => { - stderr += data.toString(); - }); - process.stderr?.on("end", () => resolve()); - }); - - const exitPromise = new Promise((resolve) => { - process.on("close", (code: number | null) => { - resolve(code ?? 0); - }); - }); - - let timedOut = false; - const timer = setTimeout(() => { - timedOut = true; - process.kill(); - }, timeout * 1000); - - const exitCode = await exitPromise; - clearTimeout(timer); - await Promise.all([stdoutPromise, stderrPromise]); - - if (timedOut) { - const errorMsg = `Command timed out after ${timeout} seconds`; - return new BashOutputResult({ - success: false, - error: errorMsg, - stdout: "", - stderr: errorMsg, - exitCode: -1, - }); - } - - const isSuccess = exitCode === 0; - let errorMsg: string | null = null; - if (!isSuccess) { - errorMsg = `Command failed with exit code ${exitCode}`; - if (stderr.trim()) { - errorMsg += `\n${stderr.trim()}`; - } - } - - return new BashOutputResult({ - success: isSuccess, - error: errorMsg, - stdout, - stderr, - exitCode, - }); - } catch (error) { - const message = (error as Error).message; - return new BashOutputResult({ - success: false, - error: message, - stdout: "", - stderr: message, - exitCode: -1, - }); - } - } -} - -export class BashOutputTool extends Tool { - /** Retrieves output from background bash shells. */ - - get name(): string { - return "bash_output"; - } - - get description(): string { - return `Retrieves output from a running or completed background bash shell. - - - Takes a bash_id parameter identifying the shell - - Always returns only new output since the last check - - Returns stdout and stderr output along with shell status - - Supports optional regex filtering to show only lines matching a pattern - - Use this tool when you need to monitor or check the output of a long-running shell - - Shell IDs can be found using the bash tool with run_in_background=true - - Process status values: - - "running": Still executing - - "completed": Finished successfully - - "failed": Finished with error - - "terminated": Was terminated - - "error": Error occurred - - Example: bash_output(bash_id="abc12345")`; - } - - get parameters(): Record { - return { - type: "object", - properties: { - bash_id: { - type: "string", - description: - "The ID of the background shell to retrieve output from. Shell IDs are returned when starting a command with run_in_background=true.", - }, - filter_str: { - type: "string", - description: - "Optional regular expression to filter the output lines. Only lines matching this regex will be included in the result. Any lines that do not match will no longer be available to read.", - }, - }, - required: ["bash_id"], - }; - } - - async execute( - bashId: string, - filterStr: string | null = null, - ): Promise { - /** Retrieves output from background shell. - * - * Args: - * bashId: The unique identifier of the background shell - * filterStr: Optional regex pattern to filter output lines - * - * Returns: - * BashOutputResult with shell output including stdout, stderr, status, and success flag - */ - try { - const bgShell = BackgroundShellManager.get(bashId); - if (!bgShell) { - const availableIds = BackgroundShellManager.getAvailableIds(); - return new BashOutputResult({ - success: false, - error: `Shell not found: ${bashId}. Available: ${availableIds.length ? availableIds : "none"}`, - stdout: "", - stderr: "", - exitCode: -1, - }); - } - - const { stdoutLines, stderrLines } = bgShell.getNewOutput(filterStr); - const stdout = stdoutLines.length ? stdoutLines.join("\n") : ""; - const stderr = stderrLines.length ? stderrLines.join("\n") : ""; - - return new BashOutputResult({ - success: true, - stdout, - stderr, - exitCode: bgShell.exitCode ?? 0, - bashId, - }); - } catch (error) { - return new BashOutputResult({ - success: false, - error: `Failed to get bash output: ${(error as Error).message}`, - stdout: "", - stderr: (error as Error).message, - exitCode: -1, - }); - } - } -} - -export class BashKillTool extends Tool { - /** Terminates a running background bash shell. */ - - get name(): string { - return "bash_kill"; - } - - get description(): string { - return `Kills a running background bash shell by its ID. - - - Takes a bash_id parameter identifying the shell to kill - - Attempts graceful termination (SIGTERM) first, then forces (SIGKILL) if needed - - Returns the final status and any remaining output before termination - - Cleans up all resources associated with the shell - - Use this tool when you need to terminate a long-running shell - - Shell IDs can be found using the bash tool with run_in_background=true - - Example: bash_kill(bash_id="abc12345")`; - } - - get parameters(): Record { - return { - type: "object", - properties: { - bash_id: { - type: "string", - description: - "The ID of the background shell to terminate. Shell IDs are returned when starting a command with run_in_background=true.", - }, - }, - required: ["bash_id"], - }; - } - - async execute(bashId: string): Promise { - /** Terminates a background shell process. - * - * Args: - * bashId: The unique identifier of the background shell to terminate - * - * Returns: - * BashOutputResult with termination status and remaining output - */ - try { - const bgShell = BackgroundShellManager.get(bashId); - const remainingOutput = bgShell - ? bgShell.getNewOutput() - : { stdoutLines: [], stderrLines: [] }; - - const terminatedShell = await BackgroundShellManager.terminate(bashId); - const stdout = remainingOutput.stdoutLines.length - ? remainingOutput.stdoutLines.join("\n") - : ""; - const stderr = remainingOutput.stderrLines.length - ? remainingOutput.stderrLines.join("\n") - : ""; - - return new BashOutputResult({ - success: true, - stdout, - stderr, - exitCode: terminatedShell.exitCode ?? 0, - bashId, - }); - } catch (error) { - if ((error as Error).message.startsWith("Shell not found")) { - const availableIds = BackgroundShellManager.getAvailableIds(); - return new BashOutputResult({ - success: false, - error: `${(error as Error).message}. Available: ${availableIds.length ? availableIds : "none"}`, - stdout: "", - stderr: (error as Error).message, - exitCode: -1, - }); - } - return new BashOutputResult({ - success: false, - error: `Failed to terminate bash shell: ${(error as Error).message}`, - stdout: "", - stderr: (error as Error).message, - exitCode: -1, - }); - } - } -} diff --git a/packages/ema/src/tools/mini_agent/file_tools.ts b/packages/ema/src/tools/mini_agent/file_tools.ts deleted file mode 100644 index 6060e88c..00000000 --- a/packages/ema/src/tools/mini_agent/file_tools.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** File operation tools. */ - -import fs from "node:fs"; -import fsp from "node:fs/promises"; -import path from "node:path"; -import { Tiktoken } from "js-tiktoken"; -import cl100k_base from "js-tiktoken/ranks/cl100k_base"; - -import { Tool } from "../base"; -import type { ToolResult } from "../base"; - -const DEFAULT_MAX_TOKENS = 32000; - -export function truncateTextByTokens(text: string, maxTokens: number): string { - /** Truncates text by token count if it exceeds the limit. - * - * When text exceeds the specified token limit, performs intelligent truncation - * by keeping the front and back parts while truncating the middle. - * - * Args: - * text: Text to be truncated - * maxTokens: Maximum token limit - * - * Returns: - * str: Truncated text if it exceeds the limit, otherwise the original text. - * - * Example: - * >>> text = "very long text..." * 10000 - * >>> truncated = truncate_text_by_tokens(text, 64000) - * >>> print(truncated) - */ - const enc = new Tiktoken(cl100k_base); - - const tokenCount = enc.encode(text).length; - - // Return original text if under limit - if (tokenCount <= maxTokens) { - return text; - } - - // Calculate token/character ratio for approximation - const charCount = text.length || 1; - const ratio = tokenCount / charCount; - - // Keep head and tail mode: allocate half space for each (with 5% safety margin) - const charsPerHalf = Math.floor((maxTokens / 2 / ratio) * 0.95); - - // Truncate front part: find nearest newline - let headPart = text.slice(0, charsPerHalf); - const lastNewlineHead = headPart.lastIndexOf("\n"); - if (lastNewlineHead > 0) { - headPart = headPart.slice(0, lastNewlineHead); - } - - // Truncate back part: find nearest newline - let tailPart = text.slice(-charsPerHalf); - const firstNewlineTail = tailPart.indexOf("\n"); - if (firstNewlineTail > 0) { - tailPart = tailPart.slice(firstNewlineTail + 1); - } - - // Combine result - const truncationNote = `\n\n... [Content truncated: ${tokenCount} tokens -> ~${maxTokens} tokens limit] ...\n\n`; - return headPart + truncationNote + tailPart; -} - -export class ReadTool extends Tool { - workspaceDir: string; - - constructor(workspaceDir: string = ".") { - /** - * Initializes ReadTool with workspace directory. - * - * Args: - * workspaceDir: Base directory for resolving relative paths - */ - super(); - this.workspaceDir = path.resolve(workspaceDir); - } - - get name(): string { - return "read_file"; - } - - get description(): string { - return ( - "Read file contents from the filesystem. Output always includes line numbers " + - "in format 'LINE_NUMBER|LINE_CONTENT' (1-indexed). Supports reading partial content " + - "by specifying line offset and limit for large files. " + - "You can call this tool multiple times in parallel to read different files simultaneously." - ); - } - - get parameters(): Record { - return { - type: "object", - properties: { - path: { - type: "string", - description: "Absolute or relative path to the file", - }, - offset: { - type: "integer", - description: - "Starting line number (1-indexed). Use for large files to read from specific line", - }, - limit: { - type: "integer", - description: - "Number of lines to read. Use with offset for large files to read in chunks", - }, - }, - required: ["path"], - }; - } - - async execute( - pathInput: string, - offset?: number | null, - limit?: number | null, - ): Promise { - /** Executes the read file operation. */ - try { - const resolvedPath = path.isAbsolute(pathInput) - ? pathInput - : path.resolve(this.workspaceDir, pathInput); - - if (!fs.existsSync(resolvedPath)) { - return { - success: false, - content: "", - error: `File not found: ${pathInput}`, - }; - } - - const rawContent = await fsp.readFile(resolvedPath, "utf-8"); - const lines = rawContent.split("\n"); - - // Apply offset and limit - let start = offset ? offset - 1 : 0; - let end = limit ? start + limit : lines.length; - if (start < 0) { - start = 0; - } - if (end > lines.length) { - end = lines.length; - } - - const selectedLines = lines.slice(start, end); - - // Format with line numbers (1-indexed) - const numberedLines: string[] = []; - selectedLines.forEach((line, index) => { - const lineContent = line.replace(/\r$/, ""); - const lineNumber = (start + index + 1).toString().padStart(6, " "); - numberedLines.push(`${lineNumber}|${lineContent}`); - }); - - let content = numberedLines.join("\n"); - - // Apply token truncation if needed - content = truncateTextByTokens(content, DEFAULT_MAX_TOKENS); - - return { success: true, content }; - } catch (error) { - return { - success: false, - content: "", - error: (error as Error).message, - }; - } - } -} - -export class WriteTool extends Tool { - workspaceDir: string; - - constructor(workspaceDir: string = ".") { - /** - * Initializes WriteTool with workspace directory. - * - * Args: - * workspaceDir: Base directory for resolving relative paths - */ - super(); - this.workspaceDir = path.resolve(workspaceDir); - } - - get name(): string { - return "write_file"; - } - - get description(): string { - return ( - "Write content to a file. Will overwrite existing files completely. " + - "For existing files, you should read the file first using read_file. " + - "Prefer editing existing files over creating new ones unless explicitly needed." - ); - } - - get parameters(): Record { - return { - type: "object", - properties: { - path: { - type: "string", - description: "Absolute or relative path to the file", - }, - content: { - type: "string", - description: - "Complete content to write (will replace existing content)", - }, - }, - required: ["path", "content"], - }; - } - - async execute(pathInput: string, content: string): Promise { - /** Executes the write file operation. */ - try { - const resolvedPath = path.isAbsolute(pathInput) - ? pathInput - : path.resolve(this.workspaceDir, pathInput); - - await fsp.mkdir(path.dirname(resolvedPath), { recursive: true }); - await fsp.writeFile(resolvedPath, content, "utf-8"); - - return { - success: true, - content: `Successfully wrote to ${resolvedPath}`, - }; - } catch (error) { - return { - success: false, - content: "", - error: (error as Error).message, - }; - } - } -} - -export class EditTool extends Tool { - workspaceDir: string; - - constructor(workspaceDir: string = ".") { - /** - * Initializes EditTool with workspace directory. - * - * Args: - * workspaceDir: Base directory for resolving relative paths - */ - super(); - this.workspaceDir = path.resolve(workspaceDir); - } - - get name(): string { - return "edit_file"; - } - - get description(): string { - return ( - "Perform exact string replacement in a file. The old_str must match exactly " + - "and appear uniquely in the file, otherwise the operation will fail. " + - "You must read the file first before editing. Preserve exact indentation from the source." - ); - } - - get parameters(): Record { - return { - type: "object", - properties: { - path: { - type: "string", - description: "Absolute or relative path to the file", - }, - old_str: { - type: "string", - description: - "Exact string to find and replace (must be unique in file)", - }, - new_str: { - type: "string", - description: - "Replacement string (use for refactoring, renaming, etc.)", - }, - }, - required: ["path", "old_str", "new_str"], - }; - } - - async execute( - pathInput: string, - oldStr: string, - newStr: string, - ): Promise { - /** Executes the edit file operation. */ - try { - const resolvedPath = path.isAbsolute(pathInput) - ? pathInput - : path.resolve(this.workspaceDir, pathInput); - - if (!fs.existsSync(resolvedPath)) { - return { - success: false, - content: "", - error: `File not found: ${pathInput}`, - }; - } - - const content = await fsp.readFile(resolvedPath, "utf-8"); - - if (!content.includes(oldStr)) { - return { - success: false, - content: "", - error: `Text not found in file: ${oldStr}`, - }; - } - - const newContent = content.replaceAll(oldStr, newStr); - await fsp.writeFile(resolvedPath, newContent, "utf-8"); - - return { - success: true, - content: `Successfully edited ${resolvedPath}`, - }; - } catch (error) { - return { - success: false, - content: "", - error: (error as Error).message, - }; - } - } -} diff --git a/packages/ema/src/tools/mini_agent/mcp_loader.ts b/packages/ema/src/tools/mini_agent/mcp_loader.ts deleted file mode 100644 index 46850a56..00000000 --- a/packages/ema/src/tools/mini_agent/mcp_loader.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** MCP tool loader with real MCP client integration. */ - -import fs from "node:fs"; -import path from "node:path"; - -import { Client } from "@modelcontextprotocol/sdk/client"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; - -import { Tool } from "../base"; -import type { ToolResult } from "../base"; - -/** Wrapper for MCP tools. */ -export class MCPTool extends Tool { - _name: string; - _description: string; - _parameters: Record; - _session: any; - - constructor(options: { - name: string; - description: string; - parameters: Record; - session: any; - }) { - super(); - this._name = options.name; - this._description = options.description; - this._parameters = options.parameters; - this._session = options.session; - } - - get name(): string { - return this._name; - } - - get description(): string { - return this._description; - } - - get parameters(): Record { - return this._parameters; - } - - async execute(kwargs: Record): Promise { - /** Executes the MCP tool via the session. */ - try { - const result = await this._session.callTool(this._name, { - arguments: kwargs, - }); - - // MCP tool results are a list of content items - const contentParts: string[] = []; - if (result?.content) { - for (const item of result.content) { - if ("text" in item) { - contentParts.push(item.text); - } else { - contentParts.push(item.toString()); - } - } - } - - const contentStr = contentParts.join("\n"); - const isError = result?.isError ?? false; - - return { - success: !isError, - content: contentStr, - error: isError ? "Tool returned error" : undefined, - }; - } catch (error) { - return { - success: false, - content: "", - error: `MCP tool execution failed: ${(error as Error).message}`, - }; - } - } -} - -class MCPServerConnection { - /** Manages connection to a single MCP server. */ - name: string; - command: string; - args: string[]; - env: Record; - session: any | null; - transport: any | null; - tools: MCPTool[]; - - constructor(options: { - name: string; - command: string; - args: string[]; - env?: Record | null; - }) { - this.name = options.name; - this.command = options.command; - this.args = options.args; - this.env = options.env ?? {}; - this.session = null; - this.transport = null; - this.tools = []; - } - - async connect(): Promise { - /** Connects to the MCP server using proper async context management. */ - try { - // Prepare transport - const transport = new StdioClientTransport({ - command: this.command, - args: this.args, - env: Object.keys(this.env).length ? this.env : undefined, - }); - - const session = new (Client as any)({ - transport, - }); - - // Connect the session - if (typeof session.connect === "function") { - await session.connect(); - } else if (typeof session.initialize === "function") { - await session.initialize(); - } - - this.session = session; - this.transport = transport; - - // List available tools - const toolsList = (await (session.listTools?.() ?? - session.list_tools?.())) ?? { tools: [] }; - - // Wrap each tool - for (const tool of toolsList.tools ?? []) { - // Convert MCP tool schema to our format - const parameters = tool.inputSchema ?? tool.input_schema ?? {}; - - const mcpTool = new MCPTool({ - name: tool.name, - description: tool.description ?? "", - parameters, - session, - }); - this.tools.push(mcpTool); - } - - console.log( - `✓ Connected to MCP server '${this.name}' - loaded ${this.tools.length} tools`, - ); - for (const tool of this.tools) { - const desc = - tool.description.length > 60 - ? tool.description.slice(0, 60) - : tool.description; - console.log(` - ${tool.name}: ${desc}...`); - } - return true; - } catch (error) { - console.log( - `✗ Failed to connect to MCP server '${this.name}': ${(error as Error).message}`, - ); - await this.disconnect(); - return false; - } - } - - async disconnect(): Promise { - /** Disconnects from the MCP server. */ - try { - if (this.session?.close) { - await this.session.close(); - } else if (this.transport?.close) { - await this.transport.close(); - } - } catch { - // Swallow errors during cleanup - } finally { - this.session = null; - this.transport = null; - } - } -} - -// Global connections registry -const _mcpConnections: MCPServerConnection[] = []; - -export async function loadMcpToolsAsync( - configPath: string = "mcp.json", -): Promise { - /** - * Loads MCP tools from the config file. - * - * This function: - * 1. Reads the MCP config file - * 2. Starts MCP server processes - * 3. Connects to each server - * 4. Fetches tool definitions - * 5. Wraps them as Tool objects - * - * Args: - * configPath: Path to MCP configuration file (default: "mcp.json") - * - * Returns: - * List of Tool objects representing MCP tools - */ - const configFile = path.resolve(configPath); - - try { - await fs.promises.access(configFile); - } catch { - console.log(`MCP config not found: ${configPath}`); - return []; - } - - try { - const rawConfig = await fs.promises.readFile(configFile, "utf-8"); - const config = JSON.parse(rawConfig); - - const mcpServers = config?.mcpServers ?? {}; - - if (!mcpServers || !Object.keys(mcpServers).length) { - console.log("No MCP servers configured"); - return []; - } - - const allTools: Tool[] = []; - - // Connect to each enabled server - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { - if (serverConfig?.disabled) { - console.log(`Skipping disabled server: ${serverName}`); - continue; - } - - const command = serverConfig?.command; - const args = serverConfig?.args ?? []; - const env = serverConfig?.env ?? {}; - - if (!command) { - console.log(`No command specified for server: ${serverName}`); - continue; - } - - const connection = new MCPServerConnection({ - name: serverName, - command, - args, - env, - }); - const success = await connection.connect(); - - if (success) { - _mcpConnections.push(connection); - allTools.push(...connection.tools); - } - } - - console.log(`\nTotal MCP tools loaded: ${allTools.length}`); - return allTools; - } catch (error) { - console.log(`Error loading MCP config: ${(error as Error).message}`); - return []; - } -} - -export async function cleanupMcpConnections(): Promise { - /** Cleans up all MCP connections. */ - for (const connection of _mcpConnections) { - await connection.disconnect(); - } - _mcpConnections.length = 0; -} diff --git a/packages/ema/src/tools/mini_agent/note_tool.ts b/packages/ema/src/tools/mini_agent/note_tool.ts deleted file mode 100644 index db84d753..00000000 --- a/packages/ema/src/tools/mini_agent/note_tool.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** Session Note Tool - Let agent record and recall important information. - * - * This tool allows the agent to: - * - Record key points and important information during sessions - * - Recall previously recorded notes - * - Maintain context across agent execution chains - */ - -import fs from "node:fs"; -import path from "node:path"; - -import { Tool } from "../base"; -import type { ToolResult } from "../base"; - -export class SessionNoteTool extends Tool { - memoryFile: string; - - constructor(memoryFile: string = "./workspace/.agent_memory.json") { - /** - * Initializes the session note tool. - * - * Args: - * memoryFile: Path to the note storage file - */ - super(); - this.memoryFile = path.resolve(memoryFile); - // Lazy loading: file and directory are only created when first note is recorded - } - - get name(): string { - return "record_note"; - } - - get description(): string { - return ( - "Record important information as session notes for future reference. " + - "Use this to record key facts, user preferences, decisions, or context " + - "that should be recalled later in the agent execution chain. Each note is timestamped." - ); - } - - get parameters(): Record { - return { - type: "object", - properties: { - content: { - type: "string", - description: - "The information to record as a note. Be concise but specific.", - }, - category: { - type: "string", - description: - "Optional category/tag for this note (e.g., 'user_preference', 'project_info', 'decision')", - }, - }, - required: ["content"], - }; - } - - async _loadFromFile(): Promise { - /** Loads notes from the file. - * - * Returns empty list if file doesn't exist (lazy loading). - */ - try { - const raw = await fs.promises.readFile(this.memoryFile, "utf-8"); - return JSON.parse(raw); - } catch (error) { - if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { - return []; - } - return []; - } - } - - async _saveToFile(notes: any[]): Promise { - /** Saves notes to the file. - * - * Creates parent directory and file if they don't exist (lazy initialization). - */ - // Ensure parent directory exists when actually saving - await fs.promises.mkdir(path.dirname(this.memoryFile), { recursive: true }); - await fs.promises.writeFile( - this.memoryFile, - JSON.stringify(notes, null, 2), - "utf-8", - ); - } - - async execute( - content: string, - category: string = "general", - ): Promise { - /** Records a session note. - * - * Args: - * content: The information to record - * category: Category/tag for this note - * - * Returns: - * ToolResult with success status - */ - try { - // Load existing notes - const notes = await this._loadFromFile(); - - // Add new note with timestamp - const note = { - timestamp: new Date().toISOString(), - category, - content, - }; - notes.push(note); - - // Save back to file - await this._saveToFile(notes); - - return { - success: true, - content: `Recorded note: ${content} (category: ${category}`, - }; - } catch (error) { - return { - success: false, - content: "", - error: `Failed to record note: ${(error as Error).message}`, - }; - } - } -} - -export class RecallNoteTool extends Tool { - memoryFile: string; - - constructor(memoryFile: string = "./workspace/.agent_memory.json") { - /** - * Initializes the recall note tool. - * - * Args: - * memoryFile: Path to the note storage file - */ - super(); - this.memoryFile = path.resolve(memoryFile); - } - - get name(): string { - return "recall_notes"; - } - - get description(): string { - return ( - "Recall all previously recorded session notes. " + - "Use this to retrieve important information, context, or decisions " + - "from earlier in the session or previous agent execution chains." - ); - } - - get parameters(): Record { - return { - type: "object", - properties: { - category: { - type: "string", - description: "Optional: filter notes by category", - }, - }, - }; - } - - async execute(category: string | null = null): Promise { - /** Recalls session notes. - * - * Args: - * category: Optional category filter - * - * Returns: - * ToolResult with notes content - */ - try { - let notes: any[]; - try { - notes = JSON.parse( - await fs.promises.readFile(this.memoryFile, "utf-8"), - ) as any[]; - } catch (error) { - if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { - return { - success: true, - content: "No notes recorded yet.", - }; - } - throw error; - } - - if (!notes?.length) { - return { - success: true, - content: "No notes recorded yet.", - }; - } - - // Filter by category if specified - let filteredNotes = notes; - if (category) { - filteredNotes = notes.filter((n) => n?.category === category); - if (!filteredNotes.length) { - return { - success: true, - content: `No notes found in category: ${category}`, - }; - } - } - - // Format notes for display - const formatted: string[] = []; - filteredNotes.forEach((note, idx) => { - const timestamp = note?.timestamp ?? "unknown time"; - const cat = note?.category ?? "general"; - const noteContent = note?.content ?? ""; - formatted.push( - `${idx + 1}. [${cat}] ${noteContent}\n (recorded at ${timestamp})`, - ); - }); - - const result = "Recorded Notes:\n" + formatted.join("\n"); - - return { success: true, content: result }; - } catch (error) { - return { - success: false, - content: "", - error: `Failed to recall notes: ${(error as Error).message}`, - }; - } - } -} diff --git a/packages/ema/src/tools/mini_agent/skill_loader.ts b/packages/ema/src/tools/mini_agent/skill_loader.ts deleted file mode 100644 index e5aa129a..00000000 --- a/packages/ema/src/tools/mini_agent/skill_loader.ts +++ /dev/null @@ -1,292 +0,0 @@ -/** - * Skill Loader - Load Claude Skills - * - * Supports loading skills from SKILL.md files and providing them to Agent - */ - -import fs from "node:fs"; -import path from "node:path"; -import yaml from "js-yaml"; - -/** Skill data structure */ -export class Skill { - name: string; - description: string; - content: string; - license?: string | null; - allowedTools?: string[] | null; - metadata?: Record | null; - skillPath?: string | null; - - constructor(options: { - name: string; - description: string; - content: string; - license?: string | null; - allowedTools?: string[] | null; - metadata?: Record | null; - skillPath?: string | null; - }) { - this.name = options.name; - this.description = options.description; - this.content = options.content; - this.license = options.license ?? null; - this.allowedTools = options.allowedTools ?? null; - this.metadata = options.metadata ?? null; - this.skillPath = options.skillPath ?? null; - } - - /** Converts the skill to the prompt format. */ - toPrompt(): string { - return ` -# Skill: ${this.name} - -${this.description} - ---- - -${this.content} -`; - } -} - -export class SkillLoader { - skillsDir: string; - loadedSkills: Record; - - /** - * Initializes Skill Loader. - * - * @param skillsDir Skills directory path - */ - constructor(skillsDir: string = "./skills") { - this.skillsDir = path.resolve(skillsDir); - this.loadedSkills = {}; - } - - /** - * Loads a single skill from a SKILL.md file. - * - * @param skillPath SKILL.md file path - * @returns Skill object, or null if loading fails - */ - loadSkill(skillPath: string): Skill | null { - try { - const content = fs.readFileSync(skillPath, "utf-8"); - - // Parse YAML frontmatter - const frontmatterMatch = content.match(/^---\n(.*?)\n---\n(.*)$/s); - - if (!frontmatterMatch) { - console.log(`⚠️ ${skillPath} missing YAML frontmatter`); - return null; - } - - const frontmatterText = frontmatterMatch[1]; - const skillContent = frontmatterMatch[2].trim(); - - // Parse YAML - let frontmatter: any; - try { - frontmatter = yaml.load(frontmatterText); - } catch (error) { - console.log(`❌ Failed to parse YAML frontmatter: ${error}`); - return null; - } - - // Required fields - if (!frontmatter?.name || !frontmatter?.description) { - console.log( - `⚠️ ${skillPath} missing required fields (name or description)`, - ); - return null; - } - - // Get skill directory (parent of SKILL.md) - const skillDir = path.dirname(skillPath); - - // Replace relative paths in content with absolute paths - // This ensures scripts and resources can be found from any working directory - const processedContent = this._processSkillPaths(skillContent, skillDir); - - // Create Skill object - const skill = new Skill({ - name: frontmatter.name as string, - description: frontmatter.description as string, - content: processedContent, - license: (frontmatter.license as string | undefined) ?? null, - allowedTools: - (frontmatter["allowed-tools"] as string[] | undefined) ?? null, - metadata: - (frontmatter.metadata as Record | undefined) ?? null, - skillPath, - }); - - return skill; - } catch (error) { - console.log(`❌ Failed to load skill (${skillPath}): ${error}`); - return null; - } - } - - /** - * Processes skill content to replace relative paths with absolute paths. - * - * Supports Progressive Disclosure Level 3+: converts relative file references - * to absolute paths so Agent can easily read nested resources. - * - * @param content Original skill content - * @param skillDir Skill directory path - * @returns Processed content with absolute paths - */ - _processSkillPaths(content: string, skillDir: string): string { - // Pattern 1: Directory-based paths (scripts/, examples/, templates/, reference/) - const replaceDirPath = (match: string, prefix: string, relPath: string) => { - const absPath = path.join(skillDir, relPath); - if (fs.existsSync(absPath)) { - return `${prefix}${absPath}`; - } - return match; - }; - - const patternDirs = - /(python\s+|`)((?:scripts|examples|templates|reference)\/[^\s`\)]+)/g; - content = content.replace(patternDirs, replaceDirPath); - - // Pattern 2: Direct markdown/document references (forms.md, reference.md, etc.) - // Matches phrases like "see reference.md" or "read forms.md" - const replaceDocPath = ( - match: string, - prefix: string, - filename: string, - suffix: string, - ) => { - const absPath = path.join(skillDir, filename); - if (fs.existsSync(absPath)) { - // Add helpful instruction for Agent - return `${prefix}\`${absPath}\` (use read_file to access)${suffix}`; - } - return match; - }; - - const patternDocs = - /(see|read|refer to|check)\s+([a-zA-Z0-9_-]+\.(?:md|txt|json|yaml))([.,;\s])/gi; - content = content.replace(patternDocs, replaceDocPath); - - // Pattern 3: Markdown links - supports multiple formats: - // - [`filename.md`](filename.md) - simple filename - // - [text](./reference/file.md) - relative path with ./ - // - [text](scripts/file.js) - directory-based path - // Matches patterns like: "Read [`docx-js.md`](docx-js.md)" or "Load [Guide](./reference/guide.md)" - const replaceMarkdownLink = ( - match: string, - prefix: string | undefined, - linkText: string, - filepath: string, - ) => { - // Remove leading ./ if present - const cleanPath = filepath.startsWith("./") - ? filepath.slice(2) - : filepath; - - const absPath = path.join(skillDir, cleanPath); - if (fs.existsSync(absPath)) { - // Preserve the link text style (with or without backticks) - const prefixText = prefix ? `${prefix} ` : ""; - return `${prefixText}[${linkText}](\`${absPath}\`) (use read_file to access)`; - } - return match; - }; - - const patternMarkdown = - /(?:(Read|See|Check|Refer to|Load|View)\s+)?\[(`?[^`\]]+`?)\]\(((?:\.\/)?[^)]+\.(?:md|txt|json|yaml|js|py|html))\)/gi; - content = content.replace(patternMarkdown, replaceMarkdownLink); - - return content; - } - - /** - * Discovers and loads all skills in the skills directory. - * - * @returns List of Skills - */ - discoverSkills(): Skill[] { - const skills: Skill[] = []; - - if (!fs.existsSync(this.skillsDir)) { - console.log(`⚠️ Skills directory does not exist: ${this.skillsDir}`); - return skills; - } - - const skillFiles: string[] = []; - const walk = (dir: string) => { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const entryPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - walk(entryPath); - } else if (entry.isFile() && entry.name === "SKILL.md") { - skillFiles.push(entryPath); - } - } - }; - - walk(this.skillsDir); - - for (const skillFile of skillFiles) { - const skill = this.loadSkill(skillFile); - if (skill) { - skills.push(skill); - this.loadedSkills[skill.name] = skill; - } - } - - return skills; - } - - /** - * Gets a loaded skill. - * - * @param name Skill name - * @returns Skill object, or null if not found - */ - getSkill(name: string): Skill | null { - return this.loadedSkills[name] ?? null; - } - - /** - * Lists all loaded skill names. - * - * @returns List of skill names - */ - listSkills(): string[] { - return Object.keys(this.loadedSkills); - } - - /** - * Generates a prompt containing ONLY metadata (name + description) for all skills. - * This implements Progressive Disclosure - Level 1. - * - * @returns Metadata-only prompt string - */ - getSkillsMetadataPrompt(): string { - if (!Object.keys(this.loadedSkills).length) { - return ""; - } - - const promptParts = ["## Available Skills\n"]; - promptParts.push( - "You have access to specialized skills. Each skill provides expert guidance for specific tasks.\n", - ); - promptParts.push( - "Load a skill's full content using the appropriate skill tool when needed.\n", - ); - - // List all skills with their descriptions - for (const skill of Object.values(this.loadedSkills)) { - promptParts.push(`- \`${skill.name}\`: ${skill.description}`); - } - - return promptParts.join("\n"); - } -} diff --git a/packages/ema/src/tools/mini_agent/skill_tool.ts b/packages/ema/src/tools/mini_agent/skill_tool.ts deleted file mode 100644 index c79416e3..00000000 --- a/packages/ema/src/tools/mini_agent/skill_tool.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Skill Tool - Tool for Agent to load Skills on-demand - * - * Implements Progressive Disclosure (Level 2): Load full skill content when needed - */ - -import { Tool } from "../base"; -import type { ToolResult } from "../base"; -import { SkillLoader } from "./skill_loader"; - -export class GetSkillTool extends Tool { - skillLoader: SkillLoader; - - constructor(skillLoader: SkillLoader) { - /** Initializes the tool to get detailed information about a specific skill. */ - super(); - this.skillLoader = skillLoader; - } - - get name(): string { - return "get_skill"; - } - - get description(): string { - return "Get complete content and guidance for a specified skill, used for executing specific types of tasks"; - } - - get parameters(): Record { - return { - type: "object", - properties: { - skill_name: { - type: "string", - description: - "Name of the skill to retrieve (use list_skills to view available skills)", - }, - }, - required: ["skill_name"], - }; - } - - async execute(skill_name: string): Promise { - /** Gets detailed information about the specified skill. */ - const skill = this.skillLoader.getSkill(skill_name); - - if (!skill) { - const available = this.skillLoader.listSkills().join(", "); - return { - success: false, - content: "", - error: `Skill '${skill_name}' does not exist. Available skills: ${available}`, - }; - } - - // Return complete skill content - const result = skill.toPrompt(); - return { success: true, content: result }; - } -} - -export function createSkillTools( - skillsDir: string = "./skills", -): [Tool[], SkillLoader | null] { - /** - * Creates skill tools for Progressive Disclosure. - * - * Only provides get_skill tool - the agent uses metadata in system prompt - * to know what skills are available, then loads them on-demand. - * - * Args: - * skillsDir: Skills directory path - * - * Returns: - * Tuple of (list of tools, skill loader) - */ - // Create skill loader - const loader = new SkillLoader(skillsDir); - - // Discover and load skills - const skills = loader.discoverSkills(); - console.log(`✅ Discovered ${skills.length} Claude Skills`); - - // Create only the get_skill tool (Progressive Disclosure Level 2) - const tools: Tool[] = [new GetSkillTool(loader)]; - - return [tools, loader]; -} From 7e1f61fbf443b623904461ea9401de6436812afd Mon Sep 17 00:00:00 2001 From: Disviel Date: Mon, 26 Jan 2026 18:43:33 +0800 Subject: [PATCH 03/16] feat: add mongodb-agenda dependency and patch for agenda --- packages/ema-ui/package.json | 1 + packages/ema/package.json | 1 + patches/@hokify__agenda@6.3.0.patch | 90 +++++++++++++++++++++++++++++ pnpm-lock.yaml | 15 ++++- pnpm-workspace.yaml | 7 ++- 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 patches/@hokify__agenda@6.3.0.patch diff --git a/packages/ema-ui/package.json b/packages/ema-ui/package.json index 7c9ed01d..8c0216c0 100644 --- a/packages/ema-ui/package.json +++ b/packages/ema-ui/package.json @@ -12,6 +12,7 @@ "@lancedb/lancedb": "^0.23.0", "arktype": "^2.1.29", "ema": "workspace:*", + "mongodb-agenda": "npm:mongodb@4.17.2", "mongodb": "^7.0.0", "next": "16.0.10", "pino": "^10.1.0", diff --git a/packages/ema/package.json b/packages/ema/package.json index b2abe3af..0a75afa4 100644 --- a/packages/ema/package.json +++ b/packages/ema/package.json @@ -37,6 +37,7 @@ "js-tiktoken": "^1.0.21", "js-yaml": "^4.1.0", "mongodb": "^7.0.0", + "mongodb-agenda": "npm:mongodb@4.17.2", "mongodb-memory-server": "^11.0.0", "openai": "^6.13.0", "pino": "^10.1.0", diff --git a/patches/@hokify__agenda@6.3.0.patch b/patches/@hokify__agenda@6.3.0.patch new file mode 100644 index 00000000..1f642e72 --- /dev/null +++ b/patches/@hokify__agenda@6.3.0.patch @@ -0,0 +1,90 @@ +diff --git a/dist/Job.d.ts b/dist/Job.d.ts +index 191a9d8f82451d04708211306271546ff1a2b33d..a9acbad14021b6afba60ee22c8a81a1042674eb5 100644 +--- a/dist/Job.d.ts ++++ b/dist/Job.d.ts +@@ -1,4 +1,4 @@ +-import { ObjectId } from 'mongodb'; ++import { ObjectId } from 'mongodb-agenda'; + import type { Agenda } from './index'; + import { IJobParameters } from './types/JobParameters'; + import { JobPriority } from './utils/priority'; +diff --git a/dist/JobDbRepository.d.ts b/dist/JobDbRepository.d.ts +index 7719da22c1a097047c373c172290f835b9816b90..5d4bc54b40cb9915121825c24d87bae1ccc63d2b 100644 +--- a/dist/JobDbRepository.d.ts ++++ b/dist/JobDbRepository.d.ts +@@ -1,4 +1,4 @@ +-import { Collection, Filter, ObjectId, Sort } from 'mongodb'; ++import { Collection, Filter, ObjectId, Sort } from 'mongodb-agenda'; + import type { Job, JobWithId } from './Job'; + import type { Agenda } from './index'; + import type { IDatabaseOptions, IDbConfig, IMongoOptions } from './types/DbOptions'; +@@ -14,7 +14,7 @@ export declare class JobDbRepository { + private createConnection; + private hasMongoConnection; + private hasDatabaseConfig; +- getJobById(id: string): Promise> | null>; ++ getJobById(id: string): Promise> | null>; + getJobs(query: Filter, sort?: Sort, limit?: number, skip?: number): Promise; + removeJobs(query: Filter): Promise; + getQueueSize(): Promise; +diff --git a/dist/JobDbRepository.js b/dist/JobDbRepository.js +index 6dec3f9301ce0f7561dba7baefdc247a132bf4c0..e5ceef0dc39dcef2f070e15c6b84ee43f1253aab 100644 +--- a/dist/JobDbRepository.js ++++ b/dist/JobDbRepository.js +@@ -2,7 +2,7 @@ + Object.defineProperty(exports, "__esModule", { value: true }); + exports.JobDbRepository = void 0; + const debug = require("debug"); +-const mongodb_1 = require("mongodb"); ++const mongodb_1 = require("mongodb-agenda"); + const hasMongoProtocol_1 = require("./utils/hasMongoProtocol"); + const log = debug('agenda:db'); + /** +diff --git a/dist/index.d.ts b/dist/index.d.ts +index a65473711ba88fad34b792c83eea536833b970b2..6f62e641627fe1abb1815a1ade4f4c8fa0e0989b 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -1,8 +1,8 @@ + /// + /// + import { EventEmitter } from 'events'; +-import type { Db, Filter, MongoClientOptions, Sort } from 'mongodb'; +-import { SortDirection } from 'mongodb'; ++import type { Db, Filter, MongoClientOptions, Sort } from 'mongodb-agenda'; ++import { SortDirection } from 'mongodb-agenda'; + import { ForkOptions } from 'child_process'; + import type { IJobDefinition } from './types/JobDefinition'; + import type { IAgendaConfig } from './types/AgendaConfig'; +diff --git a/dist/types/DbOptions.d.ts b/dist/types/DbOptions.d.ts +index 897c3975c8d16d2fe2c530369797b43680179b73..d69c7381bca685d9b8ad0efcb02195970e4aea4b 100644 +--- a/dist/types/DbOptions.d.ts ++++ b/dist/types/DbOptions.d.ts +@@ -1,4 +1,4 @@ +-import type { Db, MongoClientOptions, SortDirection } from 'mongodb'; ++import type { Db, MongoClientOptions, SortDirection } from 'mongodb-agenda'; + export interface IDatabaseOptions { + db: { + collection?: string; +diff --git a/dist/types/JobParameters.d.ts b/dist/types/JobParameters.d.ts +index 46358160593792f74a30cbdaa1178093e24fdbc7..9f8228a7898c0070007c5882eff7f17d086983a9 100644 +--- a/dist/types/JobParameters.d.ts ++++ b/dist/types/JobParameters.d.ts +@@ -1,4 +1,4 @@ +-import { Filter, ObjectId } from 'mongodb'; ++import { Filter, ObjectId } from 'mongodb-agenda'; + export interface IJobParameters { + _id?: ObjectId; + name: string; +diff --git a/package.json b/package.json +index e3c578da6d15d76378a079da77036883c62b5792..56a71ad1a489fc68e0ba804c69eb00f158ae7c57 100644 +--- a/package.json ++++ b/package.json +@@ -56,7 +56,7 @@ + "debug": "~4", + "human-interval": "~2", + "luxon": "^3", +- "mongodb": "^4" ++ "mongodb-agenda": "npm:mongodb@4.17.2" + }, + "devDependencies": { + "eslint": "^8.29.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 418545be..11b0d619 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + '@hokify/agenda@6.3.0': + hash: 106461c8850a7c4fd73b7f49b2d626d14118dc90fcf4ac7195b7f66ae0e8bfc6 + path: patches/@hokify__agenda@6.3.0.patch + importers: .: @@ -60,7 +65,7 @@ importers: version: 1.34.0(@modelcontextprotocol/sdk@1.25.1(hono@4.11.1)(zod@4.2.1)) '@hokify/agenda': specifier: ^6.3.0 - version: 6.3.0 + version: 6.3.0(patch_hash=106461c8850a7c4fd73b7f49b2d626d14118dc90fcf4ac7195b7f66ae0e8bfc6) '@lancedb/lancedb': specifier: ^0.23.0 version: 0.23.0(apache-arrow@18.1.0) @@ -85,6 +90,9 @@ importers: mongodb: specifier: ^7.0.0 version: 7.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7) + mongodb-agenda: + specifier: npm:mongodb@4.17.2 + version: mongodb@4.17.2 mongodb-memory-server: specifier: ^11.0.0 version: 11.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7) @@ -143,6 +151,9 @@ importers: mongodb: specifier: ^7.0.0 version: 7.0.0(@aws-sdk/credential-providers@3.974.0)(socks@2.8.7) + mongodb-agenda: + specifier: npm:mongodb@4.17.2 + version: mongodb@4.17.2 next: specifier: 16.0.10 version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -5451,7 +5462,7 @@ snapshots: - supports-color - utf-8-validate - '@hokify/agenda@6.3.0': + '@hokify/agenda@6.3.0(patch_hash=106461c8850a7c4fd73b7f49b2d626d14118dc90fcf4ac7195b7f66ae0e8bfc6)': dependencies: cron-parser: 4.9.0 date.js: 0.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1d73bef5..9d40b8f2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,6 @@ packages: - - "packages/*" - - "contrib/ema-dev/editor/vscode" + - packages/* + - contrib/ema-dev/editor/vscode + +patchedDependencies: + '@hokify/agenda@6.3.0': patches/@hokify__agenda@6.3.0.patch From dac8d090338723bfc4c2b133a8a5ce1b08c813c1 Mon Sep 17 00:00:00 2001 From: Disviel Date: Mon, 26 Jan 2026 20:06:13 +0800 Subject: [PATCH 04/16] feat: optimized the front-end initialization logic --- packages/ema-ui/src/app/chat/page.module.css | 29 +++---- packages/ema-ui/src/app/chat/page.tsx | 81 +++++++++++++------- 2 files changed, 69 insertions(+), 41 deletions(-) diff --git a/packages/ema-ui/src/app/chat/page.module.css b/packages/ema-ui/src/app/chat/page.module.css index ce1ab310..01101495 100644 --- a/packages/ema-ui/src/app/chat/page.module.css +++ b/packages/ema-ui/src/app/chat/page.module.css @@ -44,26 +44,27 @@ } .snapshotStatus { - max-width: 800px; - margin: -1rem auto 1rem; + position: fixed; + left: 50%; + top: 1rem; + transform: translateX(-50%); + max-width: 720px; + width: calc(100% - 2rem); + margin: 0; padding: 0.75rem 1rem; border-radius: 12px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.12); - color: #cdd6f7; + background: #cdd6f7; + border: 1px solid rgba(0, 0, 0, 0.12); + color: #30354a; font-size: 0.9rem; - animation: toastFade 3s ease forwards; + text-align: center; + animation: toastFade 2.8s ease forwards; + z-index: 30; + pointer-events: none; } @keyframes toastFade { - 0% { - opacity: 0; - transform: translateY(-4px); - } - 15% { - opacity: 1; - transform: translateY(0); - } + 0%, 70% { opacity: 1; } diff --git a/packages/ema-ui/src/app/chat/page.tsx b/packages/ema-ui/src/app/chat/page.tsx index 08ec477a..0279c3a8 100644 --- a/packages/ema-ui/src/app/chat/page.tsx +++ b/packages/ema-ui/src/app/chat/page.tsx @@ -4,12 +4,16 @@ import { useState, useEffect, useRef } from "react"; import styles from "./page.module.css"; import type { ActorAgentEvent, Message } from "ema"; +let initialLoadPromise: Promise | null = null; +let initialMessagesCache: Message[] | null = null; + // todo: consider adding tests for this component to verify message state management export default function ChatPage() { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); + const [initializing, setInitializing] = useState(true); const [snapshotting, setSnapshotting] = useState(false); - const [snapshotStatus, setSnapshotStatus] = useState(null); + const [notice, setNotice] = useState(null); const chatAreaRef = useRef(null); const messagesEndRef = useRef(null); const shouldAutoScrollRef = useRef(true); @@ -21,24 +25,48 @@ export default function ChatPage() { let isActive = true; const init = async () => { + setInitializing(true); try { - await fetch("/api/users/login"); - } catch (error) { - console.error("Error logging in:", error); - } + if (!initialLoadPromise) { + initialLoadPromise = (async () => { + try { + await fetch("/api/users/login"); + } catch (error) { + console.error("Error logging in:", error); + } - try { - const response = await fetch( - "/api/conversations/messages?conversationId=1&limit=100", - ); - if (response.ok) { - const data = (await response.json()) as { messages: Message[] }; - if (isActive && Array.isArray(data.messages)) { - setMessages(data.messages); - } + try { + const response = await fetch( + "/api/conversations/messages?conversationId=1&limit=100", + ); + if (response.ok) { + const data = (await response.json()) as { messages: Message[] }; + if (Array.isArray(data.messages)) { + return data.messages; + } + } + } catch (error) { + console.error("Error loading history:", error); + } + return null; + })().catch((error) => { + initialLoadPromise = null; + throw error; + }); + } + + if (!initialMessagesCache) { + initialMessagesCache = await initialLoadPromise; + } + + if (isActive && Array.isArray(initialMessagesCache)) { + setMessages(initialMessagesCache); + setNotice("History loaded from snapshot."); + } + } finally { + if (isActive) { + setInitializing(false); } - } catch (error) { - console.error("Error loading history:", error); } if (!isActive) { @@ -101,14 +129,14 @@ export default function ChatPage() { }, [messages.length]); useEffect(() => { - if (!snapshotStatus) { + if (!notice) { return; } if (snapshotTimerRef.current) { clearTimeout(snapshotTimerRef.current); } snapshotTimerRef.current = setTimeout(() => { - setSnapshotStatus(null); + setNotice(null); snapshotTimerRef.current = null; }, 3200); return () => { @@ -117,7 +145,7 @@ export default function ChatPage() { snapshotTimerRef.current = null; } }; - }, [snapshotStatus]); + }, [notice]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -172,7 +200,7 @@ export default function ChatPage() { }; const handleSnapshot = async () => { - setSnapshotStatus(null); + setNotice(null); setSnapshotting(true); try { const response = await fetch("/api/snapshot", { @@ -182,20 +210,20 @@ export default function ChatPage() { }); if (!response.ok) { const text = await response.text(); - setSnapshotStatus(text || "Snapshot failed."); + setNotice(text || "Snapshot failed."); return; } const data = (await response.json().catch(() => null)) as { fileName?: string; } | null; - setSnapshotStatus( + setNotice( data?.fileName ? `Snapshot saved: ${data.fileName}` : "Snapshot created.", ); } catch (error) { console.error("Snapshot error:", error); - setSnapshotStatus("Snapshot failed."); + setNotice("Snapshot failed."); } finally { setSnapshotting(false); } @@ -215,9 +243,7 @@ export default function ChatPage() { {snapshotting ? "Snapshotting..." : "Snapshot"} - {snapshotStatus ? ( -
{snapshotStatus}
- ) : null} + {notice ?
{notice}
: null}
setInputValue(e.target.value)} + disabled={initializing} />