From c047c5a23d27144480c41b8149352a3e94bcedbe Mon Sep 17 00:00:00 2001 From: Shinwoo PARK Date: Thu, 14 May 2026 19:30:34 +0900 Subject: [PATCH] feat: bootstrap BoxBrain framework --- .gitea/workflows/ci.yml | 24 +++ .gitignore | 9 + README.md | 163 +++++++++++++++ bun.lock | 412 +++++++++++++++++++++++++++++++++++++ package.json | 33 +++ src/conversation.ts | 64 ++++++ src/index.ts | 5 + src/memory.ts | 178 ++++++++++++++++ src/persona.ts | 306 +++++++++++++++++++++++++++ src/schedule.ts | 211 +++++++++++++++++++ src/types.ts | 174 ++++++++++++++++ tests/conversation.test.ts | 117 +++++++++++ tests/persona.test.ts | 43 ++++ tests/schedule.test.ts | 50 +++++ tests/sleep-memory.test.ts | 38 ++++ tsconfig.json | 19 ++ 16 files changed, 1846 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 package.json create mode 100644 src/conversation.ts create mode 100644 src/index.ts create mode 100644 src/memory.ts create mode 100644 src/persona.ts create mode 100644 src/schedule.ts create mode 100644 src/types.ts create mode 100644 tests/conversation.test.ts create mode 100644 tests/persona.test.ts create mode 100644 tests/schedule.test.ts create mode 100644 tests/sleep-memory.test.ts create mode 100644 tsconfig.json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..88068ec --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + verify: + runs-on: ubuntu-latest + container: + image: oven/bun:1 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Test + run: bun run test + - name: Typecheck + run: bun run check + - name: Build + run: bun run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e946575 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +coverage/ +.env +.env.* +!.env.example +*.log +.data/ +.hermes/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9d9f98 --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# BoxBrain + +BoxBrain is a TypeScript framework for designing LLM harnesses that make a persona feel like a real person, backed by IdentityDB memory spaces. + +This repository was reset from scratch. The current implementation focuses on a clean framework core rather than a concrete chatbot product. + +## Goals + +BoxBrain helps API users build an LLM-driven persona with: + +- persona initialization into an isolated IdentityDB-backed space +- realistic schedule generation +- schedule-derived availability (`online`, `do-not-disturb`, `offline`) +- reply and proactive conversation APIs +- sleep-time memory extraction into durable facts +- debug hooks that expose the framework flow and persona reasoning pipeline + +## Install + +```bash +bun install +``` + +## Core API + +```ts +import { Persona, createSqliteIdentityMemoryStore } from 'boxbrain'; + +const memory = await createSqliteIdentityMemoryStore('.data/mina.sqlite'); + +const persona = new Persona( + 'Mina', + 'Mina is a careful student who likes quiet cafes and is preparing for exams.', + { + memory, + models: { + conversation: yourConversationModel, + memoryExtraction: yourMemoryExtractionModel, + }, + debug: (event) => console.log(event), + }, +); + +const space = await persona.ready(); +``` + +A persona can also be loaded from an existing space: + +```ts +const persona = new Persona(space.id, { memory, models }); +await persona.ready(); +``` + +## Persona initialization + +`new Persona(displayName, message, options)` creates a new isolated persona space. The seed message is the single freeform place for personality, history, likes, dislikes, relationships, and other facts about the persona. + +`new Persona(spaceId, options)` loads an existing persona space. + +If `models.initialization` is provided, BoxBrain asks it for initial facts. If no initialization model is provided, BoxBrain stores a minimal seed fact about the persona. If the model intentionally returns an empty list, no fallback fact is stored. + +## Schedule API + +```ts +await persona.createDailySchedule(now, 'Keep a normal work day.'); +await persona.createMonthlySchedule(now, 'Mostly study, with occasional rest.'); +await persona.deleteSchedulesBefore(cutoff); +await persona.deleteSchedulesOlderThan(cutoff); +``` + +`createDailySchedule(datetime, message)` creates tomorrow's schedule in 10-minute blocks. For example, if `datetime` is May 1, the generated daily schedule covers May 2 00:00 through May 3 00:00. + +`createMonthlySchedule(datetime, message)` creates day-level schedule outlines for the next 30 days. + +Schedules are stored through the configured memory store. The IdentityDB store records schedule entries as facts under schedule-related topics. + +## Availability API + +```ts +const availability = await persona.getTodayScheduledAvailability(now); +``` + +Availability is derived from schedule entries and kept in memory rather than persisted separately. The window covers today 00:00 through tomorrow 24:00. When the date changes, BoxBrain rebuilds the snapshot from schedule entries in memory. + +Schedule activities map to availability roughly as: + +- `sleep` → `offline` +- `work`, `study`, `job-search`, `travel`, `commute` → `do-not-disturb` +- rest, meals, exercise, errands, social time, free time → `online` + +## Conversation API + +```ts +const reply = await persona.sendMessage({ + datetime: now, + messageHistory: [ + { sender: 'persona', time: yesterday, content: 'See you later.' }, + { sender: 'user', time: now, content: 'What are you doing?' }, + ], +}); + +const opener = await persona.startConversation({ + datetime: now, + messageHistory: [], +}); +``` + +Before generating a reply, BoxBrain always loads mandatory context: + +- formatted yesterday/today message history supplied by the API user +- yesterday, today, and tomorrow schedule entries +- current schedule-derived availability +- IdentityDB facts related to the persona and the user + +If no relevant mandatory memory is found, the model context explicitly says `기억이 없음` so the persona can react naturally instead of pretending to remember. + +Conversation models return one or more outgoing messages. The framework instruction tells the model to behave like a `send_message` tool and, unless the persona prefers otherwise, keep each message to at most one sentence. + +If `getLatestMessageHistory` and `models.rewrite` are provided, BoxBrain can detect messages that arrived while a draft was being generated and ask the rewrite model whether to discard and regenerate the stale draft. + +## sleepMemory + +```ts +await persona.sleepMemory({ + datetime: '2026-05-02T00:00:00.000Z', + messageHistory: messagesFromMay1, +}); +``` + +`sleepMemory` asks `models.memoryExtraction` to inspect the provided message history, objectivize durable facts, and persist them through the memory store. The recommended cadence is daily around midnight, passing the previous day's messages. + +## Debug hooks + +Every major pipeline step can emit a debug event: + +```ts +const persona = new Persona('Mina', seed, { + debug(event) { + // Write to a file, messenger, trace UI, etc. + console.log(event.name, event.data); + }, +}); +``` + +Examples include: + +- `persona.initialized` +- `persona.loaded` +- `persona.schedule.daily.generated` +- `persona.availability.refreshed` +- `persona.conversation.context.loaded` +- `persona.conversation.rewrite.checked` +- `persona.memory.sleep.persisted` + +## Development + +```bash +bun run test +bun run check +bun run build +``` + +The current test suite covers persona creation/loading, schedule generation/pruning, availability derivation, conversation context assembly, stale-draft rewrite checks, proactive conversation starts, and sleep-memory persistence. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..95afdf3 --- /dev/null +++ b/bun.lock @@ -0,0 +1,412 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "boxbrain", + "dependencies": { + "identitydb": "0.2.1", + }, + "devDependencies": { + "@types/bun": "latest", + "tsup": "latest", + "typescript": "latest", + "vitest": "latest", + }, + }, + }, + "trustedDependencies": [ + "esbuild", + ], + "packages": { + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.3", "", { "os": "android", "cpu": "arm" }, "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.3", "", { "os": "android", "cpu": "arm64" }, "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.3", "", { "os": "none", "cpu": "arm64" }, "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="], + + "@vitest/expect": ["@vitest/expect@4.1.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.6", "", { "dependencies": { "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.6", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw=="], + + "@vitest/runner": ["@vitest/runner@4.1.6", "", { "dependencies": { "@vitest/utils": "4.1.6", "pathe": "^2.0.3" } }, "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw=="], + + "@vitest/spy": ["@vitest/spy@4.1.6", "", {}, "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg=="], + + "@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "identitydb": ["identitydb@0.2.1", "", { "dependencies": { "kysely": "^0.28.8", "mysql2": "^3.15.3", "pg": "^8.16.0" } }, "sha512-e+caNqI7F6JaqgyIFQbdiT5/2Frs5PEJgy3mmF+qUVspHZ4z6QtFF5jonDnpYtJpZ9guPWfeQ/xteeaiwOJ5zA=="], + + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mysql2": ["mysql2@3.22.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], + + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + + "pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], + + "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "rolldown": ["rolldown@1.0.0", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@rolldown/pluginutils": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-x64-msvc": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA=="], + + "rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + + "vite": ["vite@8.0.12", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg=="], + + "vitest": ["vitest@4.1.6", "", { "dependencies": { "@vitest/expect": "4.1.6", "@vitest/mocker": "4.1.6", "@vitest/pretty-format": "4.1.6", "@vitest/runner": "4.1.6", "@vitest/snapshot": "4.1.6", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.6", "@vitest/browser-preview": "4.1.6", "@vitest/browser-webdriverio": "4.1.6", "@vitest/coverage-istanbul": "4.1.6", "@vitest/coverage-v8": "4.1.6", "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "vitest/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..95a9ae3 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "boxbrain", + "version": "0.1.0", + "description": "Human-like persona harness framework powered by LLMs and IdentityDB.", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "scripts": { + "test": "vitest run", + "check": "tsc --noEmit", + "build": "tsup src/index.ts --format esm --dts --sourcemap --clean" + }, + "dependencies": { + "identitydb": "0.2.1" + }, + "devDependencies": { + "@types/bun": "latest", + "tsup": "latest", + "typescript": "latest", + "vitest": "latest" + }, + "trustedDependencies": [ + "esbuild" + ], + "engines": { + "bun": ">=1.2.0" + } +} diff --git a/src/conversation.ts b/src/conversation.ts new file mode 100644 index 0000000..21fa5b0 --- /dev/null +++ b/src/conversation.ts @@ -0,0 +1,64 @@ +import type { + BoxBrainMemoryStore, + DateTimeInput, + MandatoryConversationContext, + MemorySpace, + PersonaMessage, + ScheduledAvailabilitySnapshot, +} from './types'; +import { addUtcDays, buildAvailabilitySnapshot, dateKeysAround, startOfUtcDay, toIso } from './schedule'; + +export function formatMessageHistory(input: { personaName: string; messages: PersonaMessage[] }): string { + return input.messages + .map((message) => { + const sender = message.sender === 'persona' ? input.personaName : 'user'; + return `${sender}@${toIso(message.time)}: ${message.content}`; + }) + .join('\n'); +} + +export function conversationInstruction(): string { + return [ + 'You are controlling the persona, not a generic assistant.', + 'Use the send_message tool conceptually: return one or more outgoing messages.', + 'Unless the persona strongly prefers otherwise, keep each outgoing message to at most one sentence.', + 'Prefer short, natural, chat-like wording and allow splitting one thought into multiple messages.', + 'If mandatory memory says "기억이 없음", the persona may naturally wonder about missing context instead of pretending to remember.', + ].join('\n'); +} + +export function memoryExtractionInstruction(now: string): string { + return [ + `Current objective time: ${now}.`, + 'Read the message history and extract durable facts worth remembering.', + 'Objectivize subjective statements before storage.', + 'Example: "I started TypeScript in 2025" becomes "The user started TypeScript in 2025."', + 'Prefer facts about the persona, the user, their relationship, preferences, history, schedule-relevant events, and stable traits.', + ].join('\n'); +} + +export async function buildMandatoryConversationContext(input: { + persona: MemorySpace; + now: DateTimeInput; + memory: BoxBrainMemoryStore; + messages: PersonaMessage[]; + availability: ScheduledAvailabilitySnapshot; +}): Promise { + const now = startOfUtcDay(input.now); + const from = addUtcDays(now, -1).toISOString(); + const to = addUtcDays(now, 2).toISOString(); + const scheduleEntries = await input.memory.listScheduleEntries(input.persona.id, from, to); + const personaAndUserFacts = await input.memory.findFacts(input.persona.id, ['persona', input.persona.displayName, 'user']); + const memorySummary = personaAndUserFacts.length === 0 + ? '기억이 없음' + : personaAndUserFacts.map((fact) => `- ${fact.statement}`).join('\n'); + + return { + formattedMessageHistory: formatMessageHistory({ personaName: input.persona.displayName, messages: input.messages }), + conversationWindowLabel: `Required conversation window: yesterday/today. Schedule dates: ${dateKeysAround(input.now).join(', ')}.`, + memorySummary, + personaAndUserFacts, + scheduleEntries, + availability: input.availability, + }; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f886c1e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export * from './memory'; +export * from './schedule'; +export * from './conversation'; +export * from './persona'; diff --git a/src/memory.ts b/src/memory.ts new file mode 100644 index 0000000..c5d634b --- /dev/null +++ b/src/memory.ts @@ -0,0 +1,178 @@ +import { IdentityDB, type Fact as IdentityFact } from 'identitydb'; +import type { BoxBrainMemoryStore, FactDraft, MemorySpace, ScheduleEntry, StoredFact } from './types'; + +function normalizeTopics(topics: string[]): string[] { + return [...new Set(topics.map((topic) => topic.trim()).filter(Boolean))]; +} + +function includesAnyTopic(fact: StoredFact, topics: string[]): boolean { + const normalized = new Set(normalizeTopics(topics).map((topic) => topic.toLowerCase())); + return fact.topics.some((topic) => normalized.has(topic.toLowerCase())); +} + +export class InMemoryMemoryStore implements BoxBrainMemoryStore { + readonly spaces = new Map(); + readonly facts = new Map(); + readonly schedules = new Map(); + + async createSpace(input: { displayName: string; seedMessage: string; now: string }): Promise { + const slug = input.displayName.toLowerCase().replace(/[^a-z0-9가-힣]+/gi, '-').replace(/^-|-$/g, '') || 'persona'; + const space: MemorySpace = { + id: `persona-${slug}-${crypto.randomUUID()}`, + displayName: input.displayName, + createdAt: input.now, + metadata: { seedMessage: input.seedMessage }, + }; + this.spaces.set(space.id, space); + return space; + } + + async getSpace(spaceId: string): Promise { + return this.spaces.get(spaceId) ?? null; + } + + async addFact(spaceId: string, fact: FactDraft): Promise { + const stored: StoredFact = { + id: crypto.randomUUID(), + statement: fact.statement, + topics: normalizeTopics(fact.topics), + createdAt: new Date().toISOString(), + ...(fact.confidence === undefined ? {} : { confidence: fact.confidence }), + ...(fact.source === undefined ? {} : { source: fact.source }), + ...(fact.metadata === undefined ? {} : { metadata: fact.metadata }), + }; + const existing = this.facts.get(spaceId) ?? []; + existing.push(stored); + this.facts.set(spaceId, existing); + return stored; + } + + async findFacts(spaceId: string, topics: string[]): Promise { + const facts = this.facts.get(spaceId) ?? []; + if (topics.length === 0) return [...facts]; + return facts.filter((fact) => includesAnyTopic(fact, topics)); + } + + async saveScheduleEntries(spaceId: string, entries: ScheduleEntry[]): Promise { + const existing = (this.schedules.get(spaceId) ?? []).filter((entry) => !entries.some((incoming) => incoming.id === entry.id)); + this.schedules.set(spaceId, [...existing, ...entries].sort((a, b) => a.startAt.localeCompare(b.startAt))); + } + + async listScheduleEntries(spaceId: string, fromInclusive: string, toExclusive: string): Promise { + return (this.schedules.get(spaceId) ?? []) + .filter((entry) => entry.startAt < toExclusive && entry.endAt > fromInclusive) + .sort((a, b) => a.startAt.localeCompare(b.startAt)); + } + + async deleteScheduleEntriesBefore(spaceId: string, cutoffExclusive: string): Promise { + const entries = this.schedules.get(spaceId) ?? []; + const kept = entries.filter((entry) => entry.endAt > cutoffExclusive); + this.schedules.set(spaceId, kept); + return entries.length - kept.length; + } +} + +export interface IdentityDbMemoryStoreOptions { + db: IdentityDB; +} + +export class IdentityDbMemoryStore implements BoxBrainMemoryStore { + constructor(private readonly options: IdentityDbMemoryStoreOptions) {} + + async createSpace(input: { displayName: string; seedMessage: string; now: string }): Promise { + const slug = input.displayName.toLowerCase().replace(/[^a-z0-9가-힣]+/gi, '-').replace(/^-|-$/g, '') || 'persona'; + const spaceName = `persona-${slug}-${crypto.randomUUID()}`; + const space = await this.options.db.upsertSpace({ + name: spaceName, + description: `BoxBrain persona space for ${input.displayName}`, + metadata: { + boxbrainType: 'persona-space', + displayName: input.displayName, + seedMessage: input.seedMessage, + createdAt: input.now, + }, + }); + return { + id: space.name, + displayName: input.displayName, + createdAt: space.createdAt, + metadata: { seedMessage: input.seedMessage, identityDbSpaceId: space.id }, + }; + } + + async getSpace(spaceId: string): Promise { + const space = await this.options.db.getSpaceByName(spaceId); + if (!space) return null; + const metadata = typeof space.metadata === 'object' && space.metadata !== null && !Array.isArray(space.metadata) ? space.metadata as Record : {}; + const displayName = typeof metadata['displayName'] === 'string' ? metadata['displayName'] : space.name; + return { id: space.name, displayName, createdAt: space.createdAt, metadata }; + } + + async addFact(spaceId: string, fact: FactDraft): Promise { + const stored = await this.options.db.addFact({ + spaceName: spaceId, + statement: fact.statement, + confidence: fact.confidence ?? null, + source: fact.source ?? null, + metadata: (fact.metadata ?? null) as never, + topics: normalizeTopics(fact.topics).map((topic) => ({ name: topic, category: 'entity' as const, granularity: 'concrete' as const })), + }); + return this.fromIdentityFact(stored); + } + + async findFacts(spaceId: string, topics: string[]): Promise { + const uniqueTopics = normalizeTopics(topics); + const collected = new Map(); + for (const topic of uniqueTopics) { + const facts = await this.options.db.getTopicFacts(topic, { spaceName: spaceId }); + for (const fact of facts) { + collected.set(fact.id, this.fromIdentityFact(fact)); + } + } + return [...collected.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt)); + } + + async saveScheduleEntries(spaceId: string, entries: ScheduleEntry[]): Promise { + for (const entry of entries) { + await this.addFact(spaceId, { + statement: `${entry.title} from ${entry.startAt} to ${entry.endAt}.`, + topics: ['schedule', entry.startAt.slice(0, 10), entry.activity, 'persona'], + source: 'boxbrain.schedule', + metadata: { ...entry.metadata, scheduleEntry: entry }, + }); + } + } + + async listScheduleEntries(spaceId: string, fromInclusive: string, toExclusive: string): Promise { + const facts = await this.findFacts(spaceId, ['schedule']); + return facts + .map((fact) => fact.metadata?.['scheduleEntry']) + .filter((value): value is ScheduleEntry => typeof value === 'object' && value !== null && !Array.isArray(value)) + .filter((entry) => entry.startAt < toExclusive && entry.endAt > fromInclusive) + .sort((a, b) => a.startAt.localeCompare(b.startAt)); + } + + async deleteScheduleEntriesBefore(_spaceId: string, _cutoffExclusive: string): Promise { + // IdentityDB is append-oriented at the public API level. Record schedule deletion as a fact at the Persona layer. + return 0; + } + + private fromIdentityFact(fact: IdentityFact): StoredFact { + const metadata = typeof fact.metadata === 'object' && fact.metadata !== null && !Array.isArray(fact.metadata) ? fact.metadata as Record : undefined; + return { + id: fact.id, + statement: fact.statement, + topics: fact.topics.map((topic) => topic.name), + createdAt: fact.createdAt, + ...(fact.confidence === null ? {} : { confidence: fact.confidence }), + ...(fact.source === null ? {} : { source: fact.source }), + ...(metadata === undefined ? {} : { metadata }), + }; + } +} + +export async function createSqliteIdentityMemoryStore(filename: string): Promise { + const db = await IdentityDB.connect({ client: 'sqlite', filename }); + await db.initialize(); + return new IdentityDbMemoryStore({ db }); +} diff --git a/src/persona.ts b/src/persona.ts new file mode 100644 index 0000000..36d732e --- /dev/null +++ b/src/persona.ts @@ -0,0 +1,306 @@ +import { InMemoryMemoryStore } from './memory'; +import { + addUtcDays, + buildAvailabilitySnapshot, + createMonthlyScheduleEntries, + createTenMinuteDailySchedule, + scheduleTargetDay, + startOfUtcDay, + toIso, +} from './schedule'; +import { + buildMandatoryConversationContext, + conversationInstruction, + formatMessageHistory, + memoryExtractionInstruction, +} from './conversation'; +import type { + BoxBrainMemoryStore, + DateTimeInput, + DebugEvent, + FactDraft, + MemorySpace, + OutgoingMessageDraft, + PersonaMessage, + PersonaOptions, + ScheduleEntry, + ScheduledAvailabilitySnapshot, +} from './types'; + +interface CreateMode { + type: 'create'; + displayName: string; + seedMessage: string; +} + +interface LoadMode { + type: 'load'; + spaceId: string; +} + +type Mode = CreateMode | LoadMode; + +function defaultInitialFact(displayName: string, seedMessage: string): FactDraft { + return { + statement: `${displayName} is a BoxBrain persona initialized from this seed: ${seedMessage}`, + topics: ['persona', displayName], + source: 'boxbrain.persona.initialization', + confidence: 1, + metadata: { boxbrainType: 'persona-initial-fact' }, + }; +} + +function ensureDraft(draft: OutgoingMessageDraft): OutgoingMessageDraft { + return { + messages: draft.messages.map((message) => message.trim()).filter(Boolean), + ...(draft.reasoning === undefined ? {} : { reasoning: draft.reasoning }), + }; +} + +export class Persona { + private readonly memory: BoxBrainMemoryStore; + private readonly options: PersonaOptions; + private readonly mode: Mode; + private readonly readyPromise: Promise; + private availabilitySnapshot?: ScheduledAvailabilitySnapshot; + + constructor(displayName: string, seedMessage: string, options?: PersonaOptions); + constructor(spaceId: string, options?: PersonaOptions); + constructor(first: string, second?: string | PersonaOptions, third?: PersonaOptions) { + if (typeof second === 'string') { + this.mode = { type: 'create', displayName: first, seedMessage: second }; + this.options = third ?? {}; + } else { + this.mode = { type: 'load', spaceId: first }; + this.options = second ?? {}; + } + this.memory = this.options.memory ?? new InMemoryMemoryStore(); + this.readyPromise = this.initialize(); + } + + async ready(): Promise { + return this.readyPromise; + } + + async createDailySchedule(datetime: DateTimeInput, message: string): Promise { + const persona = await this.ready(); + const targetDay = scheduleTargetDay(datetime); + const entries = createTenMinuteDailySchedule({ persona, targetDay, message }); + await this.emit('persona.schedule.daily.generated', { targetDay: targetDay.toISOString(), count: entries.length, message }); + await this.memory.saveScheduleEntries(persona.id, entries); + await this.refreshAvailability(datetime); + return entries; + } + + async createMonthlySchedule(datetime: DateTimeInput, message: string): Promise { + const persona = await this.ready(); + const entries = createMonthlyScheduleEntries({ persona, fromDay: datetime, message }); + await this.emit('persona.schedule.monthly.generated', { count: entries.length, message }); + await this.memory.saveScheduleEntries(persona.id, entries); + await this.refreshAvailability(datetime); + return entries; + } + + async deleteSchedulesBefore(cutoffExclusive: DateTimeInput): Promise { + const persona = await this.ready(); + const cutoff = toIso(cutoffExclusive); + const deleted = await this.memory.deleteScheduleEntriesBefore(persona.id, cutoff); + await this.memory.addFact(persona.id, { + statement: `Schedules before ${cutoff} were deleted or marked inactive.`, + topics: ['persona.schedule.deleted', 'schedule', cutoff.slice(0, 10)], + source: 'boxbrain.schedule.prune', + metadata: { boxbrainType: 'schedule-deletion', cutoffExclusive: cutoff, deleted }, + }); + await this.emit('persona.schedule.deleted', { cutoffExclusive: cutoff, deleted }); + return deleted; + } + + async deleteSchedulesOlderThan(datetime: DateTimeInput): Promise { + return this.deleteSchedulesBefore(datetime); + } + + async getTodayScheduledAvailability(datetime: DateTimeInput): Promise { + if (!this.availabilitySnapshot) { + await this.refreshAvailability(datetime); + } + const snapshot = this.availabilitySnapshot; + if (!snapshot) throw new Error('Availability snapshot was not initialized.'); + + const today = startOfUtcDay(datetime).toISOString(); + if (snapshot.windowStartAt !== today) { + await this.refreshAvailability(datetime); + } + + const refreshed = this.availabilitySnapshot; + if (!refreshed) throw new Error('Availability snapshot was not initialized.'); + return refreshed; + } + + async sendMessage(input: { + datetime: DateTimeInput; + messageHistory: PersonaMessage[]; + getLatestMessageHistory?: () => Promise; + }): Promise { + const persona = await this.ready(); + if (!this.options.models?.conversation) { + throw new Error('sendMessage requires options.models.conversation.'); + } + const availability = await this.getTodayScheduledAvailability(input.datetime); + const context = await buildMandatoryConversationContext({ + persona, + now: input.datetime, + memory: this.memory, + messages: input.messageHistory, + availability, + }); + await this.emit('persona.conversation.context.loaded', { + factCount: context.personaAndUserFacts.length, + scheduleEntryCount: context.scheduleEntries.length, + }); + + const userMessage = [...input.messageHistory].reverse().find((message) => message.sender === 'user')?.content; + let draft = ensureDraft(await this.options.models.conversation.generateReply({ + persona, + now: toIso(input.datetime), + mode: 'reply', + context, + ...(userMessage === undefined ? {} : { userMessage }), + instruction: conversationInstruction(), + })); + + if (input.getLatestMessageHistory && this.options.models.rewrite) { + const latest = await input.getLatestMessageHistory(); + if (latest.length > input.messageHistory.length) { + const latestContext = await buildMandatoryConversationContext({ + persona, + now: input.datetime, + memory: this.memory, + messages: latest, + availability, + }); + const decision = await this.options.models.rewrite.decide({ + persona, + now: toIso(input.datetime), + previousHistory: input.messageHistory, + latestHistory: latest, + draft, + context: latestContext, + }); + await this.emit('persona.conversation.rewrite.checked', { rewrite: decision.rewrite, reason: decision.reason ?? null }); + if (decision.rewrite) { + draft = ensureDraft(decision.draft ?? await this.options.models.conversation.generateReply({ + persona, + now: toIso(input.datetime), + mode: 'reply', + context: latestContext, + instruction: conversationInstruction(), + })); + } + } + } + + await this.emit('persona.conversation.reply.generated', { messageCount: draft.messages.length }); + return draft; + } + + async startConversation(input: { + datetime: DateTimeInput; + messageHistory: PersonaMessage[]; + }): Promise { + const persona = await this.ready(); + if (!this.options.models?.conversation) { + throw new Error('startConversation requires options.models.conversation.'); + } + const availability = await this.getTodayScheduledAvailability(input.datetime); + const context = await buildMandatoryConversationContext({ + persona, + now: input.datetime, + memory: this.memory, + messages: input.messageHistory, + availability, + }); + const draft = ensureDraft(await this.options.models.conversation.generateReply({ + persona, + now: toIso(input.datetime), + mode: 'start-conversation', + context, + instruction: conversationInstruction(), + })); + await this.emit('persona.conversation.started', { messageCount: draft.messages.length }); + return draft; + } + + async sleepMemory(input: { + datetime: DateTimeInput; + messageHistory: PersonaMessage[]; + }): Promise { + const persona = await this.ready(); + if (!this.options.models?.memoryExtraction) { + throw new Error('sleepMemory requires options.models.memoryExtraction.'); + } + const contextFacts = await this.memory.findFacts(persona.id, ['persona', persona.displayName, 'user']); + const drafts = await this.options.models.memoryExtraction.extract({ + persona, + now: toIso(input.datetime), + formattedMessageHistory: formatMessageHistory({ personaName: persona.displayName, messages: input.messageHistory }), + contextFacts, + instruction: memoryExtractionInstruction(toIso(input.datetime)), + }); + for (const draft of drafts) { + await this.memory.addFact(persona.id, { + ...draft, + topics: [...draft.topics, 'sleepMemory'], + source: draft.source ?? 'boxbrain.sleepMemory', + }); + } + await this.emit('persona.memory.sleep.persisted', { factCount: drafts.length }); + return drafts; + } + + private async initialize(): Promise { + const now = toIso(this.options.now ?? new Date()); + if (this.mode.type === 'load') { + const existing = await this.memory.getSpace(this.mode.spaceId); + if (!existing) throw new Error(`Persona space not found: ${this.mode.spaceId}`); + await this.emit('persona.loaded', { displayName: existing.displayName }); + await this.refreshAvailability(now, existing); + return existing; + } + + const space = await this.memory.createSpace({ displayName: this.mode.displayName, seedMessage: this.mode.seedMessage, now }); + const modelFacts = this.options.models?.initialization + ? await this.options.models.initialization.extractInitialFacts({ + displayName: this.mode.displayName, + seedMessage: this.mode.seedMessage, + now, + }) + : undefined; + const facts = modelFacts ?? [defaultInitialFact(this.mode.displayName, this.mode.seedMessage)]; + for (const fact of facts) { + await this.memory.addFact(space.id, fact); + } + await this.emit('persona.initialized', { displayName: space.displayName, factCount: facts.length }, space.id); + await this.refreshAvailability(now, space); + return space; + } + + private async refreshAvailability(datetime: DateTimeInput, knownPersona?: MemorySpace): Promise { + const persona = knownPersona ?? await this.ready(); + const start = startOfUtcDay(datetime); + const end = addUtcDays(start, 2); + const entries = await this.memory.listScheduleEntries(persona.id, start.toISOString(), end.toISOString()); + this.availabilitySnapshot = buildAvailabilitySnapshot({ now: datetime, entries }); + await this.emit('persona.availability.refreshed', { rangeCount: this.availabilitySnapshot.ranges.length }, persona.id); + } + + private async emit(name: string, data?: Record, explicitSpaceId?: string): Promise { + if (!this.options.debug) return; + const event: DebugEvent = { + name, + time: new Date().toISOString(), + ...(explicitSpaceId === undefined ? {} : { spaceId: explicitSpaceId }), + ...(data === undefined ? {} : { data }), + }; + await this.options.debug(event); + } +} diff --git a/src/schedule.ts b/src/schedule.ts new file mode 100644 index 0000000..489f0b1 --- /dev/null +++ b/src/schedule.ts @@ -0,0 +1,211 @@ +import type { + AvailabilityMode, + AvailabilityRange, + DateTimeInput, + MemorySpace, + ScheduleActivity, + ScheduleEntry, + ScheduledAvailabilitySnapshot, +} from './types'; + +const TEN_MINUTES_MS = 10 * 60 * 1000; +const DAY_MS = 24 * 60 * 60 * 1000; + +export function toDate(input: DateTimeInput): Date { + const date = input instanceof Date ? new Date(input.getTime()) : new Date(input); + if (Number.isNaN(date.getTime())) { + throw new Error(`Invalid datetime: ${String(input)}`); + } + return date; +} + +export function toIso(input: DateTimeInput): string { + return toDate(input).toISOString(); +} + +export function startOfUtcDay(input: DateTimeInput): Date { + const date = toDate(input); + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); +} + +export function addUtcDays(input: DateTimeInput, days: number): Date { + return new Date(startOfUtcDay(input).getTime() + days * DAY_MS); +} + +export function scheduleTargetDay(now: DateTimeInput): Date { + return addUtcDays(now, 1); +} + +function pick(activity: ScheduleActivity): { title: string; mode: AvailabilityMode } { + switch (activity) { + case 'sleep': + return { title: 'Sleep', mode: 'offline' }; + case 'work': + return { title: 'Work', mode: 'do-not-disturb' }; + case 'study': + return { title: 'Study', mode: 'do-not-disturb' }; + case 'job-search': + return { title: 'Job search', mode: 'do-not-disturb' }; + case 'travel': + return { title: 'Travel', mode: 'do-not-disturb' }; + case 'commute': + return { title: 'Commute', mode: 'do-not-disturb' }; + case 'exercise': + return { title: 'Exercise', mode: 'online' }; + case 'meal': + return { title: 'Meal', mode: 'online' }; + case 'social': + return { title: 'Social time', mode: 'online' }; + case 'errand': + return { title: 'Errand', mode: 'online' }; + case 'free-time': + return { title: 'Free time', mode: 'online' }; + case 'rest': + return { title: 'Rest', mode: 'online' }; + } +} + +function chooseDaytimeActivity(message: string): ScheduleActivity { + const lower = message.toLowerCase(); + if (lower.includes('travel') || lower.includes('trip') || lower.includes('여행')) return 'travel'; + if (lower.includes('study') || lower.includes('exam') || lower.includes('공부') || lower.includes('시험')) return 'study'; + if (lower.includes('job') || lower.includes('취업') || lower.includes('구직')) return 'job-search'; + if (lower.includes('work') || lower.includes('일') || lower.includes('회사')) return 'work'; + return 'work'; +} + +function activityForMinute(minuteOfDay: number, message: string): ScheduleActivity { + const hour = Math.floor(minuteOfDay / 60); + if (hour < 7) return 'sleep'; + if (hour === 7) return 'meal'; + if (hour === 8) return 'commute'; + if (hour >= 9 && hour < 12) return chooseDaytimeActivity(message); + if (hour === 12) return 'meal'; + if (hour >= 13 && hour < 17) return chooseDaytimeActivity(message); + if (hour === 17) return 'commute'; + if (hour === 18) return 'meal'; + if (hour >= 19 && hour < 21) return message.toLowerCase().includes('study') || message.includes('공부') ? 'study' : 'free-time'; + if (hour >= 21 && hour < 23) return 'rest'; + return 'sleep'; +} + +export function createTenMinuteDailySchedule(input: { + persona: MemorySpace; + targetDay: DateTimeInput; + message: string; +}): ScheduleEntry[] { + const target = startOfUtcDay(input.targetDay); + const entries: ScheduleEntry[] = []; + + for (let offset = 0; offset < DAY_MS; offset += TEN_MINUTES_MS) { + const start = new Date(target.getTime() + offset); + const end = new Date(start.getTime() + TEN_MINUTES_MS); + const minute = offset / (60 * 1000); + const activity = activityForMinute(minute, input.message); + const picked = pick(activity); + entries.push({ + id: crypto.randomUUID(), + spaceId: input.persona.id, + startAt: start.toISOString(), + endAt: end.toISOString(), + activity, + title: picked.title, + description: `Realistic ${picked.title.toLowerCase()} block for ${input.persona.displayName}.`, + granularity: 'ten-minute', + sourceMessage: input.message, + metadata: { + boxbrainType: 'schedule-entry', + availabilityMode: picked.mode, + targetDate: target.toISOString().slice(0, 10), + }, + }); + } + + return entries; +} + +export function createMonthlyScheduleEntries(input: { + persona: MemorySpace; + fromDay: DateTimeInput; + message: string; + days?: number; +}): ScheduleEntry[] { + const start = scheduleTargetDay(input.fromDay); + const count = input.days ?? 30; + const entries: ScheduleEntry[] = []; + for (let day = 0; day < count; day += 1) { + const dayStart = new Date(start.getTime() + day * DAY_MS); + const travelHint = day > 0 && day % 90 === 0 ? ' travel' : ''; + const activity = chooseDaytimeActivity(`${input.message}${travelHint}`); + const picked = pick(activity); + entries.push({ + id: crypto.randomUUID(), + spaceId: input.persona.id, + startAt: dayStart.toISOString(), + endAt: new Date(dayStart.getTime() + DAY_MS).toISOString(), + activity, + title: picked.title, + description: `Daily outline for ${input.persona.displayName}.`, + granularity: 'day', + sourceMessage: input.message, + metadata: { + boxbrainType: 'schedule-entry', + availabilityMode: picked.mode, + targetDate: dayStart.toISOString().slice(0, 10), + }, + }); + } + return entries; +} + +export function availabilityModeForEntry(entry: ScheduleEntry): AvailabilityMode { + const mode = entry.metadata['availabilityMode']; + if (mode === 'online' || mode === 'do-not-disturb' || mode === 'offline') return mode; + if (entry.activity === 'sleep') return 'offline'; + if (entry.activity === 'work' || entry.activity === 'study' || entry.activity === 'job-search' || entry.activity === 'travel' || entry.activity === 'commute') { + return 'do-not-disturb'; + } + return 'online'; +} + +export function buildAvailabilitySnapshot(input: { + now: DateTimeInput; + generatedAt?: DateTimeInput; + entries: ScheduleEntry[]; +}): ScheduledAvailabilitySnapshot { + const windowStart = startOfUtcDay(input.now); + const windowEnd = new Date(windowStart.getTime() + 2 * DAY_MS); + const sorted = input.entries + .filter((entry) => entry.startAt < windowEnd.toISOString() && entry.endAt > windowStart.toISOString()) + .sort((a, b) => a.startAt.localeCompare(b.startAt)); + + const ranges: AvailabilityRange[] = []; + for (const entry of sorted) { + const mode = availabilityModeForEntry(entry); + const previous = ranges.at(-1); + if (previous && previous.mode === mode && previous.endAt === entry.startAt) { + previous.endAt = entry.endAt; + previous.sourceScheduleIds.push(entry.id); + continue; + } + ranges.push({ + startAt: entry.startAt, + endAt: entry.endAt, + mode, + sourceScheduleIds: [entry.id], + reason: entry.title, + }); + } + + return { + generatedAt: toIso(input.generatedAt ?? input.now), + windowStartAt: windowStart.toISOString(), + windowEndAt: windowEnd.toISOString(), + ranges, + }; +} + +export function dateKeysAround(input: DateTimeInput): string[] { + const today = startOfUtcDay(input); + return [-1, 0, 1].map((offset) => new Date(today.getTime() + offset * DAY_MS).toISOString().slice(0, 10)); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1e088c5 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,174 @@ +export type DateTimeInput = Date | string | number; + +export type PersonaConstructorMode = 'create' | 'load'; + +export type ScheduleGranularity = 'day' | 'ten-minute'; + +export type ScheduleActivity = + | 'sleep' + | 'rest' + | 'meal' + | 'commute' + | 'work' + | 'study' + | 'job-search' + | 'travel' + | 'exercise' + | 'social' + | 'errand' + | 'free-time'; + +export type AvailabilityMode = 'online' | 'do-not-disturb' | 'offline'; + +export interface MemorySpace { + id: string; + displayName: string; + createdAt: string; + metadata: Record; +} + +export interface FactDraft { + statement: string; + topics: string[]; + confidence?: number; + source?: string; + metadata?: Record; +} + +export interface StoredFact extends FactDraft { + id: string; + createdAt: string; +} + +export interface ScheduleEntry { + id: string; + spaceId: string; + startAt: string; + endAt: string; + activity: ScheduleActivity; + title: string; + description?: string; + granularity: ScheduleGranularity; + sourceMessage?: string; + metadata: Record; +} + +export interface AvailabilityRange { + startAt: string; + endAt: string; + mode: AvailabilityMode; + sourceScheduleIds: string[]; + reason: string; +} + +export interface ScheduledAvailabilitySnapshot { + generatedAt: string; + windowStartAt: string; + windowEndAt: string; + ranges: AvailabilityRange[]; +} + +export interface PersonaMessage { + sender: 'persona' | 'user'; + time: DateTimeInput; + content: string; +} + +export interface DebugEvent { + name: string; + time: string; + spaceId?: string; + data?: Record; +} + +export type DebugHook = (event: DebugEvent) => void | Promise; + +export interface MandatoryConversationContext { + formattedMessageHistory: string; + conversationWindowLabel: string; + memorySummary: string; + personaAndUserFacts: StoredFact[]; + scheduleEntries: ScheduleEntry[]; + availability: ScheduledAvailabilitySnapshot; +} + +export interface ReplyGenerationInput { + persona: MemorySpace; + now: string; + mode: 'reply' | 'start-conversation'; + context: MandatoryConversationContext; + userMessage?: string; + instruction: string; +} + +export interface OutgoingMessageDraft { + messages: string[]; + reasoning?: string; +} + +export interface RewriteDecisionInput { + persona: MemorySpace; + now: string; + previousHistory: PersonaMessage[]; + latestHistory: PersonaMessage[]; + draft: OutgoingMessageDraft; + context: MandatoryConversationContext; +} + +export interface RewriteDecision { + rewrite: boolean; + draft?: OutgoingMessageDraft; + reason?: string; +} + +export interface ConversationModel { + generateReply(input: ReplyGenerationInput): Promise; +} + +export interface RewriteModel { + decide(input: RewriteDecisionInput): Promise; +} + +export interface MemoryExtractionInput { + persona: MemorySpace; + now: string; + formattedMessageHistory: string; + contextFacts: StoredFact[]; + instruction: string; +} + +export interface MemoryExtractionModel { + extract(input: MemoryExtractionInput): Promise; +} + +export interface PersonaInitializationModel { + extractInitialFacts(input: { + displayName: string; + seedMessage: string; + now: string; + }): Promise; +} + +export interface PersonaModels { + initialization?: PersonaInitializationModel; + conversation?: ConversationModel; + rewrite?: RewriteModel; + memoryExtraction?: MemoryExtractionModel; +} + +export interface PersonaOptions { + memory?: BoxBrainMemoryStore; + models?: PersonaModels; + debug?: DebugHook; + now?: DateTimeInput; +} + +export interface BoxBrainMemoryStore { + createSpace(input: { displayName: string; seedMessage: string; now: string }): Promise; + getSpace(spaceId: string): Promise; + addFact(spaceId: string, fact: FactDraft): Promise; + findFacts(spaceId: string, topics: string[]): Promise; + saveScheduleEntries(spaceId: string, entries: ScheduleEntry[]): Promise; + listScheduleEntries(spaceId: string, fromInclusive: string, toExclusive: string): Promise; + deleteScheduleEntriesBefore(spaceId: string, cutoffExclusive: string): Promise; +} diff --git a/tests/conversation.test.ts b/tests/conversation.test.ts new file mode 100644 index 0000000..72835cb --- /dev/null +++ b/tests/conversation.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from 'vitest'; +import { InMemoryMemoryStore, Persona, type ReplyGenerationInput } from '../src'; + +describe('Conversation API', () => { + it('loads mandatory memory, schedules, availability, and formatted history before replying', async () => { + const memory = new InMemoryMemoryStore(); + let captured: ReplyGenerationInput | undefined; + const persona = new Persona('Mina', 'Mina likes quiet cafes.', { + memory, + now: '2026-05-01T10:00:00.000Z', + models: { + conversation: { + async generateReply(input) { + captured = input; + return { messages: ['카페에 있었어.', '너는 뭐해?'] }; + }, + }, + }, + }); + const space = await persona.ready(); + await memory.addFact(space.id, { statement: 'The user is close to Mina.', topics: ['user', 'persona'] }); + await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'study for an exam'); + + const reply = await persona.sendMessage({ + datetime: '2026-05-01T12:00:00.000Z', + messageHistory: [ + { sender: 'persona', time: '2026-04-30T23:00:00.000Z', content: '다음에 보자' }, + { sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '지금 뭐해?' }, + ], + }); + + expect(reply.messages).toEqual(['카페에 있었어.', '너는 뭐해?']); + expect(captured?.context.formattedMessageHistory).toContain('Mina@2026-04-30T23:00:00.000Z: 다음에 보자'); + expect(captured?.context.formattedMessageHistory).toContain('user@2026-05-01T12:00:00.000Z: 지금 뭐해?'); + expect(captured?.context.memorySummary).toContain('The user is close to Mina.'); + expect(captured?.context.scheduleEntries.length).toBeGreaterThan(0); + expect(captured?.context.availability.ranges.length).toBeGreaterThan(0); + expect(captured?.instruction).toContain('send_message'); + }); + + it('explicitly tells the response model when mandatory memory is missing', async () => { + const memory = new InMemoryMemoryStore(); + let memorySummary = ''; + const persona = new Persona('Mina', 'Mina is new.', { + memory, + now: '2026-05-01T10:00:00.000Z', + models: { + initialization: { async extractInitialFacts() { return []; } }, + conversation: { + async generateReply(input) { + memorySummary = input.context.memorySummary; + return { messages: ['잘 모르겠어.'] }; + }, + }, + }, + }); + await persona.ready(); + + await persona.sendMessage({ + datetime: '2026-05-01T12:00:00.000Z', + messageHistory: [{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '나 기억해?' }], + }); + + expect(memorySummary).toBe('기억이 없음'); + }); + + it('lets a rewrite model discard a stale draft when new user messages arrive', async () => { + const memory = new InMemoryMemoryStore(); + const persona = new Persona('Mina', 'Mina is concise.', { + memory, + now: '2026-05-01T10:00:00.000Z', + models: { + conversation: { async generateReply() { return { messages: ['첫 답장'] }; } }, + rewrite: { + async decide() { + return { rewrite: true, draft: { messages: ['새 메시지까지 보고 답장'] }, reason: 'latest user message changes intent' }; + }, + }, + }, + }); + await persona.ready(); + + const reply = await persona.sendMessage({ + datetime: '2026-05-01T12:00:00.000Z', + messageHistory: [{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '안녕' }], + getLatestMessageHistory: async () => [ + { sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '안녕' }, + { sender: 'user', time: '2026-05-01T12:00:05.000Z', content: '아 맞다, 지금 바빠?' }, + ], + }); + + expect(reply.messages).toEqual(['새 메시지까지 보고 답장']); + }); + + it('can proactively start a conversation with the same mandatory context pipeline', async () => { + const memory = new InMemoryMemoryStore(); + let mode = ''; + const persona = new Persona('Mina', 'Mina starts soft conversations.', { + memory, + now: '2026-05-01T10:00:00.000Z', + models: { + conversation: { + async generateReply(input) { + mode = input.mode; + return { messages: ['오늘 좀 조용하네.'] }; + }, + }, + }, + }); + await persona.ready(); + + const started = await persona.startConversation({ datetime: '2026-05-01T20:00:00.000Z', messageHistory: [] }); + + expect(mode).toBe('start-conversation'); + expect(started.messages).toEqual(['오늘 좀 조용하네.']); + }); +}); diff --git a/tests/persona.test.ts b/tests/persona.test.ts new file mode 100644 index 0000000..14262ac --- /dev/null +++ b/tests/persona.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { InMemoryMemoryStore, Persona, type FactDraft } from '../src'; + +describe('Persona initialization', () => { + it('creates a new isolated persona space from displayName and seed message', async () => { + const memory = new InMemoryMemoryStore(); + const debug: string[] = []; + const persona = new Persona('Mina', 'Mina is a careful student who likes quiet cafes.', { + memory, + now: '2026-05-01T10:00:00.000Z', + debug: (event) => { debug.push(event.name); }, + models: { + initialization: { + async extractInitialFacts(input): Promise { + return [ + { + statement: `${input.displayName} likes quiet cafes.`, + topics: ['persona', input.displayName], + source: 'test', + }, + ]; + }, + }, + }, + }); + + const space = await persona.ready(); + expect(space.displayName).toBe('Mina'); + expect(space.id).toMatch(/^persona-mina-/); + expect(await memory.findFacts(space.id, ['persona'])).toHaveLength(1); + expect(debug).toContain('persona.initialized'); + }); + + it('loads an existing persona space by space id without creating another space', async () => { + const memory = new InMemoryMemoryStore(); + const created = new Persona('Joon', 'Joon is a freelance designer.', { memory, now: '2026-05-01T10:00:00.000Z' }); + const space = await created.ready(); + + const loaded = new Persona(space.id, { memory, now: '2026-05-01T11:00:00.000Z' }); + await expect(loaded.ready()).resolves.toMatchObject({ id: space.id, displayName: 'Joon' }); + expect(memory.spaces.size).toBe(1); + }); +}); diff --git a/tests/schedule.test.ts b/tests/schedule.test.ts new file mode 100644 index 0000000..67143e0 --- /dev/null +++ b/tests/schedule.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { InMemoryMemoryStore, Persona } from '../src'; + +describe('Persona schedules and availability', () => { + it('creates tomorrow as a ten-minute daily schedule and persists it in memory', async () => { + const memory = new InMemoryMemoryStore(); + const persona = new Persona('Mina', 'Mina works weekdays and studies at night.', { memory, now: '2026-05-01T10:00:00.000Z' }); + const space = await persona.ready(); + + const entries = await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.'); + + expect(entries).toHaveLength(144); + expect(entries[0]).toMatchObject({ + spaceId: space.id, + startAt: '2026-05-02T00:00:00.000Z', + endAt: '2026-05-02T00:10:00.000Z', + granularity: 'ten-minute', + }); + expect(entries.at(-1)?.endAt).toBe('2026-05-03T00:00:00.000Z'); + await expect(memory.listScheduleEntries(space.id, '2026-05-02T00:00:00.000Z', '2026-05-03T00:00:00.000Z')).resolves.toHaveLength(144); + }); + + it('derives online, do-not-disturb, and offline availability from the in-memory schedule window', async () => { + const memory = new InMemoryMemoryStore(); + const persona = new Persona('Mina', 'Mina works weekdays and studies at night.', { memory, now: '2026-05-01T10:00:00.000Z' }); + await persona.ready(); + + await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.'); + const availability = await persona.getTodayScheduledAvailability('2026-05-01T12:00:00.000Z'); + + expect(availability.windowStartAt).toBe('2026-05-01T00:00:00.000Z'); + expect(availability.windowEndAt).toBe('2026-05-03T00:00:00.000Z'); + expect(new Set(availability.ranges.map((range) => range.mode))).toEqual(new Set(['offline', 'online', 'do-not-disturb'])); + expect(availability.ranges.find((range) => range.mode === 'offline')?.startAt).toBe('2026-05-02T00:00:00.000Z'); + }); + + it('prunes schedule entries before a caller-provided cutoff', async () => { + const memory = new InMemoryMemoryStore(); + const persona = new Persona('Mina', 'Mina works weekdays.', { memory, now: '2026-05-01T10:00:00.000Z' }); + const space = await persona.ready(); + await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.'); + + const deleted = await persona.deleteSchedulesBefore('2026-05-02T12:00:00.000Z'); + + expect(deleted).toBe(72); + await expect(memory.listScheduleEntries(space.id, '2026-05-02T00:00:00.000Z', '2026-05-03T00:00:00.000Z')).resolves.toHaveLength(72); + const deletionFacts = await memory.findFacts(space.id, ['persona.schedule.deleted']); + expect(deletionFacts[0]?.metadata?.['deleted']).toBe(72); + }); +}); diff --git a/tests/sleep-memory.test.ts b/tests/sleep-memory.test.ts new file mode 100644 index 0000000..1eb9f54 --- /dev/null +++ b/tests/sleep-memory.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { InMemoryMemoryStore, Persona } from '../src'; + +describe('sleepMemory', () => { + it('objectivizes and persists extracted durable facts through the memory model', async () => { + const memory = new InMemoryMemoryStore(); + const persona = new Persona('Mina', 'Mina remembers stable details.', { + memory, + now: '2026-05-01T10:00:00.000Z', + models: { + memoryExtraction: { + async extract(input) { + expect(input.formattedMessageHistory).toContain('user@2026-05-01T15:00:00.000Z: 나는 타입스크립트를 2025년부터 시작했어'); + expect(input.instruction).toContain('Objectivize'); + return [ + { + statement: 'The user started TypeScript in 2025.', + topics: ['user', 'TypeScript', '2025'], + confidence: 0.9, + }, + ]; + }, + }, + }, + }); + const space = await persona.ready(); + + const drafts = await persona.sleepMemory({ + datetime: '2026-05-02T00:00:00.000Z', + messageHistory: [{ sender: 'user', time: '2026-05-01T15:00:00.000Z', content: '나는 타입스크립트를 2025년부터 시작했어' }], + }); + + expect(drafts).toHaveLength(1); + const facts = await memory.findFacts(space.id, ['TypeScript']); + expect(facts[0]).toMatchObject({ statement: 'The user started TypeScript in 2025.', source: 'boxbrain.sleepMemory' }); + expect(facts[0]?.topics).toContain('sleepMemory'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..15ae9cb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["bun-types"], + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "skipLibCheck": true, + "ignoreDeprecations": "6.0" + }, + "include": ["src", "tests"] +}