diff --git a/.gitignore b/.gitignore index 3af87f3d..efb28896 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,9 @@ resources/node/ resources/copilot-runtime/ resources/copilot-runtime.new/ +# Orchestrator session forensics (winorch / tmux-orchestrator-win) +.orchestrator/ + # Packaged Sharp runtime (generated by scripts/prepare-sharp-runtime.js) resources/sharp-runtime/ diff --git a/AGENTS.md b/AGENTS.md index 9b95158d..015d61e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,13 @@ Chamber is a desktop application where AI agents ("minds") operate as a Chief of - **Handoff**: Agent-to-agent delegation - **Magentic**: Manager-driven task ledger +### Memory Consolidation (Dream Daemon — experimental, opt-in) + +- **Per-mind opt-in**: `workingMemory.consolidation.enabled` in each mind's `.chamber.json`. Default OFF. +- **Toggle surfaces**: Genesis wizard role screen (pre-genesis) and agent profile modal (post-genesis); never silent. +- **Bidirectional**: Disabling rolls the structured `log.md` (and any `log.legacy.md`) back to unstructured turn-by-turn markdown via `rollbackToUnstructured`. Single source of truth after rollback. +- **Failure semantics**: Toggle resolves even if rollback fails; config flip is the contract. + ## Security Boundaries ### Credential Storage diff --git a/CHANGELOG.md b/CHANGELOG.md index bd027b5a..894a8cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Dream daemon ships as an opt-in toggle with bidirectional log migration** — The background memory consolidation daemon (introduced as scaffolding in 0.59.x) is now a first-class user-controlled feature. The genesis wizard exposes a single "Enable dream daemon (experimental)" switch on the role screen; the agent profile modal shows the same switch so existing minds can flip it post-genesis without touching `.chamber.json` by hand. Default is **OFF** — minds opted-in pre-genesis ship with `workingMemory.consolidation.enabled: true` written by `MindScaffold.create()`; minds opted-in post-genesis go through `MindManager.enableDreamDaemon(mindId)` which atomically patches the on-disk config (tmp+fsync+rename via the new `patchChamberMindConfig` helper that preserves passthrough fields and deep-merges only the `workingMemory.consolidation` subtree) and reloads the mind context so the new `MindMemoryService` daemon spins up against fresh providers. **Opt-out runs the migration in reverse**: `MindManager.disableDreamDaemon(mindId)` patches config → reloads (tearing down `DailyLogWriter` so the chat observer detaches and the daemon closes against a quiescent log) → calls the new `rollbackToUnstructured(mindPath)` which reads the structured `log.md`, parses every `chamber-structured-log/v1` frame, renders each as `## {ISO} — turn {turnId} ({model})\n\n**User**: …\n\n**Assistant**: …`, folds in any pre-existing `log.legacy.md` ahead of the rendered turns, atomically rewrites `log.md`, and removes `log.legacy.md` so the rolled-back mind has a single source of truth. Rollback is non-fatal (try/catch + `console.warn`): the user-visible toggle resolves successfully even if the rewrite fails — config is already flipped and the daemon is gone, so the worst case is a structured log that needs manual cleanup. Concurrent toggles for the same mindId are serialized through a `daemonToggling: Map>` in MindManager (same pattern as the existing `loading` map); rapid clicks return the same in-flight promise rather than racing the patch+reload+rollback pipeline. Three data-loss safety properties are now locked in by tests after the chamber-ui-tester E2E surfaced a real Flow 4 bug: (1) the structured-log parser at `StructuredLogFormat.ts::parseBlock` accepts an empty `model:` line (`/^model: (.*)$/` not `(.+)$`), so frames written by `ChatService` when no model was selected at turn time still round-trip cleanly through `parseLog`; (2) `ChatService.notifyTurnCompleted` coerces an empty `model` to the sentinel string `'unknown'` before serializing, so on-disk frames are always semantically complete; (3) `rollbackToUnstructured` adds a new `'no-op-malformed'` outcome — when the file has the sentinel and non-empty content but the parser produces zero turns AND `parsed.malformed > 0`, the rewrite is refused (file preserved byte-identical, warn logged) so unparseable history is never silently overwritten with an empty file. Tool surface for the renderer: `mind:setDreamDaemon` IPC channel (desktop adapter routes to `enable/disableDreamDaemon`, browser shim returns `unavailable`), `dreamDaemonEnabled: boolean` added to `AgentProfile` (populated from `loadChamberMindConfig`), `Switch` component in `AgentProfileModal` mirrors the role-screen ARIA pattern (`role="switch"` + `aria-checked` + sky-500/slate-700 colors). Validated by 11 `rollback.test.ts` scenarios (convert N frames, fold legacy + remove file, no-op variants for missing/empty/no-sentinel logs, idempotency, atomicity under synthetic rename failure, zero-frames sentinel-only + legacy → legacy preserved, zero-frames sentinel-only no-legacy → empty file, **Flow 4 regression: empty-model frame round-trips through rollback without data loss**, **all-malformed frames → preserved byte-identical**), 8 `MindManager` `enableDreamDaemon`/`disableDreamDaemon` tests (patch+reload happy paths, throw-on-missing, MindContext return, per-mindId serialization with promise identity, rollback ordering `['patch','reload','rollback']`, no-rollback-on-enable, disable-resolves-even-when-rollback-throws), and a live chamber-ui-tester Electron E2E driving the real Copilot SDK through all four flows (genesis OFF, genesis ON, post-genesis OFF→ON, post-genesis ON→OFF with rollback). The entire stack is also gated behind a new `dreamDaemon` app feature flag (stable/insiders both `false`, dev `true`) so stable and insiders builds do not construct `MindMemoryService`, accept `mind:setDreamDaemon` IPC mutation, write `workingMemory.consolidation.enabled: true` from genesis, or include `log.md` content in the system prompt — even for minds whose `.chamber.json` was opted-in under a dev build. Refs #288. + ### Fixed - **Load marketplace-installed skills in SDK sessions** — Mind sessions now pass each mind's `.github/skills` parent directory through `skillDirectories` when creating or resuming Copilot SDK sessions, so the SDK `skill` tool can discover marketplace-installed skills like `ttasks` and `automation`. @@ -157,6 +161,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Pin the packaged Copilot runtime to 1.0.45** — Updates the committed root and packaged `chamber-copilot-runtime` `@github/copilot` pins to match the CLI binary version expected by the package smoke check while keeping `@github/copilot-sdk` pinned at `0.3.0`. +## v0.59.8 (2026-05-13) + +### Fixes + +- **Stop the two genesis-time errors that bricked fresh-mind console hygiene** — Two production-blocking defects surfaced during manual testing of `feature/dream-daemon-memory-consolidation`: every fresh mind logged `[MindScaffold] Capability bootstrap failed (non-fatal): Error: Upgrade skill not found in genesis repo`, and `[WorkingMemoryComposer] log.md is unstructured` repeated on every system-prompt rebuild. Root causes: (1) Epic #67 moved the upgrade skill from `ianphil/genesis` to `ianphil/genesis-frontier` in commit `17580f5` but `MindScaffold.GENESIS_SOURCE` was never updated; (2) `MindScaffold.createStructure()` wrote a zero-byte `log.md` placeholder which violated the chamber-structured-log/v1 sentinel contract, so `WorkingMemoryComposer.readLog` warned and skipped on every prompt rebuild until the first `DailyLogWriter` rotation fired. `MindScaffold` now points at `ianphil/genesis-frontier@main`, the upgrade-skill error message names the repo and branch (`Upgrade skill not found in ianphil/genesis-frontier@main`) so operators can immediately see which coordinate was searched, and `createStructure()` pre-seeds `log.md` with `\n\n` (matching the byte-level format `DailyLogWriter.seedFreshLog` emits) before the `WORKING_MEMORY_FILES` placeholder loop runs — the loop's `existsSync` guard then skips the seeded file. The genesis prompt is also rewritten to drop log.md as a write target (the file is reserved for `DailyLogWriter` frames) and the SOUL.md "Continuity" section now reads "Your turn-by-turn history is preserved automatically; you do not write to it." `WorkingMemoryComposer.readLog` downgrades the unstructured-log message from `warn` to `info` so SRE dashboards don't alert on pre-existing minds whose `log.md` rotates lazily on the first turn. Validated end-to-end with a new tmpdir integration test (`tests/integration/mindScaffold.integration.test.ts`, 7 tests) that drives the full `MindScaffold.create()` against `mkdtempSync` with a fake `GitHubRegistryClient` and a fake `CopilotClientFactory`, asserting the sentinel-prefixed `log.md`, `registry.json.source === 'ianphil/genesis-frontier'`, the on-disk upgrade.js, no `log.legacy.md` after a `DailyLogWriter.write()`, and zero `WorkingMemoryComposer` warnings for a fresh mind. The 7th test locks the cross-cutting migration story for users upgrading into this release: an existing pre-fix mind (unstructured `log.md` on disk) → composer fires `info` (not `warn`) on the next system-prompt rebuild → `DailyLogWriter` rotates the legacy content to `log.legacy.md` and seeds a fresh sentinel-prefixed log on the first chat turn → composer is silent on subsequent rebuilds. Live Electron smoke (chamber-ui-tester driving the genesis wizard against an isolated `CHAMBER_E2E_GENESIS_BASE_PATH` tmpdir) confirmed both error patterns are gone and the on-disk contract holds end-to-end. + ## v0.59.7 (2026-05-12) ### Fixes diff --git a/ai-docs/feature-flags.md b/ai-docs/feature-flags.md index f396862c..1b5dd76e 100644 --- a/ai-docs/feature-flags.md +++ b/ai-docs/feature-flags.md @@ -50,12 +50,14 @@ Expected shape: "stable": { "switchboardRelay": false, "byoLlm": false, - "chamberCopilot": false + "chamberCopilot": false, + "dreamDaemon": false }, "insiders": { "switchboardRelay": true, "byoLlm": true, - "chamberCopilot": true + "chamberCopilot": true, + "dreamDaemon": false } } } @@ -108,6 +110,7 @@ normal app runs or release builds. | `switchboardRelay` | remote | remote | Hides the activity-bar relay entry point and route. | | `byoLlm` | remote | remote | Hides BYO model settings and disables desktop BYO runtime/IPC usage. | | `chamberCopilot` | remote | remote | Wires the chamber-copilot ACP provider and `cli_*` tools. | +| `dreamDaemon` | remote `false` | remote `false` | Dev-only. Gates the working-memory consolidation daemon, the per-mind opt-in toggle, and the prompt-path use of consolidated memory. | ## Local development flags @@ -126,6 +129,7 @@ export const DEV_FEATURE_FLAGS = { switchboardRelay: true, byoLlm: true, chamberCopilot: true, + dreamDaemon: true, }; ``` @@ -156,6 +160,45 @@ to mind tool providers. Stable builds also ignore the legacy `chamberCopilotEnabled` key in `~/.chamber/config.json`; users cannot turn this surface on locally. +## Dream Daemon + +`dreamDaemon` gates Chamber's working-memory consolidation surface (the +"dream daemon") and the prompt-time use of the consolidated memory it produces. +The flag rolls out as **dev-only**: both `channels.stable` and +`channels.insiders` are `false` in `docs/flags/v1/flags.json` so external +testers do not see the surface yet; `DEV_FEATURE_FLAGS.dreamDaemon = true` +keeps local development behavior unchanged. + +When disabled: + +- `apps/desktop/src/main.ts` does not call `buildMindMemoryService`, so the + daemon, scheduler, and SQLite-backed memory store are never constructed. + The `__chamberMindMemoryService` E2E global is also not exposed. +- `IPC.MIND.SET_DREAM_DAEMON` rejects with `"Dream Daemon is not available in + this build"` when the renderer asks to enable. Disable requests still pass + through so a stable build can clean up persisted opt-in state from an + insiders run. +- `genesis.create` coerces `enableDreamDaemon: false` server-side before + calling `MindScaffold.create`, regardless of the renderer payload. The + newly written `.chamber.json` always has + `workingMemory.consolidation.enabled: false`. +- `MindManager.enableDreamDaemon` throws before any mind lookup as a + defense-in-depth gate behind IPC. +- `IdentityLoader.resolveComposerConfig` forces `enabled: false` in the + composer config it returns, regardless of the per-mind `.chamber.json`. + Persisted caps (`lastKTurns`, `perTurnMaxBytes`, `memoryMaxBytes`) are kept + faithful so a future flip-on does not lose the user's settings. +- `MindProfileService.getProfile` reports `dreamDaemonEnabled: false` even + when `.chamber.json` says `true`, so the (now-hidden) UI never sees a stale + ON state. +- `RoleScreen`, `GenesisFlow`, and `AgentProfileModal` hide the + dream-daemon Switch / toggle row and coerce `enableDreamDaemon: false` at + every emit boundary in the renderer. + +The asymmetry on the IPC and `MindManager` gates is deliberate: enable is +gated, disable is always allowed. A stable build must be able to clean up +opt-in state for minds that were enabled under an insiders build. + ## Adding a new feature flag Use this checklist when introducing a flag for a feature still under diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 829cbaae..313e0e27 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -86,6 +86,7 @@ import { type Notifier, } from '@chamber/services'; import { Logger } from '@chamber/services'; +import { buildMindMemoryService, type MindMemoryComposition } from './main/services/mindMemory/buildMindMemoryService'; import { createAppTray, loadAppIcon } from './main/tray/Tray'; import { installContextMenu } from './main/contextMenu/ContextMenu'; import { installExternalNavigationGuard } from './main/navigationGuard'; @@ -205,6 +206,12 @@ const notifier: Notifier = { }; let appFeatureFlags: AppFeatureFlags = DEFAULT_APP_FEATURE_FLAGS; +// Module-level accessor over the live `appFeatureFlags` binding. Shared by +// `initializeRuntime` (IdentityLoader, MindManager, MindProfileService) and +// the `app.on('ready')` IPC setup (setupMindIPC, setupGenesisIPC). Reading +// the property at call-time tracks future remote-policy reloads that +// re-assign `appFeatureFlags`. +const dreamDaemonFeatureEnabled = (): boolean => appFeatureFlags.dreamDaemon; let credentialStore: CredentialStore; let sharp: typeof sharpModule; let configService: ConfigService; @@ -233,6 +240,8 @@ let automationBridgeStop: (() => Promise) | null = null; let authService: AuthService; let chamberCopilotService: ChamberCopilotService | null = null; let updaterService: UpdaterService; +let mindMemoryComposition: MindMemoryComposition | undefined; +let mindMemoryService: MindMemoryComposition['service'] | undefined; const taskLedgersByMindPath = new Map(); const createTaskLedger = (mindPath: string): TaskLedger => { @@ -262,7 +271,11 @@ async function initializeRuntime(): Promise { }); configService = new ConfigService(); - const identityLoader = new IdentityLoader(() => configService.load().installedTools ?? []); + const identityLoader = new IdentityLoader( + () => configService.load().installedTools ?? [], + undefined, + dreamDaemonFeatureEnabled, + ); const getGenesisMarketplaceSources = (): GenesisMindTemplateMarketplaceSource[] => configService.load().marketplaceRegistries ?? [DEFAULT_GENESIS_MIND_TEMPLATE_SOURCE]; const saveActiveLogin = (login: string | null) => { @@ -314,12 +327,13 @@ async function initializeRuntime(): Promise { viewDiscovery, () => buildProviderConfig(cachedByoLlmConfig), () => cachedByoLlmConfig?.model, + dreamDaemonFeatureEnabled, managedSkillService, ); mindProfileService = new MindProfileService({ getMindPath: (mindId) => mindManager.getMind(mindId)?.mindPath ?? null, restartMind: (mindId) => mindManager.reloadMind(mindId), - }, identityLoader, new SharpAvatarNormalizer(sharp)); + }, identityLoader, new SharpAvatarNormalizer(sharp), dreamDaemonFeatureEnabled); userProfileService = new UserProfileService(configService); microsoftGraphProfileImporter = new MicrosoftGraphProfileImporter( userProfileService, @@ -413,6 +427,40 @@ async function initializeRuntime(): Promise { mindManager.setProviders(mindToolProviders); wireLifecycleEvents({ mindManager, agentCardRegistry, a2aRelayModeService, taskManager, a2aEventBus }); viewDiscovery.setRefreshHandler(createLensRefreshHandler((mindPath, prompt) => mindManager.sendBackgroundPrompt(mindPath, prompt))); + + // ------------------------------------------------------------------------- + // MindMemory (Dream Daemon) — per-mind background memory consolidation. + // Gated behind the `dreamDaemon` app feature flag. When the flag is off, + // `mindMemoryComposition` stays undefined and the lifecycle hooks below + // are never registered, so `mind:loaded` / `mind:unloaded` are no-ops + // for memory purposes. The composition root owns close() during quit — + // requestQuit() and before-quit both guard on `mindMemoryComposition?`. + // + // Wires after providers so chatService observers + scheduler are ready + // before any mind:loaded event fires. The better-sqlite3 ctor is injected + // from the shared `loadBetterSqlite3()` resolver so dev and packaged + // builds both go through the unified `chamber-sqlite-runtime`. + // ------------------------------------------------------------------------- + if (appFeatureFlags.dreamDaemon) { + mindMemoryComposition = buildMindMemoryService({ + mindManager, + chatService, + Database: loadBetterSqlite3(), + }); + mindMemoryService = mindMemoryComposition.service; + const memoryService = mindMemoryService; + mindManager.on('mind:loaded', (ctx: MindContext) => { + memoryService.activateMind(ctx.mindId, ctx.mindPath).catch((err) => { + log.warn('mindMemory: activateMind failed', { mindId: ctx.mindId, err: String(err) }); + }); + }); + mindManager.on('mind:unloaded', (mindId: string) => { + memoryService.releaseMind(mindId).catch((err) => { + log.warn('mindMemory: releaseMind failed', { mindId, err: String(err) }); + }); + }); + } + updaterService = new UpdaterService({ currentVersion: app.getVersion(), isPackaged: app.isPackaged, @@ -471,7 +519,20 @@ const requestQuit = () => { if (isQuitting) return; isQuitting = true; - mindManager.shutdown() + // INVARIANT: close MindMemoryService BEFORE MindManager.shutdown so each + // mind's dream.db handle and scheduler entry tear down while the underlying + // Mind / SDK client is still alive — avoids cron ticks racing with mind + // teardown and leaves dream.db files cleanly closed on disk. The optional + // chain matters because requestQuit() can run before initializeRuntime() + // has assigned mindMemoryComposition (e.g., updater smoke early-return) + // and because Phase 3 will gate composition behind the dreamDaemon flag. + const closeMindMemory = mindMemoryComposition + ? mindMemoryComposition.close().catch((err) => { + log.warn('mindMemory: shutdown close failed', { err: String(err) }); + }) + : Promise.resolve(); + closeMindMemory + .then(() => mindManager.shutdown()) .then(() => { updaterService.stop(); return Promise.allSettled([a2aRelayModeService.disconnect(), stopMvpServer()]); @@ -757,6 +818,7 @@ app.on('ready', async () => { devServerUrl: MAIN_WINDOW_VITE_DEV_SERVER_URL || undefined, rendererPath: MAIN_WINDOW_VITE_DEV_SERVER_URL ? undefined : path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`), windowIcon, + dreamDaemonEnabled: dreamDaemonFeatureEnabled, }); setupMindProfileIPC(mindProfileService, mindManager, sharp); setupUserProfileIPC(userProfileService, microsoftGraphProfileImporter); @@ -775,6 +837,7 @@ app.on('ready', async () => { return result.templates; }}, genesisTemplateInstaller, + { dreamDaemonEnabled: dreamDaemonFeatureEnabled }, ); setupMarketplaceIPC(marketplaceRegistryService, { onRegistryToolsChanged: reconcileMarketplaceTools }); setupToolsIPC(toolsService); @@ -797,6 +860,18 @@ app.on('ready', async () => { setupChatroomIPC(chatroomService); setupUpdaterIPC(updaterService); + // Test-only hook: expose the MindMemoryService on globalThis so a Playwright + // driver attached via `electronApp.evaluate(...)` can drive the Dream Daemon + // (forceRun, getStatus, dbPath) without us building a renderer-facing bridge + // and the production type contract that comes with it. Gated by CHAMBER_E2E. + // After Phase 3 gates the composition behind the dreamDaemon flag, this + // assignment becomes a no-op (mindMemoryService stays undefined) when the + // flag is off — exactly what we want for the flag-off E2E smoke. + if (process.env.CHAMBER_E2E === '1' && mindMemoryService) { + (globalThis as { __chamberMindMemoryService?: typeof mindMemoryService }).__chamberMindMemoryService = + mindMemoryService; + } + // Fire-and-forget tool reconciliation: install any new marketplace tools. // Errors are logged in ToolsService and surface via tools:list later. reconcileMarketplaceTools(); diff --git a/apps/desktop/src/main/devFeatureFlags.ts b/apps/desktop/src/main/devFeatureFlags.ts index 4f99e942..13c27fef 100644 --- a/apps/desktop/src/main/devFeatureFlags.ts +++ b/apps/desktop/src/main/devFeatureFlags.ts @@ -11,4 +11,5 @@ export const DEV_FEATURE_FLAGS: AppFeatureFlags = { switchboardRelay: true, byoLlm: true, chamberCopilot: true, + dreamDaemon: true, }; diff --git a/apps/desktop/src/main/ipc/genesis.ts b/apps/desktop/src/main/ipc/genesis.ts index 147baee1..7f0d91d5 100644 --- a/apps/desktop/src/main/ipc/genesis.ts +++ b/apps/desktop/src/main/ipc/genesis.ts @@ -29,12 +29,25 @@ const createFromTemplateSchema = z }) .strict(); +export interface GenesisIPCOptions { + // Returns the current value of the `dreamDaemon` app feature flag. When this + // returns false, `IPC.GENESIS.CREATE` server-side coerces the incoming + // `enableDreamDaemon` payload to false *before* calling `scaffold.create`, + // regardless of what the renderer claims. This guarantees that a stale or + // bypassed renderer cannot scaffold a mind with `.chamber.json + // workingMemory.consolidation.enabled: true` in a build where the feature + // is off. Defaults to always-on so tests that omit it keep current behavior. + dreamDaemonEnabled?: () => boolean; +} + export function setupGenesisIPC( mindManager: MindManager, scaffold: MindScaffold, templateCatalog: GenesisMindTemplateCatalogPort, templateInstaller: GenesisMindTemplateInstallerPort, + options: GenesisIPCOptions = {}, ): void { + const dreamDaemonEnabled = options.dreamDaemonEnabled ?? (() => true); ipcMain.handle(IPC.GENESIS.GET_DEFAULT_PATH, async () => { return getDefaultGenesisBasePath(); @@ -61,13 +74,23 @@ export function setupGenesisIPC( ipcMain.handle(IPC.GENESIS.CREATE, async (event, config: GenesisConfig) => { const win = BrowserWindow.fromWebContents(event.sender); + // Server-side enforcement of the dreamDaemon feature flag. When the flag + // is off, coerce `enableDreamDaemon` to false regardless of the renderer + // payload — this is the authoritative gate for what gets written into + // `.chamber.json workingMemory.consolidation.enabled`. RoleScreen's UI + // gate (Phase 6) hides the toggle, but a bypassed or stale renderer + // must not be able to land an opted-in mind via this channel. + const sanitizedConfig: GenesisConfig = dreamDaemonEnabled() + ? config + : { ...config, enableDreamDaemon: false }; + // Issue #44 — detect name collision BEFORE scaffolding so we never // create a directory the user can't activate. The check is // case-insensitive against currently-loaded minds; persisted-but-not- // loaded minds are not considered. - const collision = mindManager.findByName(config.name); + const collision = mindManager.findByName(sanitizedConfig.name); if (collision) { - const message = `An agent named "${config.name}" already exists. Choose a different name.`; + const message = `An agent named "${sanitizedConfig.name}" already exists. Choose a different name.`; if (win) win.webContents.send(IPC.GENESIS.PROGRESS, { step: 'error', detail: message }); return { success: false, error: message }; } @@ -77,7 +100,7 @@ export function setupGenesisIPC( }); try { - const mindPath = await scaffold.create(config); + const mindPath = await scaffold.create(sanitizedConfig); return await activateCreatedMind(mindManager, mindPath); } catch (err) { const message = err instanceof Error ? err.message : String(err); diff --git a/apps/desktop/src/main/ipc/mind.ts b/apps/desktop/src/main/ipc/mind.ts index c2a5580b..d91d5509 100644 --- a/apps/desktop/src/main/ipc/mind.ts +++ b/apps/desktop/src/main/ipc/mind.ts @@ -16,9 +16,17 @@ export interface MindIPCConfig { devServerUrl?: string; rendererPath?: string; windowIcon?: NativeImage; + // Returns the current value of the `dreamDaemon` app feature flag. When this + // returns false, `IPC.MIND.SET_DREAM_DAEMON` rejects with a "feature + // unavailable" Error rather than calling into MindManager. The renderer's + // typed contract is `Promise` (not a result union), so a thrown + // Error surfaces through the caller's `await … catch` path. Defaults to + // always-on so test harnesses that omit it keep the existing contract. + dreamDaemonEnabled?: () => boolean; } export function setupMindIPC(mindManager: MindManager, chatService: ChatService, config: MindIPCConfig): void { + const dreamDaemonEnabled = config.dreamDaemonEnabled ?? (() => true); const windowByMind = new Map(); const listMinds = (): MindContext[] => mindManager.listMinds().map((mind) => ({ @@ -57,6 +65,23 @@ export function setupMindIPC(mindManager: MindManager, chatService: ChatService, return chatService.setMindModel(mindId, model); }); + ipcMain.handle(IPC.MIND.SET_DREAM_DAEMON, async (_event, mindId: string, enabled: boolean) => { + // Authoritative gate at the IPC boundary. When the app-level `dreamDaemon` + // feature flag is off, the renderer must not be able to *enable* the + // per-mind opt-in via this channel — that would activate consolidation in + // a build that disables the feature. The *disable* direction stays + // available so a future stable build that re-introduces a "clean up legacy + // state" path can route through this channel without a special case. + // The rejection surfaces via the renderer's `await … catch` path. See + // AgentProfileModal for the consuming UI. + if (enabled && !dreamDaemonEnabled()) { + throw new Error('Dream Daemon is not available in this build'); + } + return enabled + ? mindManager.enableDreamDaemon(mindId) + : mindManager.disableDreamDaemon(mindId); + }); + ipcMain.handle(IPC.MIND.SELECT_DIRECTORY, async (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (!win) return null; diff --git a/apps/desktop/src/main/services/featureFlags/FeatureFlagService.test.ts b/apps/desktop/src/main/services/featureFlags/FeatureFlagService.test.ts index 968b4af7..70a77c54 100644 --- a/apps/desktop/src/main/services/featureFlags/FeatureFlagService.test.ts +++ b/apps/desktop/src/main/services/featureFlags/FeatureFlagService.test.ts @@ -9,12 +9,14 @@ const DEV_FLAGS: AppFeatureFlags = { switchboardRelay: true, byoLlm: false, chamberCopilot: true, + dreamDaemon: true, }; const REMOTE_FLAGS: AppFeatureFlags = { switchboardRelay: true, byoLlm: true, chamberCopilot: false, + dreamDaemon: false, }; describe('FeatureFlagService', () => { @@ -114,6 +116,7 @@ describe('FeatureFlagService', () => { switchboardRelay: true, byoLlm: true, chamberCopilot: true, + dreamDaemon: true, }); expect(fetched).toBe(false); }); diff --git a/apps/desktop/src/main/services/mindMemory/buildMindMemoryService.ts b/apps/desktop/src/main/services/mindMemory/buildMindMemoryService.ts new file mode 100644 index 00000000..d2720cc2 --- /dev/null +++ b/apps/desktop/src/main/services/mindMemory/buildMindMemoryService.ts @@ -0,0 +1,188 @@ +/** + * Phase 13 desktop wiring for the per-mind background memory engine + * ("Dream Daemon"). This module is a *thin adapter*: it knows how to + * + * - open per-mind `dream.db` files at the canonical path using a + * better-sqlite3 constructor injected by the composition root (which + * resolves the runtime via the shared `chamber-sqlite-runtime` path), + * - mint *one-shot* Copilot sessions with tools disabled and a refusing + * permission handler (defense-in-depth — tools=[] should already mean + * no permission requests reach the handler), + * - construct a `DreamDaemon` from a `WorkingMemoryConsolidationConfig` + * by supplying defaults for the wider `DreamDaemonConfig` surface. + * + * The composition root in `apps/desktop/src/main.ts` calls + * `buildMindMemoryService` once and wires the resulting service into + * `MindManager`'s `mind:loaded` / `mind:unloaded` events. + */ +import path from 'node:path'; +import fs from 'node:fs'; +import { + buildOneShotSession, + createMindMemoryService, + createInternalScheduler, + createMindMemoryVault, + createMindArchiveStore, + createDreamDaemon, + createCopilotLLMClient, + defaultConfigReader, + Logger, + migrate as migrateDreamDb, + type MindMemoryService, + type MindManager, + type DaemonFactoryOptions, + type CreateOneShotSessionArgs, + type OneShotSession, +} from '@chamber/services'; +import type { TurnCompletionObserver } from '@chamber/shared'; + +type BetterSqlite3Module = typeof import('better-sqlite3'); +type BetterSqlite3Database = import('better-sqlite3').Database; + +interface BuildMindMemoryServiceOptions { + readonly mindManager: MindManager; + readonly chatService: { + addObserver(o: TurnCompletionObserver): void; + removeObserver(o: TurnCompletionObserver): void; + }; + /** + * better-sqlite3 module already resolved by the composition root. Master + * resolves this once via `loadBetterSqlite3()` in `apps/desktop/src/main.ts` + * and feeds the same module into both the task ledger (`setSqliteDatabase`) + * and the dream daemon, so packaged builds use the unified + * `chamber-sqlite-runtime` rather than an ASAR-unpacked node_modules copy. + */ + readonly Database: BetterSqlite3Module; + readonly logger?: Logger; +} + +export interface MindMemoryComposition { + readonly service: MindMemoryService; + readonly scheduler: ReturnType; + /** + * Shut everything down cleanly. Releases the service first (which releases + * each mind, closing its observer + dream.db handle), then the scheduler. + * Idempotent. + */ + close(): Promise; +} + +/** + * Build the createOneShotSession adapter for CopilotLLMClient. Each + * synthesize call mints a fresh CopilotSession scoped to the mind's + * working directory, with NO tools, NO config discovery, and a refusing + * permission handler. The session is closed in the LLMClient's `finally`. + * + * The SDK-touching plumbing lives in `@chamber/services` `buildOneShotSession` + * so the same contract is exercised by the live-SDK integration test. + */ +function makeCreateOneShotSession( + mindManager: MindManager, + logger: Logger, +): (args: CreateOneShotSessionArgs) => Promise { + return async ({ mindId, mindPath, signal }) => { + const ctx = mindManager.getMind(mindId); + if (!ctx) { + throw new Error(`MindMemory: cannot create session — mind ${mindId} is not loaded`); + } + return buildOneShotSession({ + client: ctx.client, + workingDirectory: mindPath, + signal, + onDisconnectError: (err) => + logger.warn('mindMemory: session disconnect failed', { err: String(err) }), + }); + }; +} + +// Defaults for the wider DreamDaemonConfig fields that aren't covered by +// WorkingMemoryConsolidationConfig. Tuned to match Phase 9/10 unit-test +// defaults so production behavior matches the test harness. +const MS_PER_DAY = 24 * 60 * 60 * 1000; +const DEFAULT_LLM_TIMEOUT_MS = 90_000; +const DEFAULT_LOCK_TTL_MS = 5 * 60 * 1000; +const DEFAULT_MIN_TURNS_BETWEEN_RUNS = 1; +const DEFAULT_MIN_DAILY_INTERVAL_MS = 0; +const DEFAULT_WEEKLY_ROLLUP_AFTER_DAILIES = 7; +const DEFAULT_MONTHLY_ROLLUP_AFTER_WEEKLIES = 4; +const DEFAULT_WEEKLY_MIN_INTERVAL_MS = 7 * MS_PER_DAY; +const DEFAULT_MONTHLY_MIN_INTERVAL_MS = 30 * MS_PER_DAY; + +export function buildMindMemoryService(opts: BuildMindMemoryServiceOptions): MindMemoryComposition { + const logger = opts.logger ?? Logger.create('mindMemory'); + const Database = opts.Database; + const scheduler = createInternalScheduler({ logger }); + const createOneShotSession = makeCreateOneShotSession(opts.mindManager, logger); + + const service = createMindMemoryService({ + scheduler, + chatService: opts.chatService, + configReader: defaultConfigReader, + dbFactory: (dbPath: string): BetterSqlite3Database => { + // INVARIANT: must apply the dream.db schema before returning. Without + // `migrate(db)`, the daemon's first call to `readState` / `acquireLock` + // would throw `no such table: dream_state`. Mirrors `openDreamDb` in + // dream-schema.ts, but uses the dynamically-loaded better-sqlite3 + // module so packaged builds resolve the unpacked native binding. + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + const db = new Database(dbPath); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + migrateDreamDb(db); + return db; + }, + vaultFactory: createMindMemoryVault, + archiveFactory: createMindArchiveStore, + daemonFactory: ({ mindId, mindPath, vault, archive, db, config }: DaemonFactoryOptions) => { + const llmClient = createCopilotLLMClient({ + mindId, + mindPath, + deps: { createOneShotSession }, + }); + return createDreamDaemon({ + mindId, + mindPath, + llmClient, + vault, + archiveStore: archive, + db, + config: { + memoryMaxBytes: config.memoryMaxBytes, + llmTimeoutMs: DEFAULT_LLM_TIMEOUT_MS, + lockTtlMs: DEFAULT_LOCK_TTL_MS, + minTurnsBetweenRuns: DEFAULT_MIN_TURNS_BETWEEN_RUNS, + minDailyIntervalMs: DEFAULT_MIN_DAILY_INTERVAL_MS, + weeklyRollupAfterDailies: DEFAULT_WEEKLY_ROLLUP_AFTER_DAILIES, + monthlyRollupAfterWeeklies: DEFAULT_MONTHLY_ROLLUP_AFTER_WEEKLIES, + weeklyMinIntervalMs: DEFAULT_WEEKLY_MIN_INTERVAL_MS, + monthlyMinIntervalMs: DEFAULT_MONTHLY_MIN_INTERVAL_MS, + }, + logger, + }); + }, + logger, + }); + + let closed = false; + return { + service, + scheduler, + async close(): Promise { + if (closed) return; + closed = true; + // INVARIANT: release the service BEFORE closing the scheduler so the + // scheduler can still observe `unregister` calls coming from each + // mind release. Otherwise close() on a closed scheduler would throw. + try { + await service.close(); + } catch (err) { + logger.warn('mindMemory: service close failed', { err: String(err) }); + } + try { + scheduler.close(); + } catch (err) { + logger.warn('mindMemory: scheduler close failed', { err: String(err) }); + } + }, + }; +} diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 3a8c2aab..9f39c32a 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -29,6 +29,7 @@ const electronAPI: ElectronAPI = { list: () => ipcRenderer.invoke(IPC.MIND.LIST), setActive: (mindId) => ipcRenderer.invoke(IPC.MIND.SET_ACTIVE, mindId), setModel: (mindId, model) => ipcRenderer.invoke(IPC.MIND.SET_MODEL, mindId, model), + setDreamDaemon: (mindId, enabled) => ipcRenderer.invoke(IPC.MIND.SET_DREAM_DAEMON, mindId, enabled), selectDirectory: () => ipcRenderer.invoke(IPC.MIND.SELECT_DIRECTORY), openWindow: (mindId) => ipcRenderer.invoke(IPC.MIND.OPEN_WINDOW, mindId), onMindChanged: (callback) => createIpcListener(ipcRenderer, IPC.MIND.CHANGED, callback), diff --git a/apps/web/src/browserApi.ts b/apps/web/src/browserApi.ts index 4edd0f21..ec391f14 100644 --- a/apps/web/src/browserApi.ts +++ b/apps/web/src/browserApi.ts @@ -162,6 +162,7 @@ export function installBrowserApi(): void { list: () => client.listMinds() as Promise, setActive: async () => unavailable('active mind changes'), setModel: async () => null, + setDreamDaemon: async () => unavailable('dream daemon toggling'), selectDirectory: async () => window.prompt('Enter a local agent folder path on this computer:')?.trim() || null, openWindow: async (mindId) => { window.open(`/?mindId=${encodeURIComponent(mindId)}`, '_blank', 'noopener,noreferrer'); diff --git a/apps/web/src/renderer/components/genesis/GenesisFlow.test.tsx b/apps/web/src/renderer/components/genesis/GenesisFlow.test.tsx index 5b55f973..376ad88f 100644 --- a/apps/web/src/renderer/components/genesis/GenesisFlow.test.tsx +++ b/apps/web/src/renderer/components/genesis/GenesisFlow.test.tsx @@ -6,9 +6,20 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; import { GenesisFlow } from './GenesisFlow'; import { AppStateProvider, useAppState } from '../../lib/store'; +import { DEFAULT_APP_FEATURE_FLAGS } from '@chamber/shared/feature-flags'; +import type { AppFeatureFlags } from '@chamber/shared/feature-flags'; import { installElectronAPI, mockElectronAPI } from '../../../test/helpers'; import type { GenesisMindTemplate, MindContext } from '@chamber/shared/types'; +// Helper for tests that exercise the dream-daemon path: provide an +// AppStateProvider whose feature-flag slice has `dreamDaemon: true` so +// the renderer-side coercion in GenesisFlow.handleRole does not zero out +// the opt-in. Tests without an explicit flags arg get the default-off +// shape so coercion behavior under the flag-off case stays visible. +function flagsState(flags: Partial = {}) { + return { featureFlags: { ...DEFAULT_APP_FEATURE_FLAGS, ...flags } }; +} + vi.mock('./VoidScreen', () => ({ VoidScreen: ({ onBegin, @@ -47,8 +58,17 @@ vi.mock('./VoiceScreen', () => ({ })); vi.mock('./RoleScreen', () => ({ - RoleScreen: ({ onSelect }: { onSelect: (role: string) => void }) => ( - + RoleScreen: ({ + onSelect, + }: { + onSelect: (role: string, enableDreamDaemon: boolean) => void; + }) => ( + <> + + + ), })); @@ -213,11 +233,68 @@ describe('GenesisFlow', () => { voice: 'Test Agent', voiceDescription: 'Test voice', basePath: 'C:\\Users\\test\\agents', + enableDreamDaemon: false, }); }); expect(api.genesis.createFromTemplate).not.toHaveBeenCalled(); }); + it('forwards enableDreamDaemon=true into the genesis.create IPC payload', async () => { + // RoleScreen owns the Switch; GenesisFlow.handleRole must thread the + // captured opt-in into the IPC call so MindScaffold sees it. Without + // this the user toggles the Switch and nothing reaches the main process. + // The dreamDaemon feature flag must be ON for the coercion in + // handleRole to allow the `true` through. + render( + + + , + ); + + fireEvent.click(screen.getByText('Begin')); + fireEvent.click(screen.getByText('Choose voice')); + fireEvent.click(await screen.findByText('Choose role with daemon')); + + await waitFor(() => { + expect(api.genesis.create).toHaveBeenCalledWith({ + name: 'Test Agent', + role: 'Engineering Partner', + voice: 'Test Agent', + voiceDescription: 'Test voice', + basePath: 'C:\\Users\\test\\agents', + enableDreamDaemon: true, + }); + }); + }); + + it('coerces enableDreamDaemon to false when the dreamDaemon feature flag is off', async () => { + // Renderer-side defense-in-depth: even if a child screen (test mock, + // future deep-link, stale local state) sends `true`, the GenesisFlow + // boundary must zero it out when the flag is off. The IPC layer + // (genesis.ts handler) also enforces this server-side, but the + // renderer should not depend on that. + render( + + + , + ); + + fireEvent.click(screen.getByText('Begin')); + fireEvent.click(screen.getByText('Choose voice')); + fireEvent.click(await screen.findByText('Choose role with daemon')); + + await waitFor(() => { + expect(api.genesis.create).toHaveBeenCalledWith({ + name: 'Test Agent', + role: 'Engineering Partner', + voice: 'Test Agent', + voiceDescription: 'Test voice', + basePath: 'C:\\Users\\test\\agents', + enableDreamDaemon: false, + }); + }); + }); + it('adds a marketplace from the landing page and refreshes templates', async () => { render( diff --git a/apps/web/src/renderer/components/genesis/GenesisFlow.tsx b/apps/web/src/renderer/components/genesis/GenesisFlow.tsx index e68fb123..a98de5b3 100644 --- a/apps/web/src/renderer/components/genesis/GenesisFlow.tsx +++ b/apps/web/src/renderer/components/genesis/GenesisFlow.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useRef } from 'react'; -import { useAppDispatch } from '../../lib/store'; +import { useAppDispatch, useAppState } from '../../lib/store'; import { Logger } from '../../lib/logger'; import { VoidScreen } from './VoidScreen'; import { RoleScreen } from './RoleScreen'; @@ -28,6 +28,8 @@ export function GenesisFlow({ onComplete }: Props) { const [creationError, setCreationError] = useState(null); const creationPromiseRef = useRef | null>(null); const dispatch = useAppDispatch(); + const { featureFlags } = useAppState(); + const dreamDaemonFlag = featureFlags.dreamDaemon; const loadTemplates = useCallback(async () => { setTemplateError(null); @@ -52,11 +54,18 @@ export function GenesisFlow({ onComplete }: Props) { return { success: true, message: `Added ${result.registry.label}. It will appear in New Agent templates.` }; }, [loadTemplates]); - const handleRole= useCallback(async (r: string) => { + const handleRole= useCallback(async (r: string, enableDreamDaemon: boolean) => { setRole(r); setStage('boot'); setCreationError(null); + // Defense-in-depth: RoleScreen already coerces its own state when the + // flag is off, but coerce again here so any future caller that + // bypasses the screen (deep-link, test harness) can't smuggle a + // `true` payload past the renderer boundary. The IPC layer also + // enforces this server-side. + const effectiveDreamDaemon = dreamDaemonFlag && enableDreamDaemon; + const defaultPath = await window.electronAPI.genesis.getDefaultPath(); const creationPromise = window.electronAPI.genesis.create({ name: name, @@ -64,6 +73,7 @@ export function GenesisFlow({ onComplete }: Props) { voice: name, voiceDescription: voiceDesc, basePath: defaultPath, + enableDreamDaemon: effectiveDreamDaemon, }).catch((error: unknown) => ({ success: false, error: error instanceof Error ? error.message : String(error), @@ -75,7 +85,7 @@ export function GenesisFlow({ onComplete }: Props) { setCreationError(result.error ?? 'Genesis failed.'); log.error('Failed:', result.error); } - }, [name, voiceDesc]); + }, [name, voiceDesc, dreamDaemonFlag]); const handleVoiceWithDesc = useCallback((voiceName: string, desc: string) => { setName(voiceName); diff --git a/apps/web/src/renderer/components/genesis/RoleScreen.test.tsx b/apps/web/src/renderer/components/genesis/RoleScreen.test.tsx new file mode 100644 index 00000000..9c95533e --- /dev/null +++ b/apps/web/src/renderer/components/genesis/RoleScreen.test.tsx @@ -0,0 +1,142 @@ +/** + * @vitest-environment jsdom + * + * v0.60.0 Phase 2: the dream-daemon Switch lives at the bottom of RoleScreen + * because Role is the LAST input the user makes before `genesis.create` fires. + * Capturing the Switch state here means GenesisFlow can forward it into the + * IPC payload without an extra screen + extra reload of state. + * + * Contract: + * - Switch defaults to OFF (strict opt-in). + * - `onSelect` signature is `(role: string, enableDreamDaemon: boolean)`. + * - The Switch is purely a captured field — toggling it does NOT submit. + * Clicking a role card (or pressing "That's my purpose" in the custom + * branch) is what fires `onSelect` with the captured Switch state. + */ +import React from 'react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { AppStateProvider } from '../../lib/store'; +import { DEFAULT_APP_FEATURE_FLAGS } from '@chamber/shared/feature-flags'; +import type { AppFeatureFlags } from '@chamber/shared/feature-flags'; + +// Mock TypeWriter to fire onComplete immediately — the real implementation +// uses a 35ms-per-char setInterval that takes ~1.5s to finish, which would +// blow past Vitest's default `findBy*` 1000ms wait. The cards/Switch render +// after onComplete + a 500ms delay; bypassing the typewriter is the standard +// pattern in this codebase (see GenesisFlow.test.tsx — same approach). +vi.mock('./TypeWriter', () => ({ + TypeWriter: ({ text, onComplete }: { text: string; onComplete?: () => void }) => { + React.useEffect(() => { + onComplete?.(); + }, [onComplete]); + return {text}; + }, +})); + +import { RoleScreen } from './RoleScreen'; + +afterEach(() => { + cleanup(); +}); + +// Helper: wrap RoleScreen with an AppStateProvider whose feature-flag slice +// has `dreamDaemon` set explicitly. Defaults to ON so the Switch is rendered +// (which is what every test in this file was originally written against). +// Flag-off cases pass `{ dreamDaemon: false }` to confirm the Switch is +// hidden and `enableDreamDaemon` is coerced to false in onSelect payloads. +function renderRoleScreen( + props: { name: string; onSelect: (role: string, enableDreamDaemon: boolean) => void }, + flags: Partial = { dreamDaemon: true }, +) { + return render( + + + , + ); +} + +describe('RoleScreen — dream-daemon opt-in switch', () => { + it('renders the dream-daemon Switch in the OFF position by default', async () => { + renderRoleScreen({ name: 'Test', onSelect: vi.fn() }); + const toggle = await screen.findByRole('switch', { name: /dream daemon/i }); + expect(toggle).not.toBeNull(); + expect(toggle.getAttribute('aria-checked')).toBe('false'); + }); + + it('toggling the Switch updates aria-checked to true', async () => { + renderRoleScreen({ name: 'Test', onSelect: vi.fn() }); + const toggle = await screen.findByRole('switch', { name: /dream daemon/i }); + fireEvent.click(toggle); + await waitFor(() => { + expect(toggle.getAttribute('aria-checked')).toBe('true'); + }); + }); + + it('opt-out (default): clicking a role card calls onSelect with enableDreamDaemon=false', async () => { + const onSelect = vi.fn(); + renderRoleScreen({ name: 'Test', onSelect }); + const card = await screen.findByRole('button', { name: /Chief of Staff/i }); + fireEvent.click(card); + await waitFor(() => { + expect(onSelect).toHaveBeenCalledTimes(1); + }); + expect(onSelect).toHaveBeenCalledWith('Chief of Staff', false); + }); + + it('opt-in: toggling the Switch ON, then clicking a card, calls onSelect with enableDreamDaemon=true', async () => { + const onSelect = vi.fn(); + renderRoleScreen({ name: 'Test', onSelect }); + const toggle = await screen.findByRole('switch', { name: /dream daemon/i }); + fireEvent.click(toggle); + const card = await screen.findByRole('button', { name: /Engineering Partner/i }); + fireEvent.click(card); + await waitFor(() => { + expect(onSelect).toHaveBeenCalledTimes(1); + }); + expect(onSelect).toHaveBeenCalledWith('Engineering Partner', true); + }); + + it('custom-role branch: opt-in propagates through "That\'s my purpose"', async () => { + const onSelect = vi.fn(); + renderRoleScreen({ name: 'Test', onSelect }); + const toggle = await screen.findByRole('switch', { name: /dream daemon/i }); + fireEvent.click(toggle); + + const customCard = await screen.findByRole('button', { name: /Something else/i }); + fireEvent.click(customCard); + + const input = await screen.findByPlaceholderText(/Creative Director/i); + fireEvent.change(input, { target: { value: 'Debate Coach' } }); + const submit = await screen.findByRole('button', { name: /That's my purpose/i }); + fireEvent.click(submit); + + expect(onSelect).toHaveBeenCalledTimes(1); + expect(onSelect).toHaveBeenCalledWith('Debate Coach', true); + }); +}); + +describe('RoleScreen — feature-flag gate (dreamDaemon: false)', () => { + // Mirrors the gating in IdentityLoader / MindProfileService / MindManager: + // when the app-level `dreamDaemon` flag is off, the Switch is hidden and + // the renderer coerces the value forwarded into `onSelect` to false so + // a stale local state can never smuggle an opt-in past the boundary. + it('hides the dream-daemon Switch when the feature flag is off', async () => { + renderRoleScreen({ name: 'Test', onSelect: vi.fn() }, { dreamDaemon: false }); + // The role cards still render via the typewriter completion path. + await screen.findByRole('button', { name: /Chief of Staff/i }); + // The Switch must NOT be in the DOM at all. + expect(screen.queryByRole('switch', { name: /dream daemon/i })).toBeNull(); + }); + + it('forces onSelect enableDreamDaemon=false when the feature flag is off', async () => { + const onSelect = vi.fn(); + renderRoleScreen({ name: 'Test', onSelect }, { dreamDaemon: false }); + const card = await screen.findByRole('button', { name: /Research Partner/i }); + fireEvent.click(card); + await waitFor(() => { + expect(onSelect).toHaveBeenCalledTimes(1); + }); + expect(onSelect).toHaveBeenCalledWith('Research Partner', false); + }); +}); diff --git a/apps/web/src/renderer/components/genesis/RoleScreen.tsx b/apps/web/src/renderer/components/genesis/RoleScreen.tsx index 4c310551..4f9e4f6f 100644 --- a/apps/web/src/renderer/components/genesis/RoleScreen.tsx +++ b/apps/web/src/renderer/components/genesis/RoleScreen.tsx @@ -1,10 +1,19 @@ import React, { useState, useRef, useEffect } from 'react'; import { TypeWriter } from './TypeWriter'; import { cn } from '../../lib/utils'; +import { useAppState } from '../../lib/store'; interface Props { name: string; - onSelect: (role: string) => void; + /** + * v0.60.0 Phase 2: signature changed from `(role: string)` to + * `(role: string, enableDreamDaemon: boolean)`. The boolean is captured + * from the dream-daemon Switch at the bottom of this screen — Role is the + * last input the user makes before `genesis.create` fires, so colocating + * the Switch here means GenesisFlow can forward the choice into the IPC + * payload without an extra screen or extra state hop. + */ + onSelect: (role: string, enableDreamDaemon: boolean) => void; } const ROLES = [ @@ -15,10 +24,16 @@ const ROLES = [ ]; export function RoleScreen({ name, onSelect }: Props) { + const { featureFlags } = useAppState(); + const dreamDaemonFlag = featureFlags.dreamDaemon; const [showCards, setShowCards] = useState(false); const [selected, setSelected] = useState(null); const [customRole, setCustomRole] = useState(''); const [showCustomInput, setShowCustomInput] = useState(false); + // Strict opt-in. Defaults to OFF so a user who never touches the Switch + // ends up with a quiet mind. The dream daemon never starts, log.md stays + // empty, and `.chamber.json` is never written — see MindScaffold.createStructure. + const [enableDreamDaemon, setEnableDreamDaemon] = useState(false); const inputRef = useRef(null); useEffect(() => { @@ -27,6 +42,11 @@ export function RoleScreen({ name, onSelect }: Props) { return () => clearTimeout(t); }, [showCustomInput]); + // Defense-in-depth: even if a stale component state held `true` from + // before the flag flipped off, never forward an opt-in when the + // feature flag is disabled. The IPC layer also coerces this server-side. + const effectiveDreamDaemon = dreamDaemonFlag && enableDreamDaemon; + const handleSelect = (roleId: string) => { if (roleId === 'custom') { setSelected('custom'); @@ -36,14 +56,14 @@ export function RoleScreen({ name, onSelect }: Props) { setSelected(roleId); setTimeout(() => { const role = ROLES.find(r => r.id === roleId); - onSelect(role?.label ?? roleId); + onSelect(role?.label ?? roleId, effectiveDreamDaemon); }, 300); }; const handleCustomSubmit = () => { const role = customRole.trim(); if (!role) return; - onSelect(role); + onSelect(role, effectiveDreamDaemon); }; return ( @@ -102,6 +122,47 @@ export function RoleScreen({ name, onSelect }: Props) { )} )} + + {/* + Dream-daemon opt-in. Sits at the bottom because it's a + secondary, optional choice — the role cards are the primary + decision. ARIA `switch` role + `aria-checked` is the WCAG- + recommended shape for an on/off toggle (better than a raw + checkbox here because the binary state is the whole UI). + Gated behind the app-level `dreamDaemon` feature flag: when + off, the Switch is hidden entirely so genesis creates a + quiet mind regardless of `.chamber.json` state. + */} + {dreamDaemonFlag && ( +
+
+
Enable dream daemon
+
+ Background memory consolidation. Off by default — you can change this later. +
+
+ +
+ )} )} diff --git a/apps/web/src/renderer/components/layout/ActivityBar.test.tsx b/apps/web/src/renderer/components/layout/ActivityBar.test.tsx index 1dc58064..48ad1bad 100644 --- a/apps/web/src/renderer/components/layout/ActivityBar.test.tsx +++ b/apps/web/src/renderer/components/layout/ActivityBar.test.tsx @@ -37,12 +37,12 @@ describe('ActivityBar', () => { }); it('hides the A2A Relay button when the Switchboard Relay flag is disabled', () => { - renderActivityBar({ featureFlags: { switchboardRelay: false, byoLlm: false, chamberCopilot: false } }); + renderActivityBar({ featureFlags: { switchboardRelay: false, byoLlm: false, chamberCopilot: false, dreamDaemon: false } }); expect(screen.queryByLabelText('A2A Relay')).toBeNull(); }); it('renders the A2A Relay button above settings when the Switchboard Relay flag is enabled', () => { - renderActivityBar({ featureFlags: { switchboardRelay: true, byoLlm: false, chamberCopilot: false } }); + renderActivityBar({ featureFlags: { switchboardRelay: true, byoLlm: false, chamberCopilot: false, dreamDaemon: false } }); const relayButton = screen.getByLabelText('A2A Relay'); const settingsButton = screen.getByLabelText('Settings'); const footer = relayButton.closest('[data-testid="activity-bar-footer"]'); diff --git a/apps/web/src/renderer/components/profile/AgentProfileModal.test.tsx b/apps/web/src/renderer/components/profile/AgentProfileModal.test.tsx index e909af5d..cd76933a 100644 --- a/apps/web/src/renderer/components/profile/AgentProfileModal.test.tsx +++ b/apps/web/src/renderer/components/profile/AgentProfileModal.test.tsx @@ -6,6 +6,8 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AgentProfileModal } from './AgentProfileModal'; import { AppStateProvider } from '../../lib/store'; +import { DEFAULT_APP_FEATURE_FLAGS } from '@chamber/shared/feature-flags'; +import type { AppFeatureFlags } from '@chamber/shared/feature-flags'; import { installElectronAPI, mockElectronAPI } from '../../../test/helpers'; import type { AgentProfile, MindContext } from '@chamber/shared/types'; @@ -84,11 +86,88 @@ describe('AgentProfileModal', () => { crop: expect.objectContaining({ width: 600, height: 600 }), })); }); + + describe('dream-daemon switch', () => { + it('renders the switch in the OFF position when dreamDaemonEnabled is false', async () => { + (api.mindProfile.get as ReturnType).mockResolvedValue(makeProfile({ dreamDaemonEnabled: false })); + renderProfileModal(); + + const toggle = await screen.findByRole('switch', { name: /dream daemon/i }); + expect(toggle.getAttribute('aria-checked')).toBe('false'); + }); + + it('renders the switch in the ON position when dreamDaemonEnabled is true', async () => { + (api.mindProfile.get as ReturnType).mockResolvedValue(makeProfile({ dreamDaemonEnabled: true })); + renderProfileModal(); + + const toggle = await screen.findByRole('switch', { name: /dream daemon/i }); + expect(toggle.getAttribute('aria-checked')).toBe('true'); + }); + + it('flipping the switch from OFF to ON calls mind.setDreamDaemon(mindId, true) then refreshes the profile', async () => { + const offProfile = makeProfile({ dreamDaemonEnabled: false }); + const onProfile = makeProfile({ dreamDaemonEnabled: true }); + const getMock = api.mindProfile.get as ReturnType; + getMock.mockResolvedValueOnce(offProfile); + getMock.mockResolvedValueOnce(onProfile); + (api.mind.setDreamDaemon as ReturnType).mockResolvedValue({ ...mind }); + + renderProfileModal(); + const toggle = await screen.findByRole('switch', { name: /dream daemon/i }); + fireEvent.click(toggle); + + await waitFor(() => expect(api.mind.setDreamDaemon).toHaveBeenCalledWith('mind-1', true)); + await waitFor(() => expect(getMock).toHaveBeenCalledTimes(2)); + }); + + it('flipping the switch from ON to OFF calls mind.setDreamDaemon(mindId, false)', async () => { + const onProfile = makeProfile({ dreamDaemonEnabled: true }); + const offProfile = makeProfile({ dreamDaemonEnabled: false }); + const getMock = api.mindProfile.get as ReturnType; + getMock.mockResolvedValueOnce(onProfile); + getMock.mockResolvedValueOnce(offProfile); + (api.mind.setDreamDaemon as ReturnType).mockResolvedValue({ ...mind }); + + renderProfileModal(); + const toggle = await screen.findByRole('switch', { name: /dream daemon/i }); + expect(toggle.getAttribute('aria-checked')).toBe('true'); + fireEvent.click(toggle); + + await waitFor(() => expect(api.mind.setDreamDaemon).toHaveBeenCalledWith('mind-1', false)); + }); + + describe('feature-flag gate (dreamDaemon: false)', () => { + // When the app-level flag is off the toggle row is hidden entirely. + // MindProfileService also forces `dreamDaemonEnabled: false` server-side + // in the same case, so the renderer would never even see ON — but the + // hide-on-flag-off check protects against any stale value. + it('hides the dream-daemon switch row when the feature flag is off', async () => { + (api.mindProfile.get as ReturnType).mockResolvedValue(makeProfile({ dreamDaemonEnabled: false })); + renderProfileModal({ dreamDaemon: false }); + + // The profile load + first content render must complete so the + // `Display name` label is in the DOM before we assert absence. + await screen.findByText('Display name'); + expect(screen.queryByRole('switch', { name: /dream daemon/i })).toBeNull(); + }); + + it('hides the row even if the server payload still reports dreamDaemonEnabled=true', async () => { + // Defense-in-depth: renderer must not trust a stale ON state. The + // server should force false when the flag is off, but the renderer + // gates independently. + (api.mindProfile.get as ReturnType).mockResolvedValue(makeProfile({ dreamDaemonEnabled: true })); + renderProfileModal({ dreamDaemon: false }); + + await screen.findByText('Display name'); + expect(screen.queryByRole('switch', { name: /dream daemon/i })).toBeNull(); + }); + }); + }); }); -function renderProfileModal() { +function renderProfileModal(flags: Partial = { dreamDaemon: true }) { render( - + , ); @@ -111,6 +190,7 @@ function makeProfile(overrides?: Partial): AgentProfile { }, agentFiles: [makeAgentFile('moneypenny.agent.md')], needsRestart: false, + dreamDaemonEnabled: false, ...overrides, }; } diff --git a/apps/web/src/renderer/components/profile/AgentProfileModal.tsx b/apps/web/src/renderer/components/profile/AgentProfileModal.tsx index 3182d6d4..812fe1df 100644 --- a/apps/web/src/renderer/components/profile/AgentProfileModal.tsx +++ b/apps/web/src/renderer/components/profile/AgentProfileModal.tsx @@ -9,7 +9,7 @@ import { DialogTitle, } from '../ui/dialog'; import { cn } from '../../lib/utils'; -import { useAppDispatch } from '../../lib/store'; +import { useAppDispatch, useAppState } from '../../lib/store'; import type { AgentProfile, AgentProfileAvatarCrop, @@ -31,12 +31,15 @@ const iconButtonClass = 'inline-flex items-center justify-center rounded-md bord export function AgentProfileModal({ mind, open, onOpenChange, onProfileChanged }: AgentProfileModalProps) { const dispatch = useAppDispatch(); + const { featureFlags } = useAppState(); + const dreamDaemonFlag = featureFlags.dreamDaemon; const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [editingFile, setEditingFile] = useState(null); const [avatarSource, setAvatarSource] = useState(null); const [restarting, setRestarting] = useState(false); + const [togglingDreamDaemon, setTogglingDreamDaemon] = useState(false); useEffect(() => { if (!open || !mind) { @@ -105,6 +108,23 @@ export function AgentProfileModal({ mind, open, onOpenChange, onProfileChanged } } }; + const handleToggleDreamDaemon = async () => { + if (!profile || togglingDreamDaemon) return; + const next = !profile.dreamDaemonEnabled; + setTogglingDreamDaemon(true); + setError(null); + try { + await window.electronAPI.mind.setDreamDaemon(profile.mindId, next); + const updatedProfile = await window.electronAPI.mindProfile.get(profile.mindId); + setProfile(updatedProfile); + onProfileChanged?.(updatedProfile); + } catch (toggleError) { + setError(toggleError instanceof Error ? toggleError.message : String(toggleError)); + } finally { + setTogglingDreamDaemon(false); + } + }; + return ( <> @@ -178,6 +198,52 @@ export function AgentProfileModal({ mind, open, onOpenChange, onProfileChanged } /> ))} + + {/* + Dream-daemon opt-in toggle. Mirrors the genesis-time + switch in RoleScreen so an existing mind can be opted + in/out post-creation. Flipping it triggers a mind reload + (see MindManager.enableDreamDaemon / disableDreamDaemon) + so the new opt-in state takes effect immediately — + composer reads the gate, DailyLogWriter migrates legacy + `log.md` if needed. + Gated behind the app-level `dreamDaemon` feature flag: + when off, the entire row is hidden. MindProfileService + also forces `dreamDaemonEnabled: false` server-side in + the same case, so the data and UI agree. + */} + {dreamDaemonFlag && ( +
+
+
Enable dream daemon
+
+ Background memory consolidation. When enabled, this agent's chat history is structured and summarized over time. +
+
+ +
+ )} ) : null} diff --git a/apps/web/src/renderer/components/settings/SettingsView.test.tsx b/apps/web/src/renderer/components/settings/SettingsView.test.tsx index 70c6db45..b95c02a4 100644 --- a/apps/web/src/renderer/components/settings/SettingsView.test.tsx +++ b/apps/web/src/renderer/components/settings/SettingsView.test.tsx @@ -79,7 +79,7 @@ describe('SettingsView', () => { it('shows Local & Custom LLM settings when BYO LLM is feature-flagged on', async () => { render( - + , ); diff --git a/apps/web/src/renderer/lib/store/reducer.test.ts b/apps/web/src/renderer/lib/store/reducer.test.ts index 0bf0fc2a..dd48ee1a 100644 --- a/apps/web/src/renderer/lib/store/reducer.test.ts +++ b/apps/web/src/renderer/lib/store/reducer.test.ts @@ -650,7 +650,7 @@ describe('appReducer', () => { it('SET_FEATURE_FLAGS updates feature flags', () => { const state = appReducer(initialState, { type: 'SET_FEATURE_FLAGS', - payload: { switchboardRelay: true, byoLlm: true, chamberCopilot: true }, + payload: { switchboardRelay: true, byoLlm: true, chamberCopilot: true, dreamDaemon: true }, }); expect(state.featureFlags.switchboardRelay).toBe(true); expect(state.featureFlags.byoLlm).toBe(true); diff --git a/apps/web/src/test/helpers.ts b/apps/web/src/test/helpers.ts index 35b8bac1..b543d13a 100644 --- a/apps/web/src/test/helpers.ts +++ b/apps/web/src/test/helpers.ts @@ -133,6 +133,7 @@ export function mockElectronAPI(): ElectronAPI { list: vi.fn().mockResolvedValue([]), setActive: vi.fn().mockResolvedValue(undefined), setModel: vi.fn().mockResolvedValue(null), + setDreamDaemon: vi.fn().mockResolvedValue({ mindId: 'test-1234', mindPath: 'C:\\test', identity: { name: 'Test', systemMessage: '' }, status: 'ready' }), selectDirectory: vi.fn().mockResolvedValue(null), openWindow: vi.fn().mockResolvedValue(undefined), onMindChanged: vi.fn().mockReturnValue(vi.fn()), @@ -147,6 +148,7 @@ export function mockElectronAPI(): ElectronAPI { soul: { kind: 'soul', label: 'SOUL.md', relativePath: 'SOUL.md', content: '# Test\n', exists: true, mtimeMs: 1 }, agentFiles: [{ kind: 'agent', label: 'test.agent.md', relativePath: '.github\\agents\\test.agent.md', content: '# Test agent\n', exists: true, mtimeMs: 2 }], needsRestart: false, + dreamDaemonEnabled: false, })), saveFile: vi.fn().mockResolvedValue({ success: true, needsRestart: true, profile: { mindId: 'test-1234', @@ -157,6 +159,7 @@ export function mockElectronAPI(): ElectronAPI { soul: { kind: 'soul', label: 'SOUL.md', relativePath: 'SOUL.md', content: '# Test\n', exists: true, mtimeMs: 3 }, agentFiles: [{ kind: 'agent', label: 'test.agent.md', relativePath: '.github\\agents\\test.agent.md', content: '# Test agent\n', exists: true, mtimeMs: 2 }], needsRestart: true, + dreamDaemonEnabled: false, } }), pickAvatarImage: vi.fn().mockResolvedValue({ success: false, error: 'not stubbed' }), saveAvatar: vi.fn().mockResolvedValue({ success: false, error: 'not stubbed' }), @@ -350,7 +353,7 @@ export function mockElectronAPI(): ElectronAPI { close: vi.fn(), }, app: { - getFeatureFlags: vi.fn().mockResolvedValue({ switchboardRelay: false, byoLlm: false, chamberCopilot: false }), + getFeatureFlags: vi.fn().mockResolvedValue({ switchboardRelay: false, byoLlm: false, chamberCopilot: false, dreamDaemon: false }), onStartupProgress: vi.fn().mockReturnValue(vi.fn()), }, }; diff --git a/config/vitest.config.ts b/config/vitest.config.ts index ec17f6fb..6d6eb9d4 100644 --- a/config/vitest.config.ts +++ b/config/vitest.config.ts @@ -6,10 +6,20 @@ export default defineConfig({ test: { globals: true, environment: 'node', + // INVARIANT: use the `forks` pool, not the default `threads` pool. + // better-sqlite3 native finalizers race the worker-thread teardown when + // running on a thread pool; the result is a benign "Worker exited + // unexpectedly" error after all tests pass, which makes vitest exit 1 + // and silently drops ~25 tests partway through the run. Forks isolate + // each test file in a real subprocess, so native modules clean up at + // process exit. Pair with --no-file-parallelism --maxWorkers=1 in the + // npm script for serial execution. + pool: 'forks', include: [ 'apps/**/*.{test,spec}.{ts,tsx}', 'packages/**/*.{test,spec}.{ts,tsx}', 'tests/regression/**/*.{test,spec}.{ts,tsx}', + 'tests/integration/**/*.{test,spec}.{ts,tsx}', '.github/extensions/**/*.{test,spec}.mjs', ], exclude: ['node_modules', 'dist', 'out', '.vite', 'apps/*/dist', 'packages/*/dist'], diff --git a/docs/flags/v1/flags.json b/docs/flags/v1/flags.json index 6261aa25..3e3b1064 100644 --- a/docs/flags/v1/flags.json +++ b/docs/flags/v1/flags.json @@ -5,12 +5,14 @@ "stable": { "switchboardRelay": false, "byoLlm": false, - "chamberCopilot": false + "chamberCopilot": false, + "dreamDaemon": false }, "insiders": { "switchboardRelay": true, "byoLlm": true, - "chamberCopilot": true + "chamberCopilot": true, + "dreamDaemon": false } } } diff --git a/package-lock.json b/package-lock.json index e01a1d3a..47f2f47f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,16 +111,12 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "version": "4.5.0", "dev": true, "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "license": "MIT", "dependencies": { @@ -136,8 +132,6 @@ }, "node_modules/@asamuzakjp/dom-selector": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -153,8 +147,6 @@ }, "node_modules/@asamuzakjp/generational-cache": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", "dev": true, "license": "MIT", "engines": { @@ -163,27 +155,21 @@ }, "node_modules/@asamuzakjp/nwsapi": { "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "dev": true, "license": "MIT" }, "node_modules/@azure/msal-common": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.0.tgz", - "integrity": "sha512-FemGljX0csPlBMUE5GUan7BfRn1emeMRUhHSARhqzLN6LA9nt+MgzmAQ1xVqdLm+6plVoxsq9mS5eoyKtpPSgA==", + "version": "16.6.2", "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.0.tgz", - "integrity": "sha512-b/ak8XAqpnGk1N1nsyTVV0Remp48BP3QrGQZ1uCMcvg2S8X1eSXzhHQZEae2oX276Q4KFAqCUswanDtcvIKLrw==", + "version": "5.2.2", "license": "MIT", "dependencies": { - "@azure/msal-common": "16.6.0", + "@azure/msal-common": "16.6.2", "jsonwebtoken": "^9.0.0" }, "engines": { @@ -191,13 +177,11 @@ } }, "node_modules/@azure/msal-node-extensions": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node-extensions/-/msal-node-extensions-5.2.0.tgz", - "integrity": "sha512-3qrb4wxYBfJarrG9NRG5jRVhZfL9vZaDpeq2RDKPiNnA+e0/TKS8JzvXfIZqxn4VIP6/9P6/SSCJS+Y18LIAuw==", + "version": "5.2.2", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "16.6.0", + "@azure/msal-common": "16.6.2", "@azure/msal-node-runtime": "^0.20.0", "keytar": "^7.8.0" }, @@ -207,20 +191,16 @@ }, "node_modules/@azure/msal-node-runtime": { "version": "0.20.5", - "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.20.5.tgz", - "integrity": "sha512-DqY28Lpx67AsMbT3FYal3MnDZx62Pblnhp1qA+HGtgEsm3jK8COkJVCrhVsprn80PKQfAOdKRuxgVYyvmv2rOg==", "hasInstallScript": true, "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -230,8 +210,6 @@ }, "node_modules/@babel/generator": { "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.3.tgz", - "integrity": "sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==", "dev": true, "license": "MIT", "dependencies": { @@ -247,59 +225,49 @@ } }, "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.3.tgz", - "integrity": "sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.3.tgz", - "integrity": "sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.3.tgz", - "integrity": "sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^8.0.0-rc.3" + "@babel/types": "^8.0.0-rc.6" }, "bin": { "parser": "bin/babel-parser.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.3.tgz", - "integrity": "sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^8.0.0-rc.3", - "@babel/helper-validator-identifier": "^8.0.0-rc.3" + "@babel/helper-string-parser": "^8.0.0-rc.6", + "@babel/helper-validator-identifier": "^8.0.0-rc.6" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", "dev": true, "license": "MIT", "engines": { @@ -307,9 +275,7 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", "dev": true, "license": "MIT", "engines": { @@ -317,13 +283,11 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -333,9 +297,7 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", "dev": true, "license": "MIT", "engines": { @@ -343,14 +305,12 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -358,8 +318,6 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { @@ -368,8 +326,6 @@ }, "node_modules/@bramus/specificity": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, "license": "MIT", "dependencies": { @@ -413,8 +369,6 @@ }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -432,9 +386,7 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", - "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "version": "3.2.1", "dev": true, "funding": [ { @@ -456,9 +408,7 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", - "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "version": "4.1.1", "dev": true, "funding": [ { @@ -473,7 +423,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.0" + "@csstools/css-calc": "^3.2.1" }, "engines": { "node": ">=20.19.0" @@ -485,8 +435,6 @@ }, "node_modules/@csstools/css-parser-algorithms": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -507,9 +455,7 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", - "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "version": "1.1.4", "dev": true, "funding": [ { @@ -533,8 +479,6 @@ }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -553,8 +497,6 @@ }, "node_modules/@develar/schema-utils": { "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", "dev": true, "license": "MIT", "dependencies": { @@ -570,25 +512,19 @@ } }, "node_modules/@electron-forge/cli": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.11.1.tgz", - "integrity": "sha512-pk8AoLsr7t7LBAt0cFD06XFA6uxtPdvtLx06xeal7O9o7GHGCbj29WGwFoJ8Br/ENM0Ho868S3PrAn1PtBXt5g==", + "version": "7.11.2", "dev": true, "funding": [ { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-cli?utm_medium=referral&utm_source=npm_fund" + "type": "opencollective", + "url": "https://opencollective.com/electron" } ], "license": "MIT", "dependencies": { - "@electron-forge/core": "7.11.1", - "@electron-forge/core-utils": "7.11.1", - "@electron-forge/shared-types": "7.11.1", + "@electron-forge/core": "7.11.2", + "@electron-forge/core-utils": "7.11.2", + "@electron-forge/shared-types": "7.11.2", "@electron/get": "^3.0.0", "@inquirer/prompts": "^6.0.1", "@listr2/prompt-adapter-inquirer": "^2.0.22", @@ -610,33 +546,27 @@ } }, "node_modules/@electron-forge/core": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.11.1.tgz", - "integrity": "sha512-YtuPLzggPKPabFAD2rOZFE0s7f4KaUTpGRduhSMbZUqpqD1TIPyfoDBpYiZvao3Ht8pyZeOJjbzcC0LpFs9gIQ==", + "version": "7.11.2", "dev": true, "funding": [ { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-core?utm_medium=referral&utm_source=npm_fund" + "type": "opencollective", + "url": "https://opencollective.com/electron" } ], "license": "MIT", "dependencies": { - "@electron-forge/core-utils": "7.11.1", - "@electron-forge/maker-base": "7.11.1", - "@electron-forge/plugin-base": "7.11.1", - "@electron-forge/publisher-base": "7.11.1", - "@electron-forge/shared-types": "7.11.1", - "@electron-forge/template-base": "7.11.1", - "@electron-forge/template-vite": "7.11.1", - "@electron-forge/template-vite-typescript": "7.11.1", - "@electron-forge/template-webpack": "7.11.1", - "@electron-forge/template-webpack-typescript": "7.11.1", - "@electron-forge/tracer": "7.11.1", + "@electron-forge/core-utils": "7.11.2", + "@electron-forge/maker-base": "7.11.2", + "@electron-forge/plugin-base": "7.11.2", + "@electron-forge/publisher-base": "7.11.2", + "@electron-forge/shared-types": "7.11.2", + "@electron-forge/template-base": "7.11.2", + "@electron-forge/template-vite": "7.11.2", + "@electron-forge/template-vite-typescript": "7.11.2", + "@electron-forge/template-webpack": "7.11.2", + "@electron-forge/template-webpack-typescript": "7.11.2", + "@electron-forge/tracer": "7.11.2", "@electron/get": "^3.0.0", "@electron/packager": "^18.3.5", "@electron/rebuild": "^3.7.0", @@ -644,6 +574,7 @@ "@vscode/sudo-prompt": "^9.3.1", "chalk": "^4.0.0", "debug": "^4.3.1", + "eta": "^3.5.0", "fast-glob": "^3.2.7", "filenamify": "^4.1.0", "find-up": "^5.0.0", @@ -653,7 +584,6 @@ "interpret": "^3.1.1", "jiti": "^2.4.2", "listr2": "^7.0.2", - "lodash": "^4.17.20", "log-symbols": "^4.0.0", "node-fetch": "^2.6.7", "rechoir": "^0.8.0", @@ -666,13 +596,11 @@ } }, "node_modules/@electron-forge/core-utils": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.11.1.tgz", - "integrity": "sha512-9UxRWVsfcziBsbAA2MS0Oz4yYovQCO2BhnGIfsbKNTBtMc/RcVSxAS0NMyymce44i43p1ZC/FqWhnt1XqYw3bQ==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.11.1", + "@electron-forge/shared-types": "7.11.2", "@electron/rebuild": "^3.7.0", "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", @@ -688,13 +616,11 @@ } }, "node_modules/@electron-forge/maker-base": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.11.1.tgz", - "integrity": "sha512-yhZrCGoN6bDeiB5DHFaueZ1h84AReElEj+f0hl2Ph4UbZnO0cnLpbx+Bs+XfMLAiA+beC8muB5UDK5ysfuT9BQ==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.11.1", + "@electron-forge/shared-types": "7.11.2", "fs-extra": "^10.0.0", "which": "^2.0.2" }, @@ -703,14 +629,12 @@ } }, "node_modules/@electron-forge/maker-deb": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/maker-deb/-/maker-deb-7.11.1.tgz", - "integrity": "sha512-QTYiryQLYPDkq6pIfBmx0GQ6D8QatUkowH7rTlW5MnCUa0uumX0Xu7yGIjesuwW37fxT3Lv4xi+FSXMCm2eC1w==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/maker-base": "7.11.1", - "@electron-forge/shared-types": "7.11.1" + "@electron-forge/maker-base": "7.11.2", + "@electron-forge/shared-types": "7.11.2" }, "engines": { "node": ">= 16.4.0" @@ -719,15 +643,138 @@ "electron-installer-debian": "^3.2.0" } }, + "node_modules/@electron-forge/maker-deb/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@electron-forge/maker-deb/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@electron-forge/maker-deb/node_modules/electron-installer-debian": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/electron-installer-debian/-/electron-installer-debian-3.2.0.tgz", + "integrity": "sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw==", + "dev": true, + "optional": true, + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "@malept/cross-spawn-promise": "^1.0.0", + "debug": "^4.1.1", + "electron-installer-common": "^0.10.2", + "fs-extra": "^9.0.0", + "get-folder-size": "^2.0.1", + "lodash": "^4.17.4", + "word-wrap": "^1.2.3", + "yargs": "^16.0.2" + }, + "bin": { + "electron-installer-debian": "src/cli.js" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron-forge/maker-deb/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron-forge/maker-deb/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@electron-forge/maker-deb/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "optional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron-forge/maker-deb/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/@electron-forge/maker-squirrel": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/maker-squirrel/-/maker-squirrel-7.11.1.tgz", - "integrity": "sha512-oSg7fgad6l+X0DjtRkSpMzB0AjzyDO4mb2gzM4kTodkP1ADeiMi08bxy0ZeCESqLm5+fG72cAPmEr3BAPvI1yw==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/maker-base": "7.11.1", - "@electron-forge/shared-types": "7.11.1", + "@electron-forge/maker-base": "7.11.2", + "@electron-forge/shared-types": "7.11.2", "fs-extra": "^10.0.0" }, "engines": { @@ -738,14 +785,12 @@ } }, "node_modules/@electron-forge/maker-zip": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/maker-zip/-/maker-zip-7.11.1.tgz", - "integrity": "sha512-30rcp0AbJLfkFBX2hmO14LKXx7z9V61LffTVbTCFMh5vUB2kZvcA5xAhsBk2oUJWfGVxe1DuSEU0rDR9bUMHUg==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/maker-base": "7.11.1", - "@electron-forge/shared-types": "7.11.1", + "@electron-forge/maker-base": "7.11.2", + "@electron-forge/shared-types": "7.11.2", "cross-zip": "^4.0.0", "fs-extra": "^10.0.0", "got": "^11.8.5" @@ -755,41 +800,35 @@ } }, "node_modules/@electron-forge/plugin-auto-unpack-natives": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.11.1.tgz", - "integrity": "sha512-5uRM3WNv7jIeDt8pLP3V4U2puWHPGJ/3qRuSE47RKgTp5qxpZidWHSYcEJJxjoqOL/7KFwSqKSQ/a36GoZV4Fg==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/plugin-base": "7.11.1", - "@electron-forge/shared-types": "7.11.1" + "@electron-forge/plugin-base": "7.11.2", + "@electron-forge/shared-types": "7.11.2" }, "engines": { "node": ">= 16.4.0" } }, "node_modules/@electron-forge/plugin-base": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.11.1.tgz", - "integrity": "sha512-lKpSOV1GA3FoYiD9k05i6v4KaQVmojnRgCr7d6VL1bFp13QOtXSaAWhFI9mtSY7rGElOacX6Zt7P7rPoB8T9eQ==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.11.1" + "@electron-forge/shared-types": "7.11.2" }, "engines": { "node": ">= 16.4.0" } }, "node_modules/@electron-forge/plugin-fuses": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.11.1.tgz", - "integrity": "sha512-Td517mHf+RjQAayFDM2kKb7NaGdRXrZfPbc7KOHlGbXthp5YTkFu2cCZGWokiqt1y1wsFaAodULhqBIg7vbbbw==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/plugin-base": "7.11.1", - "@electron-forge/shared-types": "7.11.1" + "@electron-forge/plugin-base": "7.11.2", + "@electron-forge/shared-types": "7.11.2" }, "engines": { "node": ">= 16.4.0" @@ -799,14 +838,12 @@ } }, "node_modules/@electron-forge/plugin-vite": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/plugin-vite/-/plugin-vite-7.11.1.tgz", - "integrity": "sha512-kc/WQs/0+9VC9Q4oSSocMa02YxKDvAYxhWtNcL+qlswZMJlxe8gX7vl/yXq9AjPQxw7f3jzf7nruUPKQ+vyLLg==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/plugin-base": "7.11.1", - "@electron-forge/shared-types": "7.11.1", + "@electron-forge/plugin-base": "7.11.2", + "@electron-forge/shared-types": "7.11.2", "chalk": "^4.0.0", "debug": "^4.3.1", "fs-extra": "^10.0.0", @@ -817,27 +854,23 @@ } }, "node_modules/@electron-forge/publisher-base": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-7.11.1.tgz", - "integrity": "sha512-rXE9oMFGMtdQrixnumWYH5TTGsp99iPHZb3jI74YWq518ctCh6DlIgWlhf6ok2X0+lhWovcIb45KJucUFAQ13w==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.11.1" + "@electron-forge/shared-types": "7.11.2" }, "engines": { "node": ">= 16.4.0" } }, "node_modules/@electron-forge/publisher-github": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/publisher-github/-/publisher-github-7.11.1.tgz", - "integrity": "sha512-3S7DS1NZRrYvf59eqH0F2ke9oLD5FQqW5+t6kY1EuEo6I8HF+u6dOkGnvzhRh+uvKkjy4ynV3j735PlqBbClGQ==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/publisher-base": "7.11.1", - "@electron-forge/shared-types": "7.11.1", + "@electron-forge/publisher-base": "7.11.2", + "@electron-forge/shared-types": "7.11.2", "@octokit/core": "^5.2.1", "@octokit/plugin-retry": "^6.1.0", "@octokit/request-error": "^5.1.1", @@ -854,13 +887,11 @@ } }, "node_modules/@electron-forge/shared-types": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.11.1.tgz", - "integrity": "sha512-vvBWdAEh53UJlDGUevpaJk1+sqDMQibfrbHR+0IPA4MPyQex7/Uhv3vYH9oGHujBVAChQahjAuJt0fG6IJBLZg==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/tracer": "7.11.1", + "@electron-forge/tracer": "7.11.2", "@electron/packager": "^18.3.5", "@electron/rebuild": "^3.7.0", "listr2": "^7.0.2" @@ -870,14 +901,12 @@ } }, "node_modules/@electron-forge/template-base": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.11.1.tgz", - "integrity": "sha512-XpTaEf+EfQw+0BlSAtSpZKYIKYvKu4raNzSGHZZoSYHp+HDC7R+MlpFQmSJiGdYQzQ14C+uxO42tVjgM0DMbpw==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/core-utils": "7.11.1", - "@electron-forge/shared-types": "7.11.1", + "@electron-forge/core-utils": "7.11.2", + "@electron-forge/shared-types": "7.11.2", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "fs-extra": "^10.0.0", @@ -889,14 +918,12 @@ } }, "node_modules/@electron-forge/template-vite": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.11.1.tgz", - "integrity": "sha512-Or8Lxf4awoeUZoMTKJEw5KQDIhqOFs24WhVka3yZXxc6VgVWN79KmYKYM6uM/YMQttmafhsBhY2t1Lxo1WR/ug==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.11.1", - "@electron-forge/template-base": "7.11.1", + "@electron-forge/shared-types": "7.11.2", + "@electron-forge/template-base": "7.11.2", "fs-extra": "^10.0.0" }, "engines": { @@ -904,14 +931,12 @@ } }, "node_modules/@electron-forge/template-vite-typescript": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.11.1.tgz", - "integrity": "sha512-Us4AHXFb+4z+gXgZImSqMBS63oKnsQWLOhqRg321xiDzu2UcQPlwgWNb4rAEKNVC1e7LXrUNDHuBiTrQkvWXbg==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.11.1", - "@electron-forge/template-base": "7.11.1", + "@electron-forge/shared-types": "7.11.2", + "@electron-forge/template-base": "7.11.2", "fs-extra": "^10.0.0" }, "engines": { @@ -919,14 +944,12 @@ } }, "node_modules/@electron-forge/template-webpack": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.11.1.tgz", - "integrity": "sha512-15lbXxi+er461MPk6sbwAOyjofAHwmQjTvxNCiNpaU2naEwbj3t0SlLq/BMr5HxnVOaMmA7+lKV9afkIom+d4Q==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.11.1", - "@electron-forge/template-base": "7.11.1", + "@electron-forge/shared-types": "7.11.2", + "@electron-forge/template-base": "7.11.2", "fs-extra": "^10.0.0" }, "engines": { @@ -934,14 +957,12 @@ } }, "node_modules/@electron-forge/template-webpack-typescript": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.11.1.tgz", - "integrity": "sha512-6ExfFnFkHBz8rvRFTFg5HVGTC12uJpbVk4q8DVg0R8rhhxhqiVNh8lF2UPtZ2yT2UtGWjXNVlyP3Y3T6q6E3GQ==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { - "@electron-forge/shared-types": "7.11.1", - "@electron-forge/template-base": "7.11.1", + "@electron-forge/shared-types": "7.11.2", + "@electron-forge/template-base": "7.11.2", "fs-extra": "^10.0.0", "typescript": "~5.4.5", "webpack": "^5.69.1" @@ -952,8 +973,6 @@ }, "node_modules/@electron-forge/template-webpack-typescript/node_modules/typescript": { "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -965,9 +984,7 @@ } }, "node_modules/@electron-forge/tracer": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.11.1.tgz", - "integrity": "sha512-tiB6cglVQFcSw9N8GRwVwZUeB9u0DOx2Mj7aFXBUsFLUYQapvVGv51tUSy/UAW5lvmubGscYIILuVko+II3+NA==", + "version": "7.11.2", "dev": true, "license": "MIT", "dependencies": { @@ -979,8 +996,6 @@ }, "node_modules/@electron/asar": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, "license": "MIT", "dependencies": { @@ -997,8 +1012,6 @@ }, "node_modules/@electron/asar/node_modules/commander": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true, "license": "MIT", "engines": { @@ -1007,8 +1020,6 @@ }, "node_modules/@electron/fuses": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-2.1.1.tgz", - "integrity": "sha512-38ho27/mtUV/LpsZ1LCDJUomKBBSUZDk/qBH4FNNtoN5fmnkmWDcIp5pm1Kv3InqhRjKZKs7Jzx+wWZNMArHrA==", "dev": true, "license": "MIT", "bin": { @@ -1020,8 +1031,6 @@ }, "node_modules/@electron/get": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1042,8 +1051,6 @@ }, "node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1057,8 +1064,6 @@ }, "node_modules/@electron/get/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -1067,8 +1072,6 @@ }, "node_modules/@electron/get/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -1077,8 +1080,6 @@ }, "node_modules/@electron/get/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -1087,8 +1088,6 @@ }, "node_modules/@electron/notarize": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { @@ -1102,8 +1101,6 @@ }, "node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1118,8 +1115,6 @@ }, "node_modules/@electron/osx-sign": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1140,8 +1135,6 @@ }, "node_modules/@electron/packager": { "version": "18.4.4", - "resolved": "https://registry.npmjs.org/@electron/packager/-/packager-18.4.4.tgz", - "integrity": "sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1178,9 +1171,7 @@ } }, "node_modules/@electron/packager/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", "dev": true, "license": "MIT", "dependencies": { @@ -1194,8 +1185,6 @@ }, "node_modules/@electron/rebuild": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", "dev": true, "license": "MIT", "dependencies": { @@ -1213,23 +1202,8 @@ "node": ">=22.12.0" } }, - "node_modules/@electron/rebuild/node_modules/node-abi": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", - "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, "node_modules/@electron/universal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", "dev": true, "license": "MIT", "dependencies": { @@ -1246,9 +1220,7 @@ } }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", "dev": true, "license": "MIT", "dependencies": { @@ -1256,9 +1228,7 @@ } }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", "dev": true, "license": "MIT", "dependencies": { @@ -1272,8 +1242,6 @@ }, "node_modules/@electron/universal/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { @@ -1288,8 +1256,6 @@ }, "node_modules/@electron/windows-sign": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1307,9 +1273,7 @@ } }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", "dev": true, "license": "MIT", "dependencies": { @@ -1321,43 +1285,8 @@ "node": ">=14.14" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1373,10 +1302,19 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -1385,8 +1323,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1400,8 +1336,6 @@ }, "node_modules/@eslint/config-array/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -1409,9 +1343,7 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", "dev": true, "license": "MIT", "dependencies": { @@ -1423,8 +1355,6 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1438,9 +1368,7 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.6.0", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1452,8 +1380,6 @@ }, "node_modules/@eslint/core": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1465,8 +1391,6 @@ }, "node_modules/@eslint/js": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { @@ -1486,8 +1410,6 @@ }, "node_modules/@eslint/object-schema": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1496,8 +1418,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1509,9 +1429,7 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "version": "1.15.1", "dev": true, "license": "MIT", "engines": { @@ -1528,8 +1446,6 @@ }, "node_modules/@floating-ui/core": { "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.11" @@ -1537,8 +1453,6 @@ }, "node_modules/@floating-ui/dom": { "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.5", @@ -1547,8 +1461,6 @@ }, "node_modules/@floating-ui/react-dom": { "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.6" @@ -1560,8 +1472,6 @@ }, "node_modules/@floating-ui/utils": { "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@github/copilot": { @@ -1569,7 +1479,6 @@ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.57-2.tgz", "integrity": "sha512-VuUUAfvoyRhgebVMqcFO8wNNcBfkyXYnRhdYspitT86y2yoJiPCEVeeUzSpYBkF1a0nlGwtaDvKt1OOxaBlRBw==", "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "detect-libc": "^2.1.2" }, @@ -1595,7 +1504,6 @@ "arm64" ], "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ "darwin" @@ -1612,7 +1520,6 @@ "x64" ], "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ "darwin" @@ -1629,7 +1536,6 @@ "arm64" ], "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ "linux" @@ -1646,7 +1552,6 @@ "x64" ], "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ "linux" @@ -1663,7 +1568,6 @@ "arm64" ], "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ "linux" @@ -1680,7 +1584,6 @@ "x64" ], "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ "linux" @@ -1694,7 +1597,6 @@ "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.0-beta.8.tgz", "integrity": "sha512-lAuBfH6E5PUaSj8P/0FVMxzvwwBUs02tlvQ56PoJFtuc47KPqzGpf9BS7+h2eEr1UmjoLNJ/yqDiVApH9Oo1Fg==", "dev": true, - "license": "MIT", "dependencies": { "@github/copilot": "^1.0.55-1", "vscode-jsonrpc": "^8.2.1", @@ -1712,7 +1614,6 @@ "arm64" ], "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ "win32" @@ -1729,7 +1630,6 @@ "x64" ], "dev": true, - "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ "win32" @@ -1739,9 +1639,7 @@ } }, "node_modules/@hono/node-server": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.2.tgz", - "integrity": "sha512-tXlTi1h/4V7sDe7i97IVP+9re9ZU7wXZZggnR5ucCRclf1+AX6YhGStrR5w8bLj+3Mlyl0pKfBh9gqTqqnGKfQ==", + "version": "2.0.4", "license": "MIT", "engines": { "node": ">=20" @@ -1752,8 +1650,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1765,8 +1661,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1780,8 +1674,6 @@ }, "node_modules/@humanfs/types": { "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1790,8 +1682,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1804,8 +1694,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1826,1285 +1714,1497 @@ }, "node_modules/@img/colour": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { + "node_modules/@img/sharp-win32-x64": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ - "arm64" + "x64" ], - "license": "Apache-2.0", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node_modules/@inquirer/checkbox": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/core": { + "version": "9.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/editor": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/input": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/number": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/password": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@inquirer/search": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.22", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.5" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } ], "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "cross-spawn": "^7.0.1" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "engines": { + "node": ">= 12.13.0" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "engines": { + "node": ">= 10.0.0" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "engines": { + "node": ">=10" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "engines": { + "node": ">= 8" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "engines": { + "node": ">= 8" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "13.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "dev": true, + "license": "MIT", "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">= 18" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "dev": true, + "license": "MIT" }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">= 18" }, - "funding": { - "url": "https://opencollective.com/libvips" + "peerDependencies": { + "@octokit/core": "5" } }, - "node_modules/@inquirer/checkbox": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", - "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "13.10.0", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/figures": "^1.0.6", - "@inquirer/type": "^2.0.0", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@inquirer/confirm": { + "node_modules/@octokit/plugin-request-log": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", - "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/type": "^2.0.0" - }, "engines": { - "node": ">=18" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" } }, - "node_modules/@inquirer/core": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", - "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.6", - "@inquirer/type": "^2.0.0", - "@types/mute-stream": "^0.0.4", - "@types/node": "^22.5.5", - "@types/wrap-ansi": "^3.0.0", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "^1.0.0", - "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "@octokit/types": "^13.8.0" }, "engines": { - "node": ">=18" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" } }, - "node_modules/@inquirer/editor": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", - "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "13.10.0", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/type": "^2.0.0", - "external-editor": "^3.1.0" - }, - "engines": { - "node": ">=18" + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@inquirer/expand": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", - "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", + "node_modules/@octokit/plugin-retry": { + "version": "6.1.0", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/type": "^2.0.0", - "yoctocolors-cjs": "^2.1.2" + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" }, "engines": { - "node": ">=18" + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" } }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "24.2.0", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/@inquirer/input": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", - "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "13.10.0", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/type": "^2.0.0" - }, - "engines": { - "node": ">=18" + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@inquirer/number": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", - "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "node_modules/@octokit/request": { + "version": "8.4.1", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/type": "^2.0.0" + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=18" + "node": ">= 18" } }, - "node_modules/@inquirer/password": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", - "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "node_modules/@octokit/request-error": { + "version": "5.1.1", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/type": "^2.0.0", - "ansi-escapes": "^4.3.2" + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">= 18" } }, - "node_modules/@inquirer/prompts": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", - "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "13.10.0", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^3.0.1", - "@inquirer/confirm": "^4.0.1", - "@inquirer/editor": "^3.0.1", - "@inquirer/expand": "^3.0.1", - "@inquirer/input": "^3.0.1", - "@inquirer/number": "^2.0.1", - "@inquirer/password": "^3.0.1", - "@inquirer/rawlist": "^3.0.1", - "@inquirer/search": "^2.0.1", - "@inquirer/select": "^3.0.1" - }, - "engines": { - "node": ">=18" + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@inquirer/rawlist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", - "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "13.10.0", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/type": "^2.0.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@inquirer/search": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", - "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "node_modules/@octokit/rest": { + "version": "20.1.2", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/figures": "^1.0.6", - "@inquirer/type": "^2.0.0", - "yoctocolors-cjs": "^2.1.2" + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" }, "engines": { - "node": ">=18" + "node": ">= 18" } }, - "node_modules/@inquirer/select": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", - "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "node_modules/@octokit/types": { + "version": "6.41.0", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.2.1", - "@inquirer/figures": "^1.0.6", - "@inquirer/type": "^2.0.0", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" + "@octokit/openapi-types": "^12.11.0" } }, - "node_modules/@inquirer/type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", - "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "node_modules/@oxc-project/types": { + "version": "0.127.0", "dev": true, "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@package-json/types": { + "version": "0.0.12", + "dev": true, + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "mute-stream": "^1.0.0" + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" }, "engines": { "node": ">=18" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "node_modules/@quansync/fs": { + "version": "1.0.0", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "minipass": "^7.0.4" + "quansync": "^1.0.0" }, - "engines": { - "node": ">=18.0.0" + "funding": { + "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.22.tgz", - "integrity": "sha512-hV36ZoY+xKL6pYOt1nPNnkciFkn89KZwqLhAFzJvYysAvL5uBQdiADZx/8bIDXIukzzwG0QlPYolgMzQUtKgpQ==", - "dev": true, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", "license": "MIT", "dependencies": { - "@inquirer/type": "^1.5.5" - }, - "engines": { - "node": ">=18.0.0" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { - "@inquirer/prompts": ">= 3 < 8" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", - "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", - "dev": true, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "mute-stream": "^1.0.0" + "@radix-ui/react-slot": "1.2.3" }, - "engines": { - "node": ">=18" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + "@types/react-dom": { + "optional": true } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" } }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" + "@radix-ui/react-slot": "1.2.3" }, - "engines": { - "node": ">= 10.0.0" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", - "dev": true, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", - "engines": { - "node": ">= 18" + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", - "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", - "dev": true, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", "license": "MIT", "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.1.0", - "@octokit/request": "^8.4.1", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" }, - "engines": { - "node": ">= 18" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/endpoint": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", - "dev": true, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", "license": "MIT", - "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": ">= 18" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@octokit/graphql": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", - "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", - "dev": true, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", "license": "MIT", "dependencies": { - "@octokit/request": "^8.4.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, - "engines": { - "node": ">= 18" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, + "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "11.4.4-cjs.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", - "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", - "dev": true, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", "license": "MIT", "dependencies": { - "@octokit/types": "^13.7.0" - }, - "engines": { - "node": ">= 18" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { - "@octokit/core": "5" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/plugin-request-log": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", - "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", - "dev": true, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", "license": "MIT", - "engines": { - "node": ">= 18" - }, "peerDependencies": { - "@octokit/core": "5" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "13.3.2-cjs.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", - "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", - "dev": true, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", "license": "MIT", "dependencies": { - "@octokit/types": "^13.8.0" - }, - "engines": { - "node": ">= 18" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { - "@octokit/core": "^5" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/plugin-retry": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.1.0.tgz", - "integrity": "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==", - "dev": true, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", "license": "MIT", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^13.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 18" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { - "@octokit/core": "5" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/request": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", - "dev": true, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": ">= 18" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@octokit/request-error": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", - "dev": true, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", "license": "MIT", "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, - "engines": { - "node": ">= 18" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/rest": { - "version": "20.1.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", - "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", - "dev": true, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@octokit/core": "^5.0.2", - "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", - "@octokit/plugin-request-log": "^4.0.0", - "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + "@radix-ui/react-slot": "1.2.3" }, - "engines": { - "node": ">= 18" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dev": true, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^12.11.0" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", - "dev": true, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@package-json/types": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@package-json/types/-/types-0.0.12.tgz", - "integrity": "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@playwright/test": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", - "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "playwright": "1.59.1" + "@radix-ui/react-slot": "1.2.3" }, - "bin": { - "playwright": "cli.js" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": ">=18" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@quansync/fs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", - "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", - "dev": true, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", "license": "MIT", "dependencies": { - "quansync": "^1.0.0" + "@radix-ui/react-use-layout-effect": "1.1.1" }, - "funding": { - "url": "https://github.com/sponsors/sxzz" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", - "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", "license": "MIT", "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.3" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -3121,21 +3221,11 @@ } } }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", - "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3152,18 +3242,28 @@ } } }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", - "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -3180,13 +3280,11 @@ } } }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3203,13 +3301,20 @@ } } }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", - "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -3226,17 +3331,11 @@ } } }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3253,20 +3352,24 @@ } } }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3283,20 +3386,11 @@ } } }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3313,16 +3407,22 @@ } } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3339,48 +3439,60 @@ } } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", - "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "node_modules/@radix-ui/react-password-toggle-field/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3397,10 +3509,8 @@ } } }, - "node_modules/@radix-ui/react-dialog": { + "node_modules/@radix-ui/react-popover": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3410,6 +3520,7 @@ "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", @@ -3433,32 +3544,41 @@ } } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3475,19 +3595,11 @@ } } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3504,30 +3616,33 @@ } } }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3544,18 +3659,12 @@ } } }, - "node_modules/@radix-ui/react-form": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", - "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3572,21 +3681,11 @@ } } }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", - "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -3603,13 +3702,11 @@ } } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -3621,12 +3718,11 @@ } } }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", "license": "MIT", "dependencies": { + "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { @@ -3644,30 +3740,62 @@ } } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3684,10 +3812,8 @@ } } }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", - "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3696,9 +3822,8 @@ "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -3716,26 +3841,11 @@ } } }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", - "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3752,23 +3862,18 @@ } } }, - "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", - "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { @@ -3786,20 +3891,11 @@ } } }, - "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", - "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3816,25 +3912,29 @@ } } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -3853,22 +3953,11 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3885,14 +3974,11 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -3909,14 +3995,11 @@ } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3933,13 +4016,21 @@ } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -3956,14 +4047,11 @@ } } }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -3980,53 +4068,33 @@ } } }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -4043,21 +4111,11 @@ } } }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4074,33 +4132,18 @@ } } }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -4117,13 +4160,11 @@ } } }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4140,23 +4181,22 @@ } } }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4173,37 +4213,34 @@ } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -4220,19 +4257,16 @@ } } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -4249,25 +4283,12 @@ "optional": true } } - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4284,15 +4305,11 @@ } } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4309,10 +4326,8 @@ } } }, - "node_modules/@radix-ui/react-toggle-group": { + "node_modules/@radix-ui/react-toolbar": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -4320,8 +4335,8 @@ "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", @@ -4338,19 +4353,11 @@ } } }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", - "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-toggle-group": "1.1.11" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -4369,8 +4376,6 @@ }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -4401,10 +4406,29 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4418,8 +4442,6 @@ }, "node_modules/@radix-ui/react-use-controllable-state": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -4437,8 +4459,6 @@ }, "node_modules/@radix-ui/react-use-effect-event": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4455,8 +4475,6 @@ }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" @@ -4473,8 +4491,6 @@ }, "node_modules/@radix-ui/react-use-is-hydrated": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", "license": "MIT", "dependencies": { "use-sync-external-store": "^1.5.0" @@ -4491,8 +4507,6 @@ }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4506,8 +4520,6 @@ }, "node_modules/@radix-ui/react-use-previous": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4521,8 +4533,6 @@ }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" @@ -4531,322 +4541,76 @@ "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", - "cpu": [ - "wasm32" - ], - "dev": true, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@radix-ui/react-use-layout-effect": "1.1.1" }, - "engines": { - "node": "^20.19.0 || >=22.12.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", "license": "MIT", - "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "license": "MIT" + }, "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "cpu": [ "x64" ], @@ -4862,15 +4626,11 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "dev": true, "license": "MIT" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, "license": "MIT", "engines": { @@ -4882,8 +4642,6 @@ }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, "license": "MIT", "engines": { @@ -4895,15 +4653,11 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, "license": "MIT", "dependencies": { @@ -4914,313 +4668,43 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", - "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "version": "4.3.0", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", + "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.4" + "tailwindcss": "4.3.0" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", - "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-x64": "4.2.4", - "@tailwindcss/oxide-freebsd-x64": "4.2.4", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-x64-musl": "4.2.4", - "@tailwindcss/oxide-wasm32-wasi": "4.2.4", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", - "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", - "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", - "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", - "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", - "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", - "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", - "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", - "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", - "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", - "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", - "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "version": "4.3.0", + "dev": true, + "license": "MIT", "engines": { "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", - "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "version": "4.3.0", "cpu": [ "x64" ], @@ -5236,8 +4720,6 @@ }, "node_modules/@tailwindcss/typography": { "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", - "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", "dev": true, "license": "MIT", "dependencies": { @@ -5248,15 +4730,13 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", - "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", + "version": "4.3.0", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "tailwindcss": "4.2.4" + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -5264,8 +4744,6 @@ }, "node_modules/@testing-library/dom": { "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", "peer": true, @@ -5285,8 +4763,6 @@ }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, "license": "MIT", "dependencies": { @@ -5305,15 +4781,11 @@ }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true, "license": "MIT" }, "node_modules/@testing-library/react": { "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, "license": "MIT", "dependencies": { @@ -5338,29 +4810,14 @@ } } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "dev": true, "license": "MIT", "dependencies": { @@ -5369,8 +4826,6 @@ }, "node_modules/@types/cacheable-request": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, "license": "MIT", "dependencies": { @@ -5382,8 +4837,6 @@ }, "node_modules/@types/chai": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -5393,8 +4846,6 @@ }, "node_modules/@types/debug": { "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -5402,57 +4853,25 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/electron-squirrel-startup": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/electron-squirrel-startup/-/electron-squirrel-startup-1.0.2.tgz", - "integrity": "sha512-AzxnvBzNh8K/0SmxMmZtpJf1/IWoGXLP+pQDuUaVkPyotI8ryvAtBSqgxR/qOSvxWHYWrxkeNsJ+Ca5xOuUxJQ==", "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", "license": "MIT", "dependencies": { "@types/estree": "*" @@ -5460,8 +4879,6 @@ }, "node_modules/@types/fs-extra": { "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, "license": "MIT", "dependencies": { @@ -5470,8 +4887,6 @@ }, "node_modules/@types/hast": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -5479,36 +4894,26 @@ }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "dev": true, "license": "MIT" }, "node_modules/@types/jsesc": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/katex": { "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", "dev": true, "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, "license": "MIT", "dependencies": { @@ -5517,8 +4922,6 @@ }, "node_modules/@types/mdast": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -5526,14 +4929,10 @@ }, "node_modules/@types/ms": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/mute-stream": { "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, "license": "MIT", "dependencies": { @@ -5541,31 +4940,15 @@ } }, "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "version": "22.19.19", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "version": "19.2.15", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -5573,8 +4956,6 @@ }, "node_modules/@types/react-dom": { "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", "peerDependencies": { @@ -5583,8 +4964,6 @@ }, "node_modules/@types/responselike": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "license": "MIT", "dependencies": { @@ -5592,30 +4971,16 @@ } }, "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "version": "2.0.11", "license": "MIT" }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", - "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", "dev": true, "license": "MIT" }, "node_modules/@types/ws": { "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -5624,8 +4989,6 @@ }, "node_modules/@types/yauzl": { "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "license": "MIT", "optional": true, @@ -5634,17 +4997,15 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", - "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "version": "8.60.0", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/type-utils": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/type-utils": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -5657,22 +5018,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.0", + "@typescript-eslint/parser": "^8.60.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", - "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "version": "8.60.0", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3" }, "engines": { @@ -5688,14 +5047,12 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", - "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "version": "8.60.0", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.0", - "@typescript-eslint/types": "^8.59.0", + "@typescript-eslint/tsconfig-utils": "^8.60.0", + "@typescript-eslint/types": "^8.60.0", "debug": "^4.4.3" }, "engines": { @@ -5710,14 +5067,12 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", - "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "version": "8.60.0", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0" + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5728,9 +5083,7 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", - "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "version": "8.60.0", "dev": true, "license": "MIT", "engines": { @@ -5745,59 +5098,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", - "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", - "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", - "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "version": "8.60.0", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.0", - "@typescript-eslint/tsconfig-utils": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0", "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "engines": { @@ -5806,370 +5114,127 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", - "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", - "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], + "node_modules/@typescript-eslint/types": { + "version": "8.60.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@typescript-eslint/project-service": "8.60.0", + "@typescript-eslint/tsconfig-utils": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", "dev": true, - "license": "MIT", - "optional": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=14.0.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/utils": { + "version": "8.60.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "license": "ISC" }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "version": "1.12.2", "cpu": [ "x64" ], @@ -6181,14 +5246,12 @@ ] }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", - "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", + "version": "4.1.7", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.7", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -6202,8 +5265,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.5", - "vitest": "4.1.5" + "@vitest/browser": "4.1.7", + "vitest": "4.1.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6212,16 +5275,14 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "version": "4.1.7", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -6229,10 +5290,33 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.7", "dev": true, "license": "MIT", "dependencies": { @@ -6243,13 +5327,11 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "version": "4.1.7", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.7", "pathe": "^2.0.3" }, "funding": { @@ -6257,14 +5339,12 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "version": "4.1.7", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -6273,9 +5353,7 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "version": "4.1.7", "dev": true, "license": "MIT", "funding": { @@ -6283,13 +5361,11 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.7", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.7", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -6299,15 +5375,11 @@ }, "node_modules/@vscode/sudo-prompt": { "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", - "integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6317,29 +5389,21 @@ }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", "dependencies": { @@ -6350,15 +5414,11 @@ }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -6370,8 +5430,6 @@ }, "node_modules/@webassemblyjs/ieee754": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", "dependencies": { @@ -6380,8 +5438,6 @@ }, "node_modules/@webassemblyjs/leb128": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6390,15 +5446,11 @@ }, "node_modules/@webassemblyjs/utf8": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6414,8 +5466,6 @@ }, "node_modules/@webassemblyjs/wasm-gen": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", "dependencies": { @@ -6428,8 +5478,6 @@ }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -6441,8 +5489,6 @@ }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6456,8 +5502,6 @@ }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", "dependencies": { @@ -6466,40 +5510,30 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "version": "0.9.10", "dev": true, "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.6" } }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, "license": "Apache-2.0" }, "node_modules/7zip-bin": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true, "license": "MIT" }, "node_modules/abbrev": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", "engines": { @@ -6508,8 +5542,6 @@ }, "node_modules/acorn": { "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -6521,8 +5553,6 @@ }, "node_modules/acorn-import-phases": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", "engines": { @@ -6534,8 +5564,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6544,15 +5572,11 @@ }, "node_modules/acorn-jsx-walk": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", - "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", "dev": true, "license": "MIT" }, "node_modules/acorn-loose": { "version": "8.5.2", - "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", - "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", "dev": true, "license": "MIT", "dependencies": { @@ -6564,8 +5588,6 @@ }, "node_modules/acorn-walk": { "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", "dependencies": { @@ -6577,8 +5599,6 @@ }, "node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -6587,8 +5607,6 @@ }, "node_modules/ajv": { "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6604,8 +5622,6 @@ }, "node_modules/ajv-formats": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { @@ -6621,9 +5637,7 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", "dev": true, "license": "MIT", "dependencies": { @@ -6639,15 +5653,11 @@ }, "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/ajv-keywords": { "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6656,8 +5666,6 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6672,8 +5680,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -6682,8 +5688,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -6697,9 +5701,7 @@ } }, "node_modules/ansis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", - "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "version": "4.3.0", "dev": true, "license": "ISC", "engines": { @@ -6708,15 +5710,11 @@ }, "node_modules/app-builder-bin": { "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", "dev": true, "license": "MIT" }, "node_modules/app-builder-lib": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", - "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -6767,8 +5765,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/fuses": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", "dependencies": { @@ -6782,8 +5778,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/fuses/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6796,10 +5790,16 @@ "node": ">=10" } }, + "node_modules/app-builder-lib/node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/app-builder-lib/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -6807,9 +5807,7 @@ } }, "node_modules/app-builder-lib/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", "dev": true, "license": "MIT", "dependencies": { @@ -6821,8 +5819,6 @@ }, "node_modules/app-builder-lib/node_modules/ci-info": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -6835,23 +5831,8 @@ "node": ">=8" } }, - "node_modules/app-builder-lib/node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/app-builder-lib/node_modules/isbinaryfile": { "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", "dev": true, "license": "MIT", "engines": { @@ -6863,31 +5844,14 @@ }, "node_modules/app-builder-lib/node_modules/isexe": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, - "node_modules/app-builder-lib/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/app-builder-lib/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -6902,8 +5866,6 @@ }, "node_modules/app-builder-lib/node_modules/pe-library": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", "dev": true, "license": "MIT", "engines": { @@ -6915,10 +5877,21 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/app-builder-lib/node_modules/plist": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/app-builder-lib/node_modules/resedit": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", "dev": true, "license": "MIT", "dependencies": { @@ -6933,10 +5906,19 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", "dependencies": { @@ -6949,23 +5931,12 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/app-builder-lib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/aria-hidden": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -6976,29 +5947,14 @@ }, "node_modules/aria-query": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -7007,8 +5963,6 @@ }, "node_modules/ast-kit": { "version": "3.0.0-beta.1", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-3.0.0-beta.1.tgz", - "integrity": "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==", "dev": true, "license": "MIT", "dependencies": { @@ -7024,59 +5978,49 @@ } }, "node_modules/ast-kit/node_modules/@babel/helper-string-parser": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.3.tgz", - "integrity": "sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/ast-kit/node_modules/@babel/helper-validator-identifier": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.3.tgz", - "integrity": "sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/ast-kit/node_modules/@babel/parser": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.3.tgz", - "integrity": "sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^8.0.0-rc.3" + "@babel/types": "^8.0.0-rc.6" }, "bin": { "parser": "bin/babel-parser.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/ast-kit/node_modules/@babel/types": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.3.tgz", - "integrity": "sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^8.0.0-rc.3", - "@babel/helper-validator-identifier": "^8.0.0-rc.3" + "@babel/helper-string-parser": "^8.0.0-rc.6", + "@babel/helper-validator-identifier": "^8.0.0-rc.6" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "version": "1.0.2", "dev": true, "license": "MIT", "dependencies": { @@ -7087,33 +6031,16 @@ }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, "node_modules/async-exit-hook": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true, "license": "MIT", "engines": { @@ -7122,15 +6049,11 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", "engines": { @@ -7139,8 +6062,6 @@ }, "node_modules/author-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", - "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", "dev": true, "license": "MIT", "engines": { @@ -7149,8 +6070,6 @@ }, "node_modules/bail": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "license": "MIT", "funding": { "type": "github", @@ -7159,15 +6078,11 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -7185,9 +6100,7 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", - "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "version": "2.10.32", "dev": true, "license": "Apache-2.0", "bin": { @@ -7199,15 +6112,11 @@ }, "node_modules/before-after-hook": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true, "license": "Apache-2.0" }, "node_modules/better-sqlite3": { "version": "12.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", - "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -7220,8 +6129,6 @@ }, "node_modules/bidi-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, "license": "MIT", "dependencies": { @@ -7230,8 +6137,6 @@ }, "node_modules/bindings": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", "dependencies": { "file-uri-to-path": "1.0.0" @@ -7239,8 +6144,6 @@ }, "node_modules/birpc": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", - "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", "dev": true, "license": "MIT", "funding": { @@ -7249,8 +6152,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -7260,31 +6161,22 @@ }, "node_modules/bluebird": { "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true, "license": "MIT" }, "node_modules/boolean": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, "license": "MIT", "optional": true }, "node_modules/bottleneck": { "version": "2.19.5", - "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", "dev": true, "license": "MIT", "dependencies": { @@ -7294,8 +6186,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -7307,8 +6197,6 @@ }, "node_modules/browserslist": { "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -7341,8 +6229,6 @@ }, "node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -7365,8 +6251,6 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", "engines": { @@ -7375,21 +6259,15 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, "node_modules/builder-util": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", - "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", "dev": true, "license": "MIT", "dependencies": { @@ -7413,8 +6291,6 @@ }, "node_modules/builder-util-runtime": { "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -7426,8 +6302,6 @@ }, "node_modules/cac": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", - "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", "dev": true, "license": "MIT", "engines": { @@ -7436,8 +6310,6 @@ }, "node_modules/cacheable-lookup": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, "license": "MIT", "engines": { @@ -7446,8 +6318,6 @@ }, "node_modules/cacheable-request": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "license": "MIT", "dependencies": { @@ -7465,8 +6335,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7478,9 +6346,7 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "version": "1.0.30001793", "dev": true, "funding": [ { @@ -7500,8 +6366,6 @@ }, "node_modules/ccount": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "license": "MIT", "funding": { "type": "github", @@ -7510,8 +6374,6 @@ }, "node_modules/chai": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -7520,8 +6382,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -7537,8 +6397,6 @@ }, "node_modules/chamber-copilot": { "version": "0.5.11", - "resolved": "https://registry.npmjs.org/chamber-copilot/-/chamber-copilot-0.5.11.tgz", - "integrity": "sha512-Bw/bnF4DWSmM44JNH9CB/tH4RrIKcx4hP87bs2TlGoju/8/Q9YtZDrwkonOg7TttE53328OSajCgLoWne8eroQ==", "license": "MIT", "dependencies": { "vscode-jsonrpc": "^8.2.1" @@ -7549,8 +6407,6 @@ }, "node_modules/character-entities": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "license": "MIT", "funding": { "type": "github", @@ -7559,8 +6415,6 @@ }, "node_modules/character-entities-html4": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "license": "MIT", "funding": { "type": "github", @@ -7569,8 +6423,6 @@ }, "node_modules/character-entities-legacy": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "license": "MIT", "funding": { "type": "github", @@ -7579,8 +6431,6 @@ }, "node_modules/character-reference-invalid": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", "license": "MIT", "funding": { "type": "github", @@ -7589,15 +6439,11 @@ }, "node_modules/chardet": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true, "license": "MIT" }, "node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7606,8 +6452,6 @@ }, "node_modules/chrome-trace-event": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", "engines": { @@ -7616,15 +6460,11 @@ }, "node_modules/chromium-pickle-js": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true, "license": "MIT" }, "node_modules/ci-info": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -7638,42 +6478,21 @@ } }, "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://polar.sh/cva" } }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "node_modules/cli-cursor": { + "version": "4.0.0", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" + "restore-cursor": "^4.0.0" }, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -7684,8 +6503,6 @@ }, "node_modules/cli-width": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, "license": "ISC", "engines": { @@ -7694,8 +6511,6 @@ }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -7707,42 +6522,8 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7759,8 +6540,6 @@ }, "node_modules/clone-response": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, "license": "MIT", "dependencies": { @@ -7772,8 +6551,6 @@ }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -7781,8 +6558,6 @@ }, "node_modules/cmdk": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", @@ -7797,8 +6572,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7810,22 +6583,16 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -7837,8 +6604,6 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "license": "MIT", "funding": { "type": "github", @@ -7847,8 +6612,6 @@ }, "node_modules/commander": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "dev": true, "license": "MIT", "engines": { @@ -7856,9 +6619,7 @@ } }, "node_modules/comment-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", - "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", + "version": "1.4.7", "dev": true, "license": "MIT", "engines": { @@ -7867,8 +6628,6 @@ }, "node_modules/compare-version": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true, "license": "MIT", "engines": { @@ -7877,41 +6636,16 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, "node_modules/croner": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/croner/-/croner-10.0.1.tgz", - "integrity": "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==", "funding": [ { "type": "other", @@ -7929,15 +6663,11 @@ }, "node_modules/cross-dirname": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -7951,8 +6681,6 @@ }, "node_modules/cross-zip": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cross-zip/-/cross-zip-4.0.1.tgz", - "integrity": "sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==", "dev": true, "funding": [ { @@ -7975,8 +6703,6 @@ }, "node_modules/css-tree": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { @@ -7989,15 +6715,11 @@ }, "node_modules/css.escape": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true, "license": "MIT" }, "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", "bin": { @@ -8009,14 +6731,10 @@ }, "node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/data-urls": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", "dependencies": { @@ -8029,8 +6747,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8046,15 +6762,11 @@ }, "node_modules/decimal.js": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, "license": "MIT" }, "node_modules/decode-named-character-reference": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -8066,8 +6778,6 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -8081,8 +6791,6 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", "engines": { "node": ">=10" @@ -8093,8 +6801,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -8102,15 +6808,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/defer-to-connect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, "license": "MIT", "engines": { @@ -8119,8 +6821,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "optional": true, @@ -8138,8 +6838,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "optional": true, @@ -8157,15 +6855,11 @@ }, "node_modules/defu": { "version": "6.1.7", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "dev": true, "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { @@ -8173,9 +6867,7 @@ } }, "node_modules/dependency-cruiser": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.3.10.tgz", - "integrity": "sha512-jF5WaIb+O+wLabXrQE7iBY2zYBEW8VlnuuL0+iZPvZHGhTaAYdLk31DI0zkwhcGE8CiHcDwGhMnn3PfOAYnVdQ==", + "version": "17.4.2", "dev": true, "license": "MIT", "dependencies": { @@ -8185,7 +6877,7 @@ "acorn-loose": "8.5.2", "acorn-walk": "8.3.5", "commander": "14.0.3", - "enhanced-resolve": "5.20.1", + "enhanced-resolve": "5.22.0", "ignore": "7.0.5", "interpret": "3.1.1", "is-installed-globally": "1.0.0", @@ -8194,7 +6886,7 @@ "prompts": "2.4.2", "rechoir": "0.8.0", "safe-regex": "2.1.1", - "semver": "7.7.4", + "semver": "7.8.1", "tsconfig-paths-webpack-plugin": "4.2.0", "watskeburt": "5.0.3" }, @@ -8212,38 +6904,19 @@ }, "node_modules/dependency-cruiser/node_modules/commander": { "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { "node": ">=20" } }, - "node_modules/dependency-cruiser/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/deprecation": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true, "license": "ISC" }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" @@ -8251,8 +6924,6 @@ }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -8260,22 +6931,16 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true, "license": "MIT", "optional": true }, "node_modules/detect-node-es": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -8287,8 +6952,6 @@ }, "node_modules/dir-compare": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8298,8 +6961,6 @@ }, "node_modules/dmg-builder": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", - "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", "dependencies": { @@ -8313,58 +6974,14 @@ "dmg-license": "^1.0.11" } }, - "node_modules/dmg-builder/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, "license": "MIT", "peer": true }, "node_modules/dotenv": { "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -8376,8 +6993,6 @@ }, "node_modules/dotenv-expand": { "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8392,8 +7007,6 @@ }, "node_modules/dts-resolver": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.3.tgz", - "integrity": "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==", "dev": true, "license": "MIT", "engines": { @@ -8413,8 +7026,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -8428,15 +7039,11 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -8444,8 +7051,6 @@ }, "node_modules/ejs": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8459,9 +7064,7 @@ } }, "node_modules/electron": { - "version": "41.3.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-41.3.0.tgz", - "integrity": "sha512-2Q5aeocmFdeheZGDUTrAvSR3t+n0c3d104AJWWEnt7syJU0tE4VdibMYaPtQ47QuXSoUf0/xSsfUUvu/uSXIfg==", + "version": "41.7.0", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8479,8 +7082,6 @@ }, "node_modules/electron-builder": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", - "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", "dev": true, "license": "MIT", "dependencies": { @@ -8505,8 +7106,6 @@ }, "node_modules/electron-builder-squirrel-windows": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", "dev": true, "license": "MIT", "peer": true, @@ -8516,246 +7115,74 @@ "electron-winstaller": "5.4.0" } }, - "node_modules/electron-installer-common": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.4.tgz", - "integrity": "sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@electron/asar": "^3.2.5", - "@malept/cross-spawn-promise": "^1.0.0", - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "glob": "^7.1.4", - "lodash": "^4.17.15", - "parse-author": "^2.0.0", - "semver": "^7.1.1", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "url": "https://github.com/electron-userland/electron-installer-common?sponsor=1" - }, - "optionalDependencies": { - "@types/fs-extra": "^9.0.1" - } - }, - "node_modules/electron-installer-common/node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/electron-installer-common/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-installer-debian": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/electron-installer-debian/-/electron-installer-debian-3.2.0.tgz", - "integrity": "sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin", - "linux" - ], - "dependencies": { - "@malept/cross-spawn-promise": "^1.0.0", - "debug": "^4.1.1", - "electron-installer-common": "^0.10.2", - "fs-extra": "^9.0.0", - "get-folder-size": "^2.0.1", - "lodash": "^4.17.4", - "word-wrap": "^1.2.3", - "yargs": "^16.0.2" - }, - "bin": { - "electron-installer-debian": "src/cli.js" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-installer-debian/node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/electron-installer-debian/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/electron-installer-debian/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/electron-installer-debian/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-installer-debian/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-installer-debian/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-installer-debian/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/electron-installer-common": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.4.tgz", + "integrity": "sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "@electron/asar": "^3.2.5", + "@malept/cross-spawn-promise": "^1.0.0", + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "glob": "^7.1.4", + "lodash": "^4.17.15", + "parse-author": "^2.0.0", + "semver": "^7.1.1", + "tmp-promise": "^3.0.2" }, "engines": { - "node": ">=10" + "node": ">= 10.0.0" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/electron-userland/electron-installer-common?sponsor=1" + }, + "optionalDependencies": { + "@types/fs-extra": "^9.0.1" } }, - "node_modules/electron-installer-debian/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/electron-installer-common/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", "dev": true, - "license": "MIT", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], "optional": true, "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "cross-spawn": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">= 10" } }, - "node_modules/electron-installer-debian/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "node_modules/electron-installer-common/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, - "license": "ISC", "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { "node": ">=10" } }, "node_modules/electron-publish": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", - "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", "dev": true, "license": "MIT", "dependencies": { @@ -8771,8 +7198,6 @@ }, "node_modules/electron-squirrel-startup": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.1.tgz", - "integrity": "sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==", "license": "Apache-2.0", "dependencies": { "debug": "^2.2.0" @@ -8780,8 +7205,6 @@ }, "node_modules/electron-squirrel-startup/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -8789,21 +7212,15 @@ }, "node_modules/electron-squirrel-startup/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.340", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", - "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "version": "1.5.361", "dev": true, "license": "ISC" }, "node_modules/electron-updater": { "version": "6.8.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", - "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", "license": "MIT", "dependencies": { "builder-util-runtime": "9.5.1", @@ -8816,10 +7233,18 @@ "tiny-typed-emitter": "^2.1.0" } }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.4", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/electron-winstaller": { "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8839,8 +7264,6 @@ }, "node_modules/electron-winstaller/node_modules/fs-extra": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", "dependencies": { @@ -8854,8 +7277,6 @@ }, "node_modules/electron-winstaller/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -8864,8 +7285,6 @@ }, "node_modules/electron-winstaller/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -8874,8 +7293,6 @@ }, "node_modules/electron/node_modules/@electron/get": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8895,9 +7312,7 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "version": "24.12.4", "dev": true, "license": "MIT", "dependencies": { @@ -8906,8 +7321,6 @@ }, "node_modules/electron/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { @@ -8921,8 +7334,6 @@ }, "node_modules/electron/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -8931,8 +7342,6 @@ }, "node_modules/electron/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -8941,15 +7350,11 @@ }, "node_modules/electron/node_modules/undici-types": { "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "node_modules/electron/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -8957,16 +7362,12 @@ } }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "8.0.0", "dev": true, "license": "MIT" }, "node_modules/emojibase": { "version": "17.0.0", - "resolved": "https://registry.npmjs.org/emojibase/-/emojibase-17.0.0.tgz", - "integrity": "sha512-bXdpf4HPY3p41zK5swVKZdC/VynsMZ4LoLxdYDE+GucqkFwzcM1GVc4ODfYAlwoKaf2U2oNNUoOO78N96ovpBA==", "license": "MIT", "peer": true, "engines": { @@ -8979,8 +7380,6 @@ }, "node_modules/emojibase-data": { "version": "17.0.0", - "resolved": "https://registry.npmjs.org/emojibase-data/-/emojibase-data-17.0.0.tgz", - "integrity": "sha512-Yvgb5AWoHViHV/gq1qr5ZAarcBip+B27/ZLRsUJkbgAEaLlZ/fof9g882LTpmEpyhBNEC0m2SEmItljHsTygjA==", "license": "MIT", "funding": { "type": "ko-fi", @@ -8991,9 +7390,7 @@ } }, "node_modules/empathic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "version": "2.0.1", "dev": true, "license": "MIT", "engines": { @@ -9002,35 +7399,29 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.22.0", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "version": "4.5.0", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=20.19.0" + "node": ">=0.12" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -9038,8 +7429,6 @@ }, "node_modules/env-paths": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", "engines": { @@ -9048,15 +7437,11 @@ }, "node_modules/err-code": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, "license": "MIT" }, "node_modules/error-ex": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9065,8 +7450,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -9075,8 +7458,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -9084,16 +7465,12 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.1.0", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", "dev": true, "license": "MIT", "dependencies": { @@ -9105,8 +7482,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -9121,16 +7496,12 @@ }, "node_modules/es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -9139,8 +7510,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -9151,16 +7520,14 @@ } }, "node_modules/eslint": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", - "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "version": "10.4.0", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", + "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", @@ -9208,8 +7575,6 @@ }, "node_modules/eslint-import-context": { "version": "0.1.9", - "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", - "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -9233,8 +7598,6 @@ }, "node_modules/eslint-import-resolver-typescript": { "version": "4.4.4", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", - "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", "dev": true, "license": "ISC", "dependencies": { @@ -9268,8 +7631,6 @@ }, "node_modules/eslint-plugin-import-x": { "version": "4.16.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.2.tgz", - "integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==", "dev": true, "license": "MIT", "dependencies": { @@ -9306,8 +7667,6 @@ }, "node_modules/eslint-plugin-import-x/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -9315,9 +7674,7 @@ } }, "node_modules/eslint-plugin-import-x/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", "dev": true, "license": "MIT", "dependencies": { @@ -9329,8 +7686,6 @@ }, "node_modules/eslint-plugin-import-x/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -9345,8 +7700,6 @@ }, "node_modules/eslint-scope": { "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9363,13 +7716,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "5.0.1", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -9377,8 +7728,6 @@ }, "node_modules/eslint/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -9386,9 +7735,7 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", "dev": true, "license": "MIT", "dependencies": { @@ -9398,23 +7745,8 @@ "node": "18 || 20 || >=22" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -9423,8 +7755,6 @@ }, "node_modules/eslint/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -9439,8 +7769,6 @@ }, "node_modules/espree": { "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9455,23 +7783,8 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9483,8 +7796,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9496,8 +7807,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -9506,8 +7815,6 @@ }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "license": "MIT", "funding": { "type": "opencollective", @@ -9516,8 +7823,6 @@ }, "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -9526,25 +7831,30 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/eta": { + "version": "3.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, "node_modules/events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", "engines": { @@ -9553,8 +7863,6 @@ }, "node_modules/execa": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "license": "MIT", "dependencies": { @@ -9572,8 +7880,6 @@ }, "node_modules/execa/node_modules/cross-spawn": { "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "license": "MIT", "dependencies": { @@ -9589,8 +7895,6 @@ }, "node_modules/execa/node_modules/get-stream": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, "license": "MIT", "dependencies": { @@ -9602,8 +7906,6 @@ }, "node_modules/execa/node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, "license": "MIT", "engines": { @@ -9612,8 +7914,6 @@ }, "node_modules/execa/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -9622,8 +7922,6 @@ }, "node_modules/execa/node_modules/shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, "license": "MIT", "dependencies": { @@ -9635,8 +7933,6 @@ }, "node_modules/execa/node_modules/shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true, "license": "MIT", "engines": { @@ -9645,15 +7941,11 @@ }, "node_modules/execa/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/execa/node_modules/which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "license": "ISC", "dependencies": { @@ -9665,8 +7957,6 @@ }, "node_modules/expand-template": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" @@ -9674,8 +7964,6 @@ }, "node_modules/expect-type": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9684,21 +7972,15 @@ }, "node_modules/exponential-backoff": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "dev": true, "license": "Apache-2.0" }, "node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, "node_modules/external-editor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, "license": "MIT", "dependencies": { @@ -9710,10 +7992,19 @@ "node": ">=4" } }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9731,28 +8022,13 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -9768,8 +8044,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -9781,22 +8055,16 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", "dev": true, "funding": [ { @@ -9812,18 +8080,30 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9835,14 +8115,10 @@ }, "node_modules/file-uri-to-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, "node_modules/filelist": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -9850,9 +8126,7 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", "dev": true, "license": "MIT", "dependencies": { @@ -9861,8 +8135,6 @@ }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -9874,8 +8146,6 @@ }, "node_modules/filename-reserved-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true, "license": "MIT", "engines": { @@ -9884,8 +8154,6 @@ }, "node_modules/filenamify": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", - "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", "dev": true, "license": "MIT", "dependencies": { @@ -9902,8 +8170,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -9915,8 +8181,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -9932,8 +8196,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -9946,15 +8208,11 @@ }, "node_modules/flatted": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/flora-colossus": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz", - "integrity": "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==", "dev": true, "license": "MIT", "dependencies": { @@ -9967,8 +8225,6 @@ }, "node_modules/form-data": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -9984,8 +8240,6 @@ }, "node_modules/frimousse": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/frimousse/-/frimousse-0.3.0.tgz", - "integrity": "sha512-kO6LMoKY/cLAYEhXXtqLRaLIE6L/DagpFPrUZaLv3LsUa1/8Iza3HhwZcgN8eZ+weXnhv69eoclNUPohcCa/IQ==", "license": "MIT", "workspaces": [ ".", @@ -10003,14 +8257,10 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, "node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -10023,30 +8273,11 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -10055,8 +8286,6 @@ }, "node_modules/galactus": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", - "integrity": "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10074,13 +8303,10 @@ "integrity": "sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, - "license": "MIT", "optional": true }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -10089,8 +8315,6 @@ }, "node_modules/get-east-asian-width": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -10105,7 +8329,6 @@ "resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-2.0.1.tgz", "integrity": "sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "gar": "^1.0.4", @@ -10117,8 +8340,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10142,8 +8363,6 @@ }, "node_modules/get-nonce": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", "engines": { "node": ">=6" @@ -10151,8 +8370,6 @@ }, "node_modules/get-package-info": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", - "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -10167,8 +8384,6 @@ }, "node_modules/get-package-info/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -10177,15 +8392,11 @@ }, "node_modules/get-package-info/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -10198,8 +8409,6 @@ }, "node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { @@ -10214,8 +8423,6 @@ }, "node_modules/get-tsconfig": { "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -10227,15 +8434,10 @@ }, "node_modules/github-from-package": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -10255,8 +8457,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -10268,15 +8468,11 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/global-agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -10294,8 +8490,6 @@ }, "node_modules/global-directory": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10310,8 +8504,6 @@ }, "node_modules/global-directory/node_modules/ini": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "dev": true, "license": "ISC", "engines": { @@ -10320,8 +8512,6 @@ }, "node_modules/global-dirs": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", "dev": true, "license": "MIT", "dependencies": { @@ -10335,9 +8525,7 @@ } }, "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "version": "17.6.0", "dev": true, "license": "MIT", "engines": { @@ -10349,8 +8537,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "optional": true, @@ -10367,8 +8553,6 @@ }, "node_modules/globby": { "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", - "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10388,8 +8572,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -10401,8 +8583,6 @@ }, "node_modules/got": { "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, "license": "MIT", "dependencies": { @@ -10427,14 +8607,10 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -10443,8 +8619,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "optional": true, @@ -10457,8 +8631,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -10470,8 +8642,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -10486,8 +8656,6 @@ }, "node_modules/hasown": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -10499,8 +8667,6 @@ }, "node_modules/hast-util-is-element": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "dev": true, "license": "MIT", "dependencies": { @@ -10513,8 +8679,6 @@ }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -10538,10 +8702,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/hast-util-to-text": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", "dev": true, "license": "MIT", "dependencies": { @@ -10555,10 +8721,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-text/node_modules/@types/unist": { + "version": "3.0.3", + "dev": true, + "license": "MIT" + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -10570,17 +8739,13 @@ }, "node_modules/highlight.js": { "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" } }, "node_modules/hono": { - "version": "4.12.15", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", - "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", + "version": "4.12.23", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -10588,200 +8753,91 @@ }, "node_modules/hookable": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", - "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", "dev": true, "license": "MIT" }, "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true, - "license": "ISC" - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "version": "4.1.0", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "ISC", "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" + "lru-cache": "^6.0.0" }, "engines": { - "node": "^8.11.2 || >=10" + "node": ">=10" } }, - "node_modules/iconv-corefoundation/node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/iconv-corefoundation/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/html-escaper": { + "version": "2.0.2", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, - "node_modules/iconv-corefoundation/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "node_modules/html-url-attributes": { + "version": "3.0.1", "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/iconv-corefoundation/node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", "dev": true, "license": "MIT", - "optional": true + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } }, - "node_modules/iconv-corefoundation/node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "node_modules/http2-wrapper": { + "version": "1.0.3", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=10.19.0" } }, - "node_modules/iconv-corefoundation/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=8" + "node": ">= 14" } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -10789,8 +8845,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -10809,8 +8863,6 @@ }, "node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -10819,8 +8871,6 @@ }, "node_modules/import-without-cache": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.3.3.tgz", - "integrity": "sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ==", "dev": true, "license": "MIT", "engines": { @@ -10832,8 +8882,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -10842,8 +8890,6 @@ }, "node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -10852,9 +8898,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", "dependencies": { @@ -10864,14 +8907,10 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true, "license": "ISC", "engines": { @@ -10880,14 +8919,10 @@ }, "node_modules/inline-style-parser": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, "node_modules/interpret": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, "license": "MIT", "engines": { @@ -10896,8 +8931,6 @@ }, "node_modules/is-alphabetical": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "license": "MIT", "funding": { "type": "github", @@ -10906,8 +8939,6 @@ }, "node_modules/is-alphanumerical": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -10920,15 +8951,11 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, "node_modules/is-bun-module": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10936,13 +8963,11 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -10953,8 +8978,6 @@ }, "node_modules/is-decimal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", "license": "MIT", "funding": { "type": "github", @@ -10963,8 +8986,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -10972,22 +8993,15 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "3.0.0", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -10999,8 +9013,6 @@ }, "node_modules/is-hexadecimal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "license": "MIT", "funding": { "type": "github", @@ -11009,8 +9021,6 @@ }, "node_modules/is-installed-globally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11026,8 +9036,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -11036,8 +9044,6 @@ }, "node_modules/is-path-inside": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "dev": true, "license": "MIT", "engines": { @@ -11049,8 +9055,6 @@ }, "node_modules/is-plain-obj": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { "node": ">=12" @@ -11061,15 +9065,11 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, "license": "MIT" }, "node_modules/is-stream": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, "license": "MIT", "engines": { @@ -11078,8 +9078,6 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, "license": "MIT", "engines": { @@ -11091,8 +9089,6 @@ }, "node_modules/isbinaryfile": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "license": "MIT", "engines": { @@ -11104,15 +9100,11 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -11121,8 +9113,6 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11136,8 +9126,6 @@ }, "node_modules/istanbul-reports": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11150,8 +9138,6 @@ }, "node_modules/jake": { "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -11168,8 +9154,6 @@ }, "node_modules/jest-worker": { "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", "dependencies": { @@ -11183,8 +9167,6 @@ }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -11198,9 +9180,7 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", "dev": true, "license": "MIT", "bin": { @@ -11209,16 +9189,12 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT", "peer": true }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -11228,28 +9204,26 @@ } }, "node_modules/jsdom": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", - "integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==", + "version": "29.1.1", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.1.5", - "@asamuzakjp/dom-selector": "^7.0.6", + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.7", - "parse5": "^8.0.0", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", - "undici": "^7.24.5", + "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", @@ -11268,10 +9242,16 @@ } } }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -11283,37 +9263,27 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, "license": "ISC", "optional": true }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -11325,15 +9295,11 @@ }, "node_modules/jsonc-parser": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true, "license": "MIT" }, "node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -11344,8 +9310,6 @@ }, "node_modules/jsonpointer": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, "license": "MIT", "engines": { @@ -11354,8 +9318,6 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { "jws": "^4.0.1", @@ -11376,8 +9338,6 @@ }, "node_modules/junk": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", - "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", "dev": true, "license": "MIT", "engines": { @@ -11386,347 +9346,119 @@ }, "node_modules/jwa": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", - "dev": true, - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/keytar": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", - "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "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" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], + "node_modules/jws": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/katex": { + "version": "0.16.47", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "bin": { + "katex": "cli.js" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 12" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], + "node_modules/keytar": { + "version": "7.9.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keytar/node_modules/node-addon-api": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], + "node_modules/kleur": { + "version": "3.0.3", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=6" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], + "node_modules/lazy-val": { + "version": "1.0.5", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/lightningcss-win32-arm64-msvc": { + "node_modules/lightningcss": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], "dev": true, "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { "node": ">= 12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + }, + "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" } }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -11745,10 +9477,18 @@ } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -11756,8 +9496,6 @@ }, "node_modules/listr2": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", - "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", "dev": true, "license": "MIT", "dependencies": { @@ -11774,8 +9512,6 @@ }, "node_modules/listr2/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -11787,8 +9523,6 @@ }, "node_modules/listr2/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -11798,10 +9532,70 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/listr2/node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { @@ -11816,8 +9610,6 @@ }, "node_modules/listr2/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11834,8 +9626,6 @@ }, "node_modules/load-json-file": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11849,9 +9639,7 @@ } }, "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "version": "4.3.2", "dev": true, "license": "MIT", "engines": { @@ -11864,8 +9652,6 @@ }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -11880,78 +9666,52 @@ }, "node_modules/lodash": { "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "dev": true, "license": "MIT" }, "node_modules/lodash.includes": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "license": "MIT", "dependencies": { @@ -11967,8 +9727,6 @@ }, "node_modules/log-update": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", - "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", "dev": true, "license": "MIT", "dependencies": { @@ -11982,18 +9740,54 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", - "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "node_modules/log-update/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^1.0.2" - }, "engines": { "node": ">=12" }, @@ -12001,36 +9795,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/log-update/node_modules/slice-ansi": { + "version": "5.0.0", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/log-update/node_modules/string-width": { + "version": "5.1.2", "dev": true, "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update/node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { @@ -12045,8 +9842,6 @@ }, "node_modules/log-update/node_modules/type-fest": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -12058,8 +9853,6 @@ }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12076,8 +9869,6 @@ }, "node_modules/longest-streak": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "license": "MIT", "funding": { "type": "github", @@ -12086,8 +9877,6 @@ }, "node_modules/lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, "license": "MIT", "engines": { @@ -12096,8 +9885,6 @@ }, "node_modules/lowlight": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", - "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12111,19 +9898,18 @@ } }, "node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "6.0.0", "dev": true, - "license": "BlueOak-1.0.0", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": "20 || >=22" + "node": ">=10" } }, "node_modules/lucide-react": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.9.0.tgz", - "integrity": "sha512-6qVAmbgCjcJz7sAGSPSSJ++RAwjlK2XCbRrZKv63Ciko1KT8jX0//CXxgI3jg2HlJu8tADqdYlNDebmYjeoruA==", + "version": "1.16.0", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -12131,8 +9917,6 @@ }, "node_modules/lz-string": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", "peer": true, @@ -12142,8 +9926,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12151,21 +9933,17 @@ } }, "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "version": "0.5.3", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", + "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -12180,8 +9958,6 @@ }, "node_modules/map-age-cleaner": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", "dev": true, "license": "MIT", "dependencies": { @@ -12193,8 +9969,6 @@ }, "node_modules/markdown-it": { "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "license": "MIT", "dependencies": { @@ -12209,23 +9983,8 @@ "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/markdown-table": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", "license": "MIT", "funding": { "type": "github", @@ -12234,8 +9993,6 @@ }, "node_modules/markdownlint": { "version": "0.40.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.40.0.tgz", - "integrity": "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==", "dev": true, "license": "MIT", "dependencies": { @@ -12258,8 +10015,6 @@ }, "node_modules/markdownlint-cli2": { "version": "0.22.1", - "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.22.1.tgz", - "integrity": "sha512-X14ZbytybDCXAViDmtN4DKLt9ZTrRn+oOrxTYlg3a65jS6QcYYbAkGPh/En2L/GDNbFYJ6lKaQSUNrrbN1bPrw==", "dev": true, "license": "MIT", "dependencies": { @@ -12285,8 +10040,6 @@ }, "node_modules/markdownlint-cli2-formatter-default": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", - "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", "dev": true, "license": "MIT", "funding": { @@ -12298,8 +10051,6 @@ }, "node_modules/markdownlint/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -12311,8 +10062,6 @@ }, "node_modules/markdownlint/node_modules/string-width": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", "dev": true, "license": "MIT", "dependencies": { @@ -12328,8 +10077,6 @@ }, "node_modules/markdownlint/node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { @@ -12344,8 +10091,6 @@ }, "node_modules/matcher": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "dev": true, "license": "MIT", "optional": true, @@ -12358,8 +10103,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -12368,8 +10111,6 @@ }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12384,8 +10125,6 @@ }, "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { "node": ">=12" @@ -12396,8 +10135,6 @@ }, "node_modules/mdast-util-from-markdown": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12418,10 +10155,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-from-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/mdast-util-gfm": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", @@ -12439,8 +10178,6 @@ }, "node_modules/mdast-util-gfm-autolink-literal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12456,8 +10193,6 @@ }, "node_modules/mdast-util-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12473,8 +10208,6 @@ }, "node_modules/mdast-util-gfm-strikethrough": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12488,8 +10221,6 @@ }, "node_modules/mdast-util-gfm-table": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12505,8 +10236,6 @@ }, "node_modules/mdast-util-gfm-task-list-item": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12521,8 +10250,6 @@ }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -12539,8 +10266,6 @@ }, "node_modules/mdast-util-mdx-jsx": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -12561,10 +10286,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/mdast-util-mdxjs-esm": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -12581,8 +10308,6 @@ }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12595,8 +10320,6 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -12616,8 +10339,6 @@ }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12635,10 +10356,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/mdast-util-to-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" @@ -12650,22 +10373,16 @@ }, "node_modules/mdn-data": { "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0" }, "node_modules/mdurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true, "license": "MIT" }, "node_modules/mem": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "dev": true, "license": "MIT", "dependencies": { @@ -12679,15 +10396,11 @@ }, "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -12696,8 +10409,6 @@ }, "node_modules/micromark": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -12731,8 +10442,6 @@ }, "node_modules/micromark-core-commonmark": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -12765,8 +10474,6 @@ }, "node_modules/micromark-extension-directive": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", - "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", "dev": true, "license": "MIT", "dependencies": { @@ -12785,8 +10492,6 @@ }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", "license": "MIT", "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", @@ -12805,8 +10510,6 @@ }, "node_modules/micromark-extension-gfm-autolink-literal": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", @@ -12821,8 +10524,6 @@ }, "node_modules/micromark-extension-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -12841,8 +10542,6 @@ }, "node_modules/micromark-extension-gfm-strikethrough": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -12859,8 +10558,6 @@ }, "node_modules/micromark-extension-gfm-table": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -12876,8 +10573,6 @@ }, "node_modules/micromark-extension-gfm-tagfilter": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" @@ -12889,8 +10584,6 @@ }, "node_modules/micromark-extension-gfm-task-list-item": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -12906,8 +10599,6 @@ }, "node_modules/micromark-extension-math": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", "dev": true, "license": "MIT", "dependencies": { @@ -12926,8 +10617,6 @@ }, "node_modules/micromark-factory-destination": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", @@ -12947,8 +10636,6 @@ }, "node_modules/micromark-factory-label": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", @@ -12969,8 +10656,6 @@ }, "node_modules/micromark-factory-space": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -12989,8 +10674,6 @@ }, "node_modules/micromark-factory-title": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", @@ -13011,8 +10694,6 @@ }, "node_modules/micromark-factory-whitespace": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -13033,8 +10714,6 @@ }, "node_modules/micromark-util-character": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13053,8 +10732,6 @@ }, "node_modules/micromark-util-chunked": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", @@ -13072,8 +10749,6 @@ }, "node_modules/micromark-util-classify-character": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13093,8 +10768,6 @@ }, "node_modules/micromark-util-combine-extensions": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -13113,8 +10786,6 @@ }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -13132,8 +10803,6 @@ }, "node_modules/micromark-util-decode-string": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -13154,8 +10823,6 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -13170,8 +10837,6 @@ }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -13186,8 +10851,6 @@ }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13205,8 +10868,6 @@ }, "node_modules/micromark-util-resolve-all": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -13224,8 +10885,6 @@ }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -13245,8 +10904,6 @@ }, "node_modules/micromark-util-subtokenize": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -13267,8 +10924,6 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13283,8 +10938,6 @@ }, "node_modules/micromark-util-types": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -13299,8 +10952,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -13311,10 +10962,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", "bin": { @@ -13326,8 +10986,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { @@ -13336,8 +10994,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { @@ -13349,8 +11005,6 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "license": "MIT", "engines": { @@ -13359,8 +11013,6 @@ }, "node_modules/mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, "license": "MIT", "engines": { @@ -13369,8 +11021,6 @@ }, "node_modules/min-indent": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, "license": "MIT", "engines": { @@ -13379,8 +11029,6 @@ }, "node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -13392,8 +11040,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13401,8 +11047,6 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -13411,8 +11055,6 @@ }, "node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -13422,22 +11064,27 @@ "node": ">= 18" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/mute-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, "license": "ISC", "engines": { @@ -13445,9 +11092,7 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", "dev": true, "funding": [ { @@ -13465,14 +11110,10 @@ }, "node_modules/napi-build-utils": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, "node_modules/napi-postinstall": { "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, "license": "MIT", "bin": { @@ -13487,47 +11128,32 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/neo-async": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, "license": "MIT" }, "node_modules/nice-try": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true, "license": "MIT" }, "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "version": "4.31.0", + "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "semver": "^7.6.3" }, "engines": { - "node": ">=10" + "node": ">=22.12.0" } }, - "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "license": "MIT" - }, "node_modules/node-api-version": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13536,8 +11162,6 @@ }, "node_modules/node-fetch": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", "dependencies": { @@ -13557,22 +11181,16 @@ }, "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, "license": "MIT" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", "dependencies": { @@ -13582,8 +11200,6 @@ }, "node_modules/node-gyp": { "version": "12.3.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", - "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -13607,8 +11223,6 @@ }, "node_modules/node-gyp/node_modules/isexe": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -13616,9 +11230,7 @@ } }, "node_modules/node-gyp/node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "version": "6.26.0", "dev": true, "license": "MIT", "engines": { @@ -13627,8 +11239,6 @@ }, "node_modules/node-gyp/node_modules/which": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { @@ -13642,16 +11252,15 @@ } }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "version": "2.0.46", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nopt": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { @@ -13666,8 +11275,6 @@ }, "node_modules/normalize-package-data": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -13677,10 +11284,13 @@ "validate-npm-package-license": "^3.0.1" } }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -13689,8 +11299,6 @@ }, "node_modules/normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, "license": "MIT", "engines": { @@ -13702,8 +11310,6 @@ }, "node_modules/npm-run-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "dev": true, "license": "MIT", "dependencies": { @@ -13715,8 +11321,6 @@ }, "node_modules/npm-run-path/node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, "license": "MIT", "engines": { @@ -13725,8 +11329,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "optional": true, @@ -13736,8 +11338,6 @@ }, "node_modules/obug": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -13747,8 +11347,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -13756,8 +11354,6 @@ }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { @@ -13772,8 +11368,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -13790,8 +11384,6 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, "license": "MIT", "engines": { @@ -13800,8 +11392,6 @@ }, "node_modules/p-defer": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", "dev": true, "license": "MIT", "engines": { @@ -13810,8 +11400,6 @@ }, "node_modules/p-finally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true, "license": "MIT", "engines": { @@ -13820,8 +11408,6 @@ }, "node_modules/p-is-promise": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", "dev": true, "license": "MIT", "engines": { @@ -13830,8 +11416,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13846,8 +11430,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -13862,8 +11444,6 @@ }, "node_modules/p-try": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "dev": true, "license": "MIT", "engines": { @@ -13872,8 +11452,6 @@ }, "node_modules/parse-author": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", - "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", "dev": true, "license": "MIT", "dependencies": { @@ -13885,8 +11463,6 @@ }, "node_modules/parse-entities": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -13902,16 +11478,8 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, "node_modules/parse-json": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13923,8 +11491,6 @@ }, "node_modules/parse5": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "license": "MIT", "dependencies": { @@ -13934,10 +11500,19 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -13946,8 +11521,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { @@ -13956,8 +11529,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -13966,15 +11537,11 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13986,15 +11553,11 @@ }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pe-library": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-1.0.1.tgz", - "integrity": "sha512-nh39Mo1eGWmZS7y+mK/dQIqg7S1lp38DpRxkyoHf0ZcUs/HDc+yyTjuOtTvSMZHmfSLuSQaX945u05Y2Q6UWZg==", "dev": true, "license": "MIT", "engines": { @@ -14010,24 +11573,19 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.4", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -14035,8 +11593,6 @@ }, "node_modules/pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "license": "MIT", "engines": { @@ -14044,13 +11600,11 @@ } }, "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "version": "1.60.0", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.59.1" + "playwright-core": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -14063,9 +11617,7 @@ } }, "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "version": "1.60.0", "dev": true, "license": "Apache-2.0", "bin": { @@ -14075,29 +11627,12 @@ "node": ">=18" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "version": "3.1.1", "dev": true, "license": "MIT", "dependencies": { - "@xmldom/xmldom": "^0.8.8", + "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, @@ -14106,9 +11641,7 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.15", "dev": true, "funding": [ { @@ -14126,7 +11659,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -14136,8 +11669,6 @@ }, "node_modules/postcss-selector-parser": { "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", "dev": true, "license": "MIT", "dependencies": { @@ -14150,8 +11681,6 @@ }, "node_modules/postject": { "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", "dev": true, "license": "MIT", "dependencies": { @@ -14166,8 +11695,6 @@ }, "node_modules/postject/node_modules/commander": { "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", "engines": { @@ -14176,9 +11703,6 @@ }, "node_modules/prebuild-install": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -14201,10 +11725,18 @@ "node": ">=10" } }, + "node_modules/prebuild-install/node_modules/node-abi": { + "version": "3.92.0", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -14213,8 +11745,6 @@ }, "node_modules/prettier": { "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -14229,8 +11759,6 @@ }, "node_modules/pretty-format": { "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "peer": true, @@ -14245,8 +11773,6 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "peer": true, @@ -14259,8 +11785,6 @@ }, "node_modules/proc-log": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -14269,8 +11793,6 @@ }, "node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "license": "MIT", "engines": { @@ -14279,8 +11801,6 @@ }, "node_modules/promise-retry": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, "license": "MIT", "dependencies": { @@ -14293,8 +11813,6 @@ }, "node_modules/prompts": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -14307,8 +11825,6 @@ }, "node_modules/proper-lockfile": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "dev": true, "license": "MIT", "dependencies": { @@ -14319,15 +11835,11 @@ }, "node_modules/proper-lockfile/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/property-information": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", @@ -14336,8 +11848,6 @@ }, "node_modules/pump": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -14346,8 +11856,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -14356,8 +11864,6 @@ }, "node_modules/punycode.js": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dev": true, "license": "MIT", "engines": { @@ -14366,8 +11872,6 @@ }, "node_modules/quansync": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", - "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", "dev": true, "funding": [ { @@ -14383,8 +11887,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -14404,8 +11906,6 @@ }, "node_modules/quick-lru": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, "license": "MIT", "engines": { @@ -14417,8 +11917,6 @@ }, "node_modules/radix-ui": { "version": "1.4.3", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", - "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -14492,10 +11990,29 @@ } } }, + "node_modules/radix-ui/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -14509,52 +12026,33 @@ }, "node_modules/rc/node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "version": "19.2.6", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "version": "19.2.6", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.5" + "react": "^19.2.6" } }, "node_modules/react-is": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT", "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", - "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -14580,8 +12078,6 @@ }, "node_modules/react-remove-scroll": { "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -14605,8 +12101,6 @@ }, "node_modules/react-remove-scroll-bar": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", @@ -14627,8 +12121,6 @@ }, "node_modules/react-style-singleton": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", @@ -14649,8 +12141,6 @@ }, "node_modules/read-binary-file-arch": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", "dev": true, "license": "MIT", "dependencies": { @@ -14662,8 +12152,6 @@ }, "node_modules/read-pkg": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", "dev": true, "license": "MIT", "dependencies": { @@ -14677,8 +12165,6 @@ }, "node_modules/read-pkg-up": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", "dev": true, "license": "MIT", "dependencies": { @@ -14691,8 +12177,6 @@ }, "node_modules/read-pkg-up/node_modules/find-up": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14704,8 +12188,6 @@ }, "node_modules/read-pkg-up/node_modules/locate-path": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dev": true, "license": "MIT", "dependencies": { @@ -14718,8 +12200,6 @@ }, "node_modules/read-pkg-up/node_modules/p-limit": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -14731,8 +12211,6 @@ }, "node_modules/read-pkg-up/node_modules/p-locate": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dev": true, "license": "MIT", "dependencies": { @@ -14744,8 +12222,6 @@ }, "node_modules/read-pkg-up/node_modules/path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, "license": "MIT", "engines": { @@ -14754,8 +12230,6 @@ }, "node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -14768,8 +12242,6 @@ }, "node_modules/rechoir": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14781,8 +12253,6 @@ }, "node_modules/redent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, "license": "MIT", "dependencies": { @@ -14795,8 +12265,6 @@ }, "node_modules/regexp-tree": { "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", "dev": true, "license": "MIT", "bin": { @@ -14805,8 +12273,6 @@ }, "node_modules/rehype-highlight": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", - "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", "dev": true, "license": "MIT", "dependencies": { @@ -14823,8 +12289,6 @@ }, "node_modules/remark-gfm": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -14841,8 +12305,6 @@ }, "node_modules/remark-parse": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -14857,8 +12319,6 @@ }, "node_modules/remark-rehype": { "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -14874,8 +12334,6 @@ }, "node_modules/remark-stringify": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -14889,8 +12347,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -14899,8 +12355,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { @@ -14909,8 +12363,6 @@ }, "node_modules/resedit": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-2.0.3.tgz", - "integrity": "sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==", "dev": true, "license": "MIT", "dependencies": { @@ -14927,8 +12379,6 @@ }, "node_modules/resolve": { "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { @@ -14949,15 +12399,11 @@ }, "node_modules/resolve-alpn": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true, "license": "MIT" }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", "funding": { @@ -14966,8 +12412,6 @@ }, "node_modules/responselike": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, "license": "MIT", "dependencies": { @@ -14979,8 +12423,6 @@ }, "node_modules/restore-cursor": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "dev": true, "license": "MIT", "dependencies": { @@ -14996,15 +12438,11 @@ }, "node_modules/restore-cursor/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", "engines": { @@ -15013,8 +12451,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -15024,15 +12460,22 @@ }, "node_modules/rfdc": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true, "license": "MIT" }, + "node_modules/rimraf": { + "version": "2.6.3", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/roarr": { "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -15050,8 +12493,6 @@ }, "node_modules/rolldown": { "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "dev": true, "license": "MIT", "dependencies": { @@ -15084,8 +12525,6 @@ }, "node_modules/rolldown-plugin-dts": { "version": "0.23.2", - "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.23.2.tgz", - "integrity": "sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15129,19 +12568,15 @@ } }, "node_modules/rolldown-plugin-dts/node_modules/@babel/helper-string-parser": { - "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.3.tgz", - "integrity": "sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==", + "version": "8.0.0-rc.6", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^22.18.0 || >=24.11.0" } }, "node_modules/rolldown-plugin-dts/node_modules/@babel/helper-validator-identifier": { "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.3.tgz", - "integrity": "sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==", "dev": true, "license": "MIT", "engines": { @@ -15150,8 +12585,6 @@ }, "node_modules/rolldown-plugin-dts/node_modules/@babel/parser": { "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.3.tgz", - "integrity": "sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15166,8 +12599,6 @@ }, "node_modules/rolldown-plugin-dts/node_modules/@babel/types": { "version": "8.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.3.tgz", - "integrity": "sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -15178,23 +12609,8 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/rolldown-plugin-dts/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -15217,8 +12633,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -15237,8 +12651,6 @@ }, "node_modules/safe-regex": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", - "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", "dev": true, "license": "MIT", "dependencies": { @@ -15247,15 +12659,11 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", "dev": true, "license": "WTFPL OR ISC", "dependencies": { @@ -15264,8 +12672,6 @@ }, "node_modules/sax": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -15273,8 +12679,6 @@ }, "node_modules/saxes": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { @@ -15286,14 +12690,10 @@ }, "node_modules/scheduler": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/schema-utils": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { @@ -15311,9 +12711,7 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", "dev": true, "license": "MIT", "dependencies": { @@ -15329,8 +12727,6 @@ }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", "dependencies": { @@ -15342,15 +12738,11 @@ }, "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -15361,16 +12753,12 @@ }, "node_modules/semver-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, "license": "MIT", "optional": true }, "node_modules/serialize-error": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dev": true, "license": "MIT", "optional": true, @@ -15386,8 +12774,6 @@ }, "node_modules/serialize-error/node_modules/type-fest": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, @@ -15400,8 +12786,6 @@ }, "node_modules/sharp": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -15444,8 +12828,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -15457,8 +12839,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -15467,15 +12847,11 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { @@ -15487,8 +12863,6 @@ }, "node_modules/simple-concat": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "funding": [ { "type": "github", @@ -15507,8 +12881,6 @@ }, "node_modules/simple-get": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "funding": [ { "type": "github", @@ -15532,8 +12904,6 @@ }, "node_modules/simple-update-notifier": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "license": "MIT", "dependencies": { @@ -15545,15 +12915,11 @@ }, "node_modules/sisteransi": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true, "license": "MIT" }, "node_modules/slash": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "license": "MIT", "engines": { @@ -15563,52 +12929,8 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/smol-toml": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -15620,8 +12942,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -15630,8 +12950,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -15640,8 +12958,6 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { @@ -15651,8 +12967,6 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "license": "MIT", "funding": { "type": "github", @@ -15661,8 +12975,6 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -15672,15 +12984,11 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -15690,23 +12998,17 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/sprintf-js": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, "license": "BSD-3-Clause", "optional": true }, "node_modules/stable-hash-x": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", - "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", "dev": true, "license": "MIT", "engines": { @@ -15715,15 +13017,11 @@ }, "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/stat-mode": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true, "license": "MIT", "engines": { @@ -15732,71 +13030,31 @@ }, "node_modules/std-env": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, "node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "version": "4.2.3", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/stringify-entities": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", @@ -15809,8 +13067,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -15822,8 +13078,6 @@ }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { @@ -15832,8 +13086,6 @@ }, "node_modules/strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", "dev": true, "license": "MIT", "engines": { @@ -15842,8 +13094,6 @@ }, "node_modules/strip-indent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15853,10 +13103,15 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-outer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", "dev": true, "license": "MIT", "dependencies": { @@ -15868,8 +13123,6 @@ }, "node_modules/strip-outer/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -15878,8 +13131,6 @@ }, "node_modules/style-to-js": { "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", "license": "MIT", "dependencies": { "style-to-object": "1.0.14" @@ -15887,8 +13138,6 @@ }, "node_modules/style-to-object": { "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", "dependencies": { "inline-style-parser": "0.2.7" @@ -15896,8 +13145,6 @@ }, "node_modules/sumchecker": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -15909,8 +13156,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -15922,8 +13167,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -15935,15 +13178,11 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, "license": "MIT" }, "node_modules/tailwind-merge": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", - "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "version": "3.6.0", "license": "MIT", "funding": { "type": "github", @@ -15951,16 +13190,12 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", - "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "version": "4.3.0", "dev": true, "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", "dev": true, "license": "MIT", "engines": { @@ -15972,9 +13207,7 @@ } }, "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "version": "7.5.15", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -15990,8 +13223,6 @@ }, "node_modules/tar-fs": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -16002,14 +13233,10 @@ }, "node_modules/tar-fs/node_modules/chownr": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, "node_modules/tar-stream": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -16022,10 +13249,16 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/temp": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", "dependencies": { @@ -16038,8 +13271,6 @@ }, "node_modules/temp-file": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", "dev": true, "license": "MIT", "dependencies": { @@ -16047,37 +13278,8 @@ "fs-extra": "^10.0.0" } }, - "node_modules/temp/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/temp/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/terser": { - "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "version": "5.48.0", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -16094,9 +13296,7 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", - "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "version": "5.6.0", "dev": true, "license": "MIT", "dependencies": { @@ -16116,12 +13316,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -16129,15 +13356,11 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, "node_modules/tiny-async-pool": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", "dev": true, "license": "MIT", "dependencies": { @@ -16146,8 +13369,6 @@ }, "node_modules/tiny-async-pool/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -16159,26 +13380,19 @@ "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", "integrity": "sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/tiny-typed-emitter": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "license": "MIT" }, "node_modules/tinybench": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "version": "1.2.2", "dev": true, "license": "MIT", "engines": { @@ -16187,8 +13401,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -16202,41 +13414,8 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinyrainbow": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -16244,29 +13423,23 @@ } }, "node_modules/tldts": { - "version": "7.0.28", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz", - "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==", + "version": "7.4.0", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.28" + "tldts-core": "^7.4.0" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.28", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz", - "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==", + "version": "7.4.0", "dev": true, "license": "MIT" }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.6", "dev": true, "license": "MIT", "engines": { @@ -16275,8 +13448,6 @@ }, "node_modules/tmp-promise": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16285,8 +13456,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16298,8 +13467,6 @@ }, "node_modules/tough-cookie": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -16311,8 +13478,6 @@ }, "node_modules/tr46": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", "dependencies": { @@ -16324,8 +13489,6 @@ }, "node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { @@ -16334,8 +13497,6 @@ }, "node_modules/trim-lines": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "license": "MIT", "funding": { "type": "github", @@ -16344,8 +13505,6 @@ }, "node_modules/trim-repeated": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, "license": "MIT", "dependencies": { @@ -16357,8 +13516,6 @@ }, "node_modules/trim-repeated/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -16367,8 +13524,6 @@ }, "node_modules/trough": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "license": "MIT", "funding": { "type": "github", @@ -16377,8 +13532,6 @@ }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, "license": "WTFPL", "dependencies": { @@ -16387,8 +13540,6 @@ }, "node_modules/ts-api-utils": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -16400,8 +13551,6 @@ }, "node_modules/tsconfig-paths": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "license": "MIT", "dependencies": { @@ -16415,8 +13564,6 @@ }, "node_modules/tsconfig-paths-webpack-plugin": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", - "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", "dev": true, "license": "MIT", "dependencies": { @@ -16431,8 +13578,6 @@ }, "node_modules/tsdown": { "version": "0.21.10", - "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.21.10.tgz", - "integrity": "sha512-3wk73yBhZe/wX7REqSdivNQ84TDs1mJ+IlnzrrEREP70xlJ/AEIzqaI04l/TzMKVIdkTdC3CPaADn2Lk/0SkdA==", "dev": true, "license": "MIT", "dependencies": { @@ -16495,29 +13640,12 @@ } } }, - "node_modules/tsdown/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -16528,8 +13656,6 @@ }, "node_modules/tw-animate-css": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", - "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Wombosvideo" @@ -16537,8 +13663,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -16550,8 +13674,6 @@ }, "node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -16563,8 +13685,6 @@ }, "node_modules/typescript": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -16576,16 +13696,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", - "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "version": "8.60.0", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.0", - "@typescript-eslint/parser": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0" + "@typescript-eslint/eslint-plugin": "8.60.0", + "@typescript-eslint/parser": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -16601,15 +13719,11 @@ }, "node_modules/uc.micro": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true, "license": "MIT" }, "node_modules/unconfig-core": { "version": "7.5.0", - "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", - "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", "dev": true, "license": "MIT", "dependencies": { @@ -16621,9 +13735,7 @@ } }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.26.0", "dev": true, "license": "MIT", "engines": { @@ -16632,15 +13744,11 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", - "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", "dev": true, "license": "MIT", "engines": { @@ -16652,8 +13760,6 @@ }, "node_modules/unified": { "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -16669,10 +13775,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unified/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/unist-util-find-after": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16684,10 +13792,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-find-after/node_modules/@types/unist": { + "version": "3.0.3", + "dev": true, + "license": "MIT" + }, "node_modules/unist-util-is": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -16697,10 +13808,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-is/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/unist-util-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -16710,10 +13823,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -16723,10 +13838,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-stringify-position/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/unist-util-visit": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -16740,8 +13857,6 @@ }, "node_modules/unist-util-visit-parents": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -16752,61 +13867,64 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/universal-user-agent": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", "dev": true, "license": "ISC" }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "version": "1.12.2", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "napi-postinstall": "^0.3.0" + "napi-postinstall": "^0.3.4" }, "funding": { "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, "node_modules/unrun": { - "version": "0.2.37", - "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.37.tgz", - "integrity": "sha512-AA7vDuYsgeSYVzJMm16UKA+aXFKhy7nFqW9z5l7q44K4ppFWZAMqYS58ePRZbugMLPH0fwwMzD5A8nP0avxwZQ==", + "version": "0.2.39", "dev": true, "license": "MIT", "dependencies": { @@ -16832,8 +13950,6 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -16863,8 +13979,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -16873,8 +13987,6 @@ }, "node_modules/use-callback-ref": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -16894,8 +14006,6 @@ }, "node_modules/use-sidecar": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", @@ -16916,8 +14026,6 @@ }, "node_modules/use-sync-external-store": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -16925,8 +14033,6 @@ }, "node_modules/username": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", - "integrity": "sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==", "dev": true, "license": "MIT", "dependencies": { @@ -16939,21 +14045,15 @@ }, "node_modules/utf8-byte-length": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true, "license": "(WTFPL OR MIT)" }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16961,26 +14061,8 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/vfile": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -16993,8 +14075,6 @@ }, "node_modules/vfile-message": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17005,17 +14085,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-message/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" + }, "node_modules/vite": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "version": "8.0.14", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.17", + "postcss": "^8.5.15", + "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "bin": { @@ -17032,7 +14118,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -17083,33 +14169,78 @@ } } }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "node_modules/vite/node_modules/@oxc-project/types": { + "version": "0.132.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/rolldown": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" } }, "node_modules/vitest": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "version": "4.1.7", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.5", - "@vitest/mocker": "4.1.5", - "@vitest/pretty-format": "4.1.5", - "@vitest/runner": "4.1.5", - "@vitest/snapshot": "4.1.5", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -17137,12 +14268,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.5", - "@vitest/browser-preview": "4.1.5", - "@vitest/browser-webdriverio": "4.1.5", - "@vitest/coverage-istanbul": "4.1.5", - "@vitest/coverage-v8": "4.1.5", - "@vitest/ui": "4.1.5", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -17186,50 +14317,8 @@ } } }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.5", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vscode-jsonrpc": { "version": "8.2.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", - "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -17237,8 +14326,6 @@ }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { @@ -17250,8 +14337,6 @@ }, "node_modules/watchpack": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", "dependencies": { @@ -17264,8 +14349,6 @@ }, "node_modules/watskeburt": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-5.0.3.tgz", - "integrity": "sha512-g9CXukMjazlJJVQ3OHzXsnG25KFYgSgKMIyoJrD8ggr0DbS9UNF7OzIqWmmKKBMedkxj3T01uqEaGnn+y7QhMA==", "dev": true, "license": "MIT", "bin": { @@ -17277,8 +14360,6 @@ }, "node_modules/webidl-conversions": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -17286,13 +14367,10 @@ } }, "node_modules/webpack": { - "version": "5.106.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", - "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "version": "5.107.2", "dev": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", @@ -17302,20 +14380,20 @@ "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.20.0", - "es-module-lexer": "^2.0.0", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "loader-runner": "^4.3.1", + "loader-runner": "^4.3.2", "mime-db": "^1.54.0", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.17", + "terser-webpack-plugin": "^5.5.0", "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" + "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" @@ -17334,9 +14412,7 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "version": "3.5.0", "dev": true, "license": "MIT", "engines": { @@ -17345,8 +14421,6 @@ }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -17359,8 +14433,6 @@ }, "node_modules/webpack/node_modules/estraverse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -17369,8 +14441,6 @@ }, "node_modules/webpack/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -17379,8 +14449,6 @@ }, "node_modules/whatwg-mimetype": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", "engines": { @@ -17389,8 +14457,6 @@ }, "node_modules/whatwg-url": { "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", "dependencies": { @@ -17404,8 +14470,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -17420,8 +14484,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -17437,8 +14499,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -17447,8 +14507,6 @@ }, "node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -17460,48 +14518,12 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -17521,8 +14543,6 @@ }, "node_modules/xml-name-validator": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -17531,8 +14551,6 @@ }, "node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "dev": true, "license": "MIT", "engines": { @@ -17541,15 +14559,11 @@ }, "node_modules/xmlchars": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -17557,19 +14571,12 @@ } }, "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "version": "4.0.0", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } + "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -17587,52 +14594,17 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yauzl": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.1.tgz", "integrity": "sha512-RNPCUkiE/ZgO4w8i9U5yDQVHaFDdnzaFANElRvpJteCspvmv2VqrRb9lvS6odVD+jqI/zDsxAHJVsafpcheVQQ==", "dev": true, - "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" @@ -17643,8 +14615,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -17656,8 +14626,6 @@ }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "dev": true, "license": "MIT", "engines": { @@ -17669,8 +14637,6 @@ }, "node_modules/zod": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -17678,8 +14644,6 @@ }, "node_modules/zwitch": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "license": "MIT", "funding": { "type": "github", diff --git a/package.json b/package.json index c1bce595..6de9d9c7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "deps:check": "depcruise apps packages --config config/dependency-cruiser.cjs", "typecheck": "tsc --noEmit", "test": "vitest run --config config/vitest.config.ts --no-file-parallelism --maxWorkers=1", + "pretest": "node scripts/ensure-native-abi.cjs node", "test:coverage": "vitest run --config config/vitest.config.ts --coverage --no-file-parallelism --maxWorkers=1", "clean": "node scripts/clean-artifacts.js", "capture:hero": "node scripts/capture-readme-hero.js", @@ -39,6 +40,7 @@ "smoke:server-sdk": "npm --workspace @chamber/server run build && node scripts/run-server-sdk-smoke-test.js", "smoke:web": "npm run playwright:install && playwright test --config config/playwright.config.ts --project=web", "smoke:desktop": "npm run playwright:install && playwright test --config config/playwright.config.ts --project=electron --workers=1", + "presmoke:desktop": "node scripts/ensure-native-abi.cjs electron", "smoke:a2a-relay": "npm run playwright:install && playwright test --config config/playwright.config.ts --project=electron --workers=1 --headed tests/e2e/electron/a2a-relay-roundtrip.spec.ts", "smoke:byo-llm": "npm run playwright:install && playwright test --config config/playwright.config.ts --project=electron --workers=1 tests/e2e/electron/byo-llm-settings.spec.ts", "smoke:packaged-runtime": "npm --workspace @chamber/server run build && node scripts/packaged-smoke.js", diff --git a/packages/services/src/chat/ChatService.observers.test.ts b/packages/services/src/chat/ChatService.observers.test.ts new file mode 100644 index 00000000..80a6f88e --- /dev/null +++ b/packages/services/src/chat/ChatService.observers.test.ts @@ -0,0 +1,304 @@ +/** + * Phase 6 — TurnCompletionObserver wiring in ChatService. + * + * The success contract is exact: every observer is notified exactly once per + * turn that reached the SDK `done` state, with the full CompletedTurn + * payload. Aborted turns, errored turns, and SDK contract drift all suppress + * notification. One observer throwing — sync or async — must not block any + * other observer and must not leak back into the streaming path. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ChatService } from './ChatService'; +import { TurnQueue } from './TurnQueue'; +import type { MindManager } from '../mind'; +import type { CompletedTurn, TurnCompletionObserver } from '@chamber/shared/turn-observer'; + +interface SessionListeners { + idle: Array<() => void>; + error: Array<(event: unknown) => void>; + message: Array<(event: unknown) => void>; + delta: Array<(event: unknown) => void>; +} + +function createMockSession() { + const listeners: SessionListeners = { idle: [], error: [], message: [], delta: [] }; + const session = { + send: vi.fn().mockResolvedValue(undefined), + abort: vi.fn().mockResolvedValue(undefined), + destroy: vi.fn().mockResolvedValue(undefined), + on: vi.fn((event: string, cb: (...args: unknown[]) => void) => { + if (event === 'session.idle') listeners.idle.push(cb as () => void); + else if (event === 'session.error') listeners.error.push(cb as (event: unknown) => void); + else if (event === 'assistant.message') listeners.message.push(cb as (event: unknown) => void); + else if (event === 'assistant.message_delta') listeners.delta.push(cb as (event: unknown) => void); + return vi.fn(); + }), + }; + return { session, listeners }; +} + +function createMockManager(session: unknown) { + return { + getMind: vi.fn(() => ({ + session, + client: { listModels: vi.fn(async () => []) }, + activeSessionId: 'sdk-session-abc', + selectedModel: 'gpt-5.4', + })), + setMindModel: vi.fn(async () => null), + recoverActiveConversationSession: vi.fn(), + markActiveConversationHasMessages: vi.fn(), + listConversationHistory: vi.fn(() => []), + startNewConversation: vi.fn(), + resumeConversation: vi.fn(), + deleteConversation: vi.fn(), + renameConversation: vi.fn(() => []), + recycleClientForMind: vi.fn(), + reloadMind: vi.fn(), + }; +} + +const dateTimeProvider = () => ({ + currentDateTime: '2026-05-12T17:00:00.000Z', + timezone: 'America/New_York', +}); + +describe('ChatService — TurnCompletionObserver wiring (Phase 6)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('notifies every observer exactly once per successful turn with the full CompletedTurn payload', async () => { + const { session, listeners } = createMockSession(); + const mgr = createMockManager(session); + session.send.mockImplementation(async () => { + // Final assistant message arrives between send and idle. + listeners.message.forEach((cb) => + cb({ data: { messageId: 'sdk-msg-1', content: 'pong' } }), + ); + listeners.idle.forEach((cb) => cb()); + }); + + const captured: CompletedTurn[] = []; + const observerA: TurnCompletionObserver = { onTurnCompleted: (t) => { captured.push(t); } }; + const observerB: TurnCompletionObserver = { onTurnCompleted: (t) => { captured.push(t); } }; + + const svc = new ChatService( + mgr as unknown as MindManager, + new TurnQueue(), + dateTimeProvider, + ); + svc.addObserver(observerA); + svc.addObserver(observerB); + + await svc.sendMessage('mind-1', 'ping', 'msg-1', vi.fn()); + + expect(captured).toHaveLength(2); + const [a, b] = captured; + expect(a).toEqual(b); + expect(a.prompt).toBe('ping'); + expect(a.finalAssistantMessage).toBe('pong'); + expect(a.sessionId).toBe('sdk-session-abc'); + expect(a.model).toBe('gpt-5.4'); + expect(a.status).toBe('completed'); + expect(typeof a.turnId).toBe('string'); + expect(a.turnId.length).toBeGreaterThan(0); + expect(typeof a.startedAt).toBe('string'); + expect(typeof a.endedAt).toBe('string'); + expect(Date.parse(a.startedAt)).not.toBeNaN(); + expect(Date.parse(a.endedAt)).not.toBeNaN(); + expect(Date.parse(a.endedAt)).toBeGreaterThanOrEqual(Date.parse(a.startedAt)); + }); + + it('uses the explicitly-requested model from sendMessage when provided', async () => { + const { session, listeners } = createMockSession(); + const mgr = createMockManager(session); + session.send.mockImplementation(async () => { + listeners.message.forEach((cb) => cb({ data: { messageId: 'm', content: 'hi' } })); + listeners.idle.forEach((cb) => cb()); + }); + + const captured: CompletedTurn[] = []; + const svc = new ChatService( + mgr as unknown as MindManager, + new TurnQueue(), + dateTimeProvider, + ); + svc.addObserver({ onTurnCompleted: (t) => { captured.push(t); } }); + + await svc.sendMessage('mind-1', 'hi', 'msg-1', vi.fn(), 'opus-4.7'); + + expect(captured).toHaveLength(1); + expect(captured[0].model).toBe('opus-4.7'); + }); + + it('does NOT notify observers when the user aborts the turn mid-stream', async () => { + const { session, listeners } = createMockSession(); + const mgr = createMockManager(session); + // session.idle never fires; we abort externally. + session.send.mockResolvedValue(undefined); + + const observer = { onTurnCompleted: vi.fn() }; + const svc = new ChatService( + mgr as unknown as MindManager, + new TurnQueue(), + dateTimeProvider, + ); + svc.addObserver(observer); + + const pending = svc.sendMessage('mind-1', 'long-running', 'msg-1', vi.fn()); + // Allow the queue/streamTurn to wire up listeners and call send. + for (let i = 0; i < 10; i++) await Promise.resolve(); + // Even if the assistant text was captured before abort, abort suppresses notification. + listeners.message.forEach((cb) => cb({ data: { messageId: 'm', content: 'partial' } })); + await svc.cancelMessage('mind-1', 'msg-1'); + await pending; + + expect(observer.onTurnCompleted).not.toHaveBeenCalled(); + }); + + it('does NOT notify observers when the SDK signals session.error', async () => { + const { session, listeners } = createMockSession(); + const mgr = createMockManager(session); + session.send.mockImplementation(async () => { + listeners.error.forEach((cb) => cb({ data: { message: 'boom' } })); + }); + + const observer = { onTurnCompleted: vi.fn() }; + const svc = new ChatService( + mgr as unknown as MindManager, + new TurnQueue(), + dateTimeProvider, + ); + svc.addObserver(observer); + + const emit = vi.fn(); + await svc.sendMessage('mind-1', 'hi', 'msg-1', emit); + + expect(emit).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })); + expect(observer.onTurnCompleted).not.toHaveBeenCalled(); + }); + + it('does NOT notify observers when sendMessage rejects synchronously (mind missing)', async () => { + const mgr = createMockManager(null); + mgr.getMind.mockReturnValue(undefined as never); + + const observer = { onTurnCompleted: vi.fn() }; + const svc = new ChatService( + mgr as unknown as MindManager, + new TurnQueue(), + dateTimeProvider, + ); + svc.addObserver(observer); + + const emit = vi.fn(); + await svc.sendMessage('missing', 'hi', 'msg-1', emit); + + expect(emit).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })); + expect(observer.onTurnCompleted).not.toHaveBeenCalled(); + }); + + it('one observer throwing synchronously does NOT block subsequent observers and does NOT break streaming', async () => { + const { session, listeners } = createMockSession(); + const mgr = createMockManager(session); + session.send.mockImplementation(async () => { + listeners.message.forEach((cb) => cb({ data: { messageId: 'm', content: 'pong' } })); + listeners.idle.forEach((cb) => cb()); + }); + + const order: string[] = []; + const observerA: TurnCompletionObserver = { + onTurnCompleted: () => { order.push('a'); throw new Error('observer A boom'); }, + }; + const observerB: TurnCompletionObserver = { + onTurnCompleted: () => { order.push('b'); }, + }; + const observerC: TurnCompletionObserver = { + onTurnCompleted: () => { order.push('c'); }, + }; + + const consoleWarn = vi.spyOn(console, 'log').mockImplementation(() => undefined); + try { + const svc = new ChatService( + mgr as unknown as MindManager, + new TurnQueue(), + dateTimeProvider, + ); + svc.addObserver(observerA); + svc.addObserver(observerB); + svc.addObserver(observerC); + + const emit = vi.fn(); + // Whole streaming path must still resolve cleanly. + await expect(svc.sendMessage('mind-1', 'ping', 'msg-1', emit)).resolves.toBeUndefined(); + + expect(order).toEqual(['a', 'b', 'c']); + expect(emit).toHaveBeenCalledWith({ type: 'done' }); + expect(emit).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })); + } finally { + consoleWarn.mockRestore(); + } + }); + + it('an observer rejecting asynchronously is logged and does NOT surface back into the streaming path', async () => { + const { session, listeners } = createMockSession(); + const mgr = createMockManager(session); + session.send.mockImplementation(async () => { + listeners.message.forEach((cb) => cb({ data: { messageId: 'm', content: 'pong' } })); + listeners.idle.forEach((cb) => cb()); + }); + + const observerA: TurnCompletionObserver = { + onTurnCompleted: async () => { throw new Error('async boom'); }, + }; + const observerB = { onTurnCompleted: vi.fn() }; + + const consoleWarn = vi.spyOn(console, 'log').mockImplementation(() => undefined); + try { + const svc = new ChatService( + mgr as unknown as MindManager, + new TurnQueue(), + dateTimeProvider, + ); + svc.addObserver(observerA); + svc.addObserver(observerB); + + const emit = vi.fn(); + await expect(svc.sendMessage('mind-1', 'ping', 'msg-1', emit)).resolves.toBeUndefined(); + + // observer B was still invoked despite A's async rejection. + expect(observerB.onTurnCompleted).toHaveBeenCalledTimes(1); + // Streaming path stayed clean — no error event leaked from the observer failure. + expect(emit).toHaveBeenCalledWith({ type: 'done' }); + expect(emit).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })); + + // Drain microtasks to give the .catch on the rejected observer promise + // a chance to run before the test ends so we know it was attached + // (otherwise vitest would surface an unhandled rejection). + for (let i = 0; i < 5; i++) await Promise.resolve(); + } finally { + consoleWarn.mockRestore(); + } + }); + + it('default observers list is empty — existing 3-arg constructor callers keep working unchanged', async () => { + const { session, listeners } = createMockSession(); + const mgr = createMockManager(session); + session.send.mockImplementation(async () => { + listeners.idle.forEach((cb) => cb()); + }); + + // Three-arg construction (no observers) is the legacy contract. + const svc = new ChatService( + mgr as unknown as MindManager, + new TurnQueue(), + dateTimeProvider, + ); + + const emit = vi.fn(); + await svc.sendMessage('mind-1', 'hi', 'msg-1', emit); + + expect(emit).toHaveBeenCalledWith({ type: 'done' }); + }); +}); diff --git a/packages/services/src/chat/ChatService.ts b/packages/services/src/chat/ChatService.ts index 41e2a26e..fb30b754 100644 --- a/packages/services/src/chat/ChatService.ts +++ b/packages/services/src/chat/ChatService.ts @@ -1,8 +1,10 @@ // ChatService — thin message streaming layer. // Gets sessions from MindManager, streams SDK events via callback. +import { randomUUID } from 'node:crypto'; import type { MindManager } from '../mind'; import type { ChatEvent, ChatImageAttachment, ConversationResumeResult, ConversationSummary, ModelInfo } from '@chamber/shared/types'; +import type { CompletedTurn, TurnCompletionObserver } from '@chamber/shared/turn-observer'; import { modelSelectionKeyFromModel } from '@chamber/shared/model-selection'; import type { CopilotSession } from '../mind/types'; import { isStaleSessionError, SEND_TIMEOUT_MS, sendTimeoutError } from '@chamber/shared/sessionErrors'; @@ -34,6 +36,7 @@ const TURN_END_QUIESCENCE_MS = 1_000; export class ChatService { private abortControllers = new Map(); + private readonly observers: TurnCompletionObserver[] = []; constructor( private readonly mindManager: MindManager, @@ -50,6 +53,26 @@ export class ChatService { private readonly byoLlmModelsProvider?: () => Promise, ) {} + /** + * Register a turn-completion observer (Phase 11 wiring — used by + * MindMemoryService to attach the per-mind DailyLogWriter when a mind is + * activated). Adding an observer mid-flight is safe because + * `notifyTurnCompleted` reads the array at notify time. + */ + addObserver(observer: TurnCompletionObserver): void { + this.observers.push(observer); + } + + /** + * Remove a previously-registered observer (no-op if not present). Used + * by MindMemoryService.releaseMind so a swapped-out mind stops receiving + * turn frames. + */ + removeObserver(observer: TurnCompletionObserver): void { + const i = this.observers.indexOf(observer); + if (i !== -1) this.observers.splice(i, 1); + } + async sendMessage( mindId: string, prompt: string, @@ -62,17 +85,21 @@ export class ChatService { const abortController = new AbortController(); this.abortControllers.set(mindId, abortController); + const startedAt = new Date().toISOString(); + const turnId = randomUUID(); + try { const context = this.mindManager.getMind(mindId); if (!context?.session) { throw new Error(`Mind ${mindId} not found or has no session`); } + let finalAssistantMessage: string | null = null; try { const session = model ? await this.mindManager.setMindModel(mindId, model) : null; const currentSession = session ? this.mindManager.getMind(mindId)?.session : context.session; if (!currentSession) throw new Error(`Mind ${mindId} not found or has no session`); - await this.streamTurn(currentSession, prompt, abortController, emit, attachments, () => { + finalAssistantMessage = await this.streamTurn(currentSession, prompt, abortController, emit, attachments, () => { this.mindManager.markActiveConversationHasMessages(mindId, prompt); }); } catch (err) { @@ -84,10 +111,35 @@ export class ChatService { emit({ type: 'reconnecting' }); const recoveredSession = await this.mindManager.recoverActiveConversationSession(mindId); if (abortController.signal.aborted) return; - await this.streamTurn(recoveredSession, prompt, abortController, emit, attachments, () => { + finalAssistantMessage = await this.streamTurn(recoveredSession, prompt, abortController, emit, attachments, () => { this.mindManager.markActiveConversationHasMessages(mindId, prompt); }); } + + // Notify TurnCompletionObservers ONLY on successful completion. The + // streamTurn helper returns null whenever the turn was aborted by + // the user or torn down by an SDK contract failure; both branches + // skip notification. Errors thrown out of streamTurn fall through + // to the outer catch below, which also bypasses notification. + if (finalAssistantMessage !== null && !abortController.signal.aborted) { + const endedAt = new Date().toISOString(); + const refreshed = this.mindManager.getMind(mindId); + // Coerce empty model to a sentinel so the structured-log frame is + // semantically meaningful. The parser accepts empty values, but + // 'unknown' is more useful in rendered rollback markdown than a + // bare `(`. + const turnModel = model ?? refreshed?.selectedModel ?? ''; + this.notifyTurnCompleted({ + turnId, + sessionId: refreshed?.activeSessionId ?? '', + model: turnModel.length > 0 ? turnModel : 'unknown', + status: 'completed', + startedAt, + endedAt, + prompt, + finalAssistantMessage, + }); + } } catch (err) { if (abortController.signal.aborted) return; const rawMessage = err instanceof Error ? err.message : String(err); @@ -98,6 +150,28 @@ export class ChatService { }); } + /** + * Notify each observer of a completed turn. One observer throwing (sync + * or async) must NOT block subsequent observers and must NOT propagate + * back into the streaming path. Failures are logged at warn level with + * the offending observer's index for triage. + */ + private notifyTurnCompleted(turn: CompletedTurn): void { + for (let i = 0; i < this.observers.length; i++) { + const observer = this.observers[i]; + try { + const result = observer.onTurnCompleted(turn); + if (result && typeof (result as Promise).then === 'function') { + Promise.resolve(result).catch((err: unknown) => { + log.warn(`TurnCompletionObserver[${i}] failed asynchronously`, err); + }); + } + } catch (err) { + log.warn(`TurnCompletionObserver[${i}] failed`, err); + } + } + } + private async streamTurn( session: CopilotSession, prompt: string, @@ -105,10 +179,11 @@ export class ChatService { emit: (event: ChatEvent) => void, attachments?: ChatImageAttachment[], onSendAccepted?: () => void, - ): Promise{ - if (abortController.signal.aborted) return; + ): Promise{ + if (abortController.signal.aborted) return null; const unsubs: (() => void)[] = []; + let finalAssistantMessage = ''; const guard = (fn: () => void) => { if (!abortController.signal.aborted) fn(); }; let sdkContractFailed = false; const failSdkContract = (error: unknown) => { @@ -243,9 +318,16 @@ export class ChatService { emitMapped(() => mapSdkAssistantMessageDelta(event)); })); - // Final assistant message + // Final assistant message — also captured for TurnCompletionObservers + // so the observer payload carries the same text the renderer sees. + // The SDK can fire `assistant.message` more than once per turn (e.g. + // around tool use); keep the most recent non-null content. unsubs.push(session.on('assistant.message', (event) => { - emitMapped(() => mapSdkAssistantMessage(event)); + emitMapped(() => { + const mapped = mapSdkAssistantMessage(event); + if (mapped) finalAssistantMessage = mapped.content; + return mapped; + }); })); // Reasoning @@ -361,8 +443,9 @@ export class ChatService { // Wait for idle (listeners already active from before send). await turnDone; - if (abortController.signal.aborted) return; + if (abortController.signal.aborted) return null; emit({ type: 'done' }); + return finalAssistantMessage; } finally { clearTurnEndQuiescence(); for (const unsub of unsubs) unsub(); diff --git a/packages/services/src/chat/IdentityLoader.test.ts b/packages/services/src/chat/IdentityLoader.test.ts index 7935d4d7..86eb3be4 100644 --- a/packages/services/src/chat/IdentityLoader.test.ts +++ b/packages/services/src/chat/IdentityLoader.test.ts @@ -89,11 +89,27 @@ describe('IdentityLoader', () => { return [] as unknown as ReturnType; }); - const result = loader.load('/tmp/test'); + // Use an injected composer so the test does not depend on `node:fs` + // (the real composer reads via `node:fs`, which the `vi.mock('fs')` setup + // above does not intercept). The composer contract here is only that + // IdentityLoader forwards mindPath and inserts the returned section. + const composer = { + compose: vi.fn(() => 'Curated memory\n\n---\n\nOperational rule'), + }; + const customLoader = new IdentityLoader(() => [], composer); + const result = customLoader.load('/tmp/test'); + expect(composer.compose).toHaveBeenCalledWith('/tmp/test', expect.objectContaining({ + lastKTurns: expect.any(Number), + perTurnMaxBytes: expect.any(Number), + memoryMaxBytes: expect.any(Number), + })); expect(result?.systemMessage).toContain('Curated memory'); expect(result?.systemMessage).toContain('Operational rule'); - expect(result?.systemMessage).toContain('Chronological note'); + // Unstructured log.md is filtered out by the composer; the IdentityLoader + // never includes it directly. The fake composer above returns no log + // section, so the chronological note must NOT appear. + expect(result?.systemMessage).not.toContain('Chronological note'); }); it('does not extract the mind name from working-memory headings', () => { @@ -118,7 +134,9 @@ describe('IdentityLoader', () => { return [] as unknown as ReturnType; }); - const result = loader.load('/tmp/agents/fox'); + const composer = { compose: vi.fn(() => '# Memory\nCurated memory') }; + const customLoader = new IdentityLoader(() => [], composer); + const result = customLoader.load('/tmp/agents/fox'); expect(result?.name).toBe('fox'); expect(result?.systemMessage).toContain('# Memory'); @@ -182,5 +200,158 @@ describe('IdentityLoader', () => { expect(systemMessage.indexOf('## Chamber')).toBeGreaterThan(systemMessage.indexOf('# Q')); expect(systemMessage.indexOf('## Tools')).toBeGreaterThan(systemMessage.indexOf('## Chamber')); }); + + it('uses a default WorkingMemoryComposer when none injected', () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + vi.mocked(fs.readFileSync).mockReturnValue(''); + vi.mocked(fs.readdirSync).mockReturnValue([]); + // Should not throw — proves the default composer is constructed and called. + const defaultLoader = new IdentityLoader(); + expect(() => defaultLoader.load('/tmp/test')).not.toThrow(); + }); + + it('forwards mindPath and resolved config defaults to the composer', () => { + vi.mocked(fs.existsSync).mockImplementation((candidate) => { + const normalized = String(candidate).replace(/\\/g, '/'); + return normalized.endsWith('SOUL.md'); + }); + vi.mocked(fs.readFileSync).mockReturnValue('# Soul'); + vi.mocked(fs.readdirSync).mockReturnValue([]); + const composer = { compose: vi.fn(() => '') }; + const loader2 = new IdentityLoader(() => [], composer); + loader2.load('/tmp/agents/widget'); + + expect(composer.compose).toHaveBeenCalledWith( + '/tmp/agents/widget', + { + // Defaults from chamberMindConfig (Phase 4) when no .chamber.json exists. + // Phase 1 of v0.60.0 added `enabled` (strict opt-in for the dream daemon). + enabled: false, + lastKTurns: 10, + perTurnMaxBytes: 2048, + memoryMaxBytes: 8192, + }, + ); + }); + + it('backward compat: builds a system prompt when composer returns empty', () => { + vi.mocked(fs.existsSync).mockImplementation((candidate) => { + const normalized = String(candidate).replace(/\\/g, '/'); + return normalized.endsWith('SOUL.md'); + }); + vi.mocked(fs.readFileSync).mockReturnValue('# Mind\nIdentity body'); + vi.mocked(fs.readdirSync).mockReturnValue([]); + const composer = { compose: vi.fn(() => '') }; + const loader2 = new IdentityLoader(() => [], composer); + const result = loader2.load('/tmp/test'); + + expect(result).not.toBeNull(); + expect(result?.systemMessage).toContain('Identity body'); + expect(result?.systemMessage).toContain('## Chamber'); + }); + + it('backward compat: does not crash when composer throws', () => { + vi.mocked(fs.existsSync).mockImplementation((candidate) => { + const normalized = String(candidate).replace(/\\/g, '/'); + return normalized.endsWith('SOUL.md'); + }); + vi.mocked(fs.readFileSync).mockReturnValue('# Soul'); + vi.mocked(fs.readdirSync).mockReturnValue([]); + const composer = { + compose: vi.fn(() => { + throw new Error('composer boom'); + }), + }; + const loader2 = new IdentityLoader(() => [], composer); + expect(() => loader2.load('/tmp/test')).not.toThrow(); + const result = loader2.load('/tmp/test'); + expect(result?.systemMessage).toContain('# Soul'); + }); + + describe('feature-flag gate (dreamDaemonFeatureEnabled)', () => { + // The app-level `dreamDaemon` flag must be authoritative over the + // per-mind `.chamber.json workingMemory.consolidation.enabled` field. + // A stable build that picks up a mind opted-in under insiders must + // still pass `enabled: false` to the composer so the system prompt + // never references consolidated structured-log content. + const mockChamberJsonWithDaemonEnabled = () => { + vi.mocked(fs.existsSync).mockImplementation((candidate) => { + const normalized = String(candidate).replace(/\\/g, '/'); + return normalized.endsWith('SOUL.md') || normalized.endsWith('.chamber.json'); + }); + vi.mocked(fs.readFileSync).mockImplementation((candidate) => { + const normalized = String(candidate).replace(/\\/g, '/'); + if (normalized.endsWith('.chamber.json')) { + return JSON.stringify({ + workingMemory: { + consolidation: { + enabled: true, + lastKTurns: 7, + perTurnMaxBytes: 4096, + memoryMaxBytes: 16384, + }, + }, + }); + } + return '# Soul'; + }); + vi.mocked(fs.readdirSync).mockReturnValue([]); + }; + + it('forces enabled:false when the feature accessor returns false', () => { + mockChamberJsonWithDaemonEnabled(); + const composer = { compose: vi.fn(() => '') }; + const loader2 = new IdentityLoader(() => [], composer, () => false); + + loader2.load('/tmp/agents/widget'); + + expect(composer.compose).toHaveBeenCalledWith( + '/tmp/agents/widget', + // Caps come from .chamber.json (faithful to user config) so a + // future re-enable resumes with the persisted limits. Only the + // `enabled` bit is overridden by the app-level flag. + { + enabled: false, + lastKTurns: 7, + perTurnMaxBytes: 4096, + memoryMaxBytes: 16384, + }, + ); + }); + + it('honors .chamber.json enabled:true when the feature accessor returns true', () => { + mockChamberJsonWithDaemonEnabled(); + const composer = { compose: vi.fn(() => '') }; + const loader2 = new IdentityLoader(() => [], composer, () => true); + + loader2.load('/tmp/agents/widget'); + + expect(composer.compose).toHaveBeenCalledWith( + '/tmp/agents/widget', + { + enabled: true, + lastKTurns: 7, + perTurnMaxBytes: 4096, + memoryMaxBytes: 16384, + }, + ); + }); + + it('default-on accessor preserves existing behavior when omitted', () => { + // Backwards-compatibility guarantee: every existing IdentityLoader + // call site (server bin, tests, e2e harness) constructs without + // the third arg and must continue to honor .chamber.json verbatim. + mockChamberJsonWithDaemonEnabled(); + const composer = { compose: vi.fn(() => '') }; + const loader2 = new IdentityLoader(() => [], composer); + + loader2.load('/tmp/agents/widget'); + + expect(composer.compose).toHaveBeenCalledWith( + '/tmp/agents/widget', + expect.objectContaining({ enabled: true }), + ); + }); + }); }); }); diff --git a/packages/services/src/chat/IdentityLoader.ts b/packages/services/src/chat/IdentityLoader.ts index 97b6d59d..c167a4a8 100644 --- a/packages/services/src/chat/IdentityLoader.ts +++ b/packages/services/src/chat/IdentityLoader.ts @@ -3,15 +3,40 @@ import * as path from 'path'; import type { InstalledTool, MindIdentity } from '@chamber/shared/types'; import { buildToolsSection } from '../tools/toolsSystemMessage'; import { buildChamberSection } from './chamberSystemMessage'; +import { + createWorkingMemoryComposer, + type WorkingMemoryComposer, + type WorkingMemoryComposerConfig, +} from './WorkingMemoryComposer'; +import { + loadChamberMindConfig, + DEFAULT_WORKING_MEMORY_CONSOLIDATION, +} from '../mind/chamberMindConfig'; const FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/; const H1_RE = /^#\s+(.+)$/m; -const WORKING_MEMORY_FILES = ['memory.md', 'rules.md', 'log.md']; export type InstalledToolsProvider = () => InstalledTool[]; export class IdentityLoader { - constructor(private readonly getInstalledTools: InstalledToolsProvider = () => []) {} + private readonly composer: WorkingMemoryComposer; + + constructor( + private readonly getInstalledTools: InstalledToolsProvider = () => [], + composer: WorkingMemoryComposer = createWorkingMemoryComposer(), + /** + * Returns the current value of the app-level `dreamDaemon` feature flag. + * When false, `resolveComposerConfig` forces `enabled: false` regardless + * of `.chamber.json workingMemory.consolidation.enabled` — so the system + * prompt never includes consolidated `log.md` content in a build where + * the feature is off, even if a mind was opted-in under an insiders run. + * Defaults to always-on so the services package stays decoupled from + * app-shell types and existing test fixtures keep working. + */ + private readonly dreamDaemonFeatureEnabled: () => boolean = () => true, + ) { + this.composer = composer; + } load(mindPath: string | null): MindIdentity | null { if (!mindPath) return null; @@ -39,18 +64,10 @@ export class IdentityLoader { } catch { /* missing */ } try { - const memoryDir = path.join(mindPath, '.working-memory'); - if (!fs.existsSync(memoryDir)) throw new Error('missing working-memory'); - const files = fs.readdirSync(memoryDir) - .map((file) => String(file)) - .filter((file) => WORKING_MEMORY_FILES.includes(file)) - .sort((a, b) => WORKING_MEMORY_FILES.indexOf(a) - WORKING_MEMORY_FILES.indexOf(b)); - for (const file of files) { - const filePath = path.join(memoryDir, file); - const content = fs.readFileSync(filePath, 'utf-8').trim(); - if (content.length > 0) memoryParts.push(content); - } - } catch { /* missing */ } + const composerConfig = this.resolveComposerConfig(mindPath); + const memorySection = this.composer.compose(mindPath, composerConfig); + if (memorySection.length > 0) memoryParts.push(memorySection); + } catch { /* composer is defensive; defense-in-depth */ } const parts = [...identityParts, ...memoryParts]; if (parts.length === 0) return null; @@ -74,4 +91,33 @@ export class IdentityLoader { } return path.basename(mindPath); } + + private resolveComposerConfig(mindPath: string): WorkingMemoryComposerConfig { + // .chamber.json is the source of truth for composer caps. loadChamberMindConfig + // already returns DEFAULT_WORKING_MEMORY_CONSOLIDATION when the file is missing, + // unparseable, or schema-invalid, so this never throws. Defaults are also + // exported here so a composer-only failure path still has a fallback. + // + // App-level feature flag is authoritative over per-mind opt-in: when the + // `dreamDaemon` flag is off, force `enabled: false` regardless of what + // `.chamber.json` says. The caps (lastKTurns / perTurnMaxBytes / + // memoryMaxBytes) are kept faithfully so a future re-enable can resume + // with the user's previously-configured limits. + try { + const c = loadChamberMindConfig(mindPath).workingMemory.consolidation; + return { + enabled: this.dreamDaemonFeatureEnabled() ? c.enabled : false, + lastKTurns: c.lastKTurns, + perTurnMaxBytes: c.perTurnMaxBytes, + memoryMaxBytes: c.memoryMaxBytes, + }; + } catch { + return { + enabled: DEFAULT_WORKING_MEMORY_CONSOLIDATION.enabled, + lastKTurns: DEFAULT_WORKING_MEMORY_CONSOLIDATION.lastKTurns, + perTurnMaxBytes: DEFAULT_WORKING_MEMORY_CONSOLIDATION.perTurnMaxBytes, + memoryMaxBytes: DEFAULT_WORKING_MEMORY_CONSOLIDATION.memoryMaxBytes, + }; + } + } } diff --git a/packages/services/src/chat/WorkingMemoryComposer.test.ts b/packages/services/src/chat/WorkingMemoryComposer.test.ts new file mode 100644 index 00000000..1bc44069 --- /dev/null +++ b/packages/services/src/chat/WorkingMemoryComposer.test.ts @@ -0,0 +1,349 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { createWorkingMemoryComposer, type WorkingMemoryComposerConfig } from './WorkingMemoryComposer'; +import { STRUCTURED_LOG_SENTINEL, serializeTurn, type CompletedTurn } from '../mindMemory/StructuredLogFormat'; + +// Most existing tests assert opted-in behaviour (sentinel logs, truncation, +// info-on-unstructured). Default `enabled: true` keeps those tests unchanged. +// New tests below pass `enabled: false` to exercise the opt-out gate added +// in v0.60.0. See "Dream Daemon Opt-In UX" (issue tracked in plan.md). +const DEFAULTS: WorkingMemoryComposerConfig = { + enabled: true, + lastKTurns: 10, + perTurnMaxBytes: 2048, + memoryMaxBytes: 8192, +}; + +let mindRoot: string; +let workingMemoryDir: string; + +beforeEach(() => { + mindRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-wmc-')); + workingMemoryDir = path.join(mindRoot, '.working-memory'); + fs.mkdirSync(workingMemoryDir, { recursive: true }); +}); + +afterEach(() => { + fs.rmSync(mindRoot, { recursive: true, force: true }); +}); + +function makeTurn(i: number, overrides: Partial = {}): CompletedTurn { + const ts = `2026-05-12T17:${String(20 + i).padStart(2, '0')}:00Z`; + return { + turnId: `turn-${i}`, + sessionId: `sess-${i}`, + model: 'claude-opus-4.7', + status: 'completed', + startedAt: ts, + endedAt: ts, + prompt: `prompt body ${i}`, + finalAssistantMessage: `assistant body ${i}`, + ...overrides, + }; +} + +function writeStructuredLog(turns: CompletedTurn[]): void { + const body = STRUCTURED_LOG_SENTINEL + '\n\n' + turns.map(serializeTurn).join('\n'); + fs.writeFileSync(path.join(workingMemoryDir, 'log.md'), body, 'utf-8'); +} + +describe('WorkingMemoryComposer.compose', () => { + it('returns empty string when working-memory dir is empty', () => { + const composer = createWorkingMemoryComposer(); + expect(composer.compose(mindRoot, DEFAULTS)).toBe(''); + }); + + it('returns empty string when working-memory dir is missing', () => { + fs.rmSync(workingMemoryDir, { recursive: true, force: true }); + const composer = createWorkingMemoryComposer(); + expect(composer.compose(mindRoot, DEFAULTS)).toBe(''); + }); + + it('includes only rules.md when memory and log are absent', () => { + fs.writeFileSync(path.join(workingMemoryDir, 'rules.md'), 'Operational rule', 'utf-8'); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, DEFAULTS); + expect(out).toContain('Operational rule'); + expect(out).not.toContain('---'); + }); + + it('includes memory.md and rules.md joined by separator', () => { + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'Curated memory', 'utf-8'); + fs.writeFileSync(path.join(workingMemoryDir, 'rules.md'), 'Operational rule', 'utf-8'); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, DEFAULTS); + expect(out).toBe('Curated memory\n\n---\n\nOperational rule'); + }); + + it('returns only the last K structured turns when log has more than K', () => { + const turns = Array.from({ length: 15 }, (_, i) => makeTurn(i)); + writeStructuredLog(turns); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, { ...DEFAULTS, lastKTurns: 5 }); + expect(out).toContain('turn:turn-14'); + expect(out).toContain('turn:turn-10'); + expect(out).not.toContain('turn:turn-9'); + expect(out).not.toContain('turn:turn-0'); + }); + + it('returns all turns when log has fewer than K', () => { + writeStructuredLog([makeTurn(0), makeTurn(1)]); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, { ...DEFAULTS, lastKTurns: 10 }); + expect(out).toContain('turn:turn-0'); + expect(out).toContain('turn:turn-1'); + }); + + it('truncates a turn whose rendered size exceeds perTurnMaxBytes', () => { + const huge = 'x'.repeat(10_000); + writeStructuredLog([makeTurn(0, { finalAssistantMessage: huge })]); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, { ...DEFAULTS, perTurnMaxBytes: 512 }); + // The rendered turn (or its truncation block) must not exceed the cap by more than a small margin. + // We assert two things: + // 1) The full huge body is NOT present verbatim. + expect(out).not.toContain(huge); + // 2) A truncation marker is present. + expect(out).toMatch(/truncated/); + }); + + it('logs at info-level (not warn) when log.md is unstructured, and contributes nothing', () => { + // Migration window: pre-existing minds may still have an unstructured + // log.md. DailyLogWriter will rotate it on first turn. Until then, the + // composer must skip the section without elevating to a [warn] (which + // misleads SREs into thinking something failed). Uncle Bob (plan review) + // rejected the Set dedupe — this is the lightweight alternative. + // TODO: remove the info-level fallback after the migration window closes. + fs.writeFileSync(path.join(workingMemoryDir, 'log.md'), 'just freeform notes\nnot structured\n', 'utf-8'); + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'mem', 'utf-8'); + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + const out = composer.compose(mindRoot, DEFAULTS); + expect(out).toBe('mem'); + expect(out).not.toContain('freeform'); + expect(warn).not.toHaveBeenCalled(); + expect(info).toHaveBeenCalledTimes(1); + expect(info.mock.calls[0][0]).toMatch(/unstructured/i); + // Cosmetic (Uncle Bob plan-review finding 6): the message text must NOT + // start with `WorkingMemoryComposer:` — the Logger already prepends the + // tag, so duplicating it produces noisy `[WorkingMemoryComposer] WorkingMemoryComposer: ...` + // lines in tray logs. Locked here to prevent regression. + expect(info.mock.calls[0][0]).not.toMatch(/^WorkingMemoryComposer:/); + }); + + it('emits neither warn nor info when log.md is sentinel-only with zero turns (the new-mind default)', () => { + // After Fix 2, MindScaffold.createStructure seeds log.md with the + // chamber-structured-log/v1 sentinel and no turn frames. The composer must + // recognise this as a structured-but-empty log, not as unstructured noise. + fs.writeFileSync( + path.join(workingMemoryDir, 'log.md'), + STRUCTURED_LOG_SENTINEL + '\n', + 'utf-8', + ); + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + const out = composer.compose(mindRoot, DEFAULTS); + expect(out).toBe(''); + expect(warn).not.toHaveBeenCalled(); + expect(info).not.toHaveBeenCalled(); + }); + + it('omits log entirely when log.md is unstructured (no sentinel) — historical assertion superseded by warn/info split above', () => { + // Kept as a regression check that the section is omitted, regardless of + // log level. The level itself is locked by the test above. + fs.writeFileSync(path.join(workingMemoryDir, 'log.md'), 'just freeform notes\nnot structured\n', 'utf-8'); + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'mem', 'utf-8'); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, DEFAULTS); + expect(out).toBe('mem'); + expect(out).not.toContain('freeform'); + }); + + it('contributes nothing when log.md is missing', () => { + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'mem', 'utf-8'); + const composer = createWorkingMemoryComposer(); + expect(composer.compose(mindRoot, DEFAULTS)).toBe('mem'); + }); + + it('never includes log.legacy.md content', () => { + writeStructuredLog([makeTurn(0)]); + fs.writeFileSync( + path.join(workingMemoryDir, 'log.legacy.md'), + 'LEGACY-CONTENT-MUST-NOT-APPEAR', + 'utf-8', + ); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, DEFAULTS); + expect(out).not.toContain('LEGACY-CONTENT-MUST-NOT-APPEAR'); + expect(out).toContain('turn:turn-0'); + }); + + it('truncates memory.md when it exceeds memoryMaxBytes', () => { + const huge = 'm'.repeat(20_000); + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), huge, 'utf-8'); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, { ...DEFAULTS, memoryMaxBytes: 1024 }); + expect(Buffer.byteLength(out, 'utf-8')).toBeLessThanOrEqual(1024 + 100); // marker tolerance + expect(out).toMatch(/truncated/); + }); + + it('does not throw when memory.md, rules.md, and log.md are all missing', () => { + const composer = createWorkingMemoryComposer(); + expect(() => composer.compose(mindRoot, DEFAULTS)).not.toThrow(); + expect(composer.compose(mindRoot, DEFAULTS)).toBe(''); + }); + + it('respects custom lastKTurns from config', () => { + const turns = Array.from({ length: 8 }, (_, i) => makeTurn(i)); + writeStructuredLog(turns); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, { ...DEFAULTS, lastKTurns: 3 }); + expect(out).toContain('turn:turn-7'); + expect(out).toContain('turn:turn-6'); + expect(out).toContain('turn:turn-5'); + expect(out).not.toContain('turn:turn-4'); + }); + + it('orders sections memory → rules → log', () => { + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'MEMSECTION', 'utf-8'); + fs.writeFileSync(path.join(workingMemoryDir, 'rules.md'), 'RULESECTION', 'utf-8'); + writeStructuredLog([makeTurn(0)]); + const composer = createWorkingMemoryComposer(); + const out = composer.compose(mindRoot, DEFAULTS); + const memIdx = out.indexOf('MEMSECTION'); + const ruleIdx = out.indexOf('RULESECTION'); + const logIdx = out.indexOf('turn:turn-0'); + expect(memIdx).toBeGreaterThanOrEqual(0); + expect(ruleIdx).toBeGreaterThan(memIdx); + expect(logIdx).toBeGreaterThan(ruleIdx); + }); + + it('skips empty/whitespace-only files', () => { + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), ' \n\n', 'utf-8'); + fs.writeFileSync(path.join(workingMemoryDir, 'rules.md'), 'rules-content', 'utf-8'); + const composer = createWorkingMemoryComposer(); + expect(composer.compose(mindRoot, DEFAULTS)).toBe('rules-content'); + }); +}); + +// --------------------------------------------------------------------------- +// v0.60.0 — Dream Daemon opt-in gate (Phase 1) +// +// The composer must NOT include the structured-log section when the mind has +// opted out of dream-daemon consolidation (the default). For opted-out minds +// the composer also must NOT log info or warn — silence is the contract; +// otherwise tray logs would scream "log.md is unstructured" for every brand- +// new mind that hasn't enabled the feature. +// +// Per Uncle Bob plan-review (finding 6) the info-once dedupe Set lives on the +// COMPOSER INSTANCE, not module scope, so each test gets fresh state without +// resetModules() acrobatics. The test below proves the dedupe path on a +// single instance shared across two compose() calls. +// --------------------------------------------------------------------------- + +describe('WorkingMemoryComposer.compose — dream-daemon opt-in gate', () => { + it('opted-out + structured log → log section omitted, no info, no warn', () => { + writeStructuredLog([makeTurn(0)]); + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'mem', 'utf-8'); + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + const out = composer.compose(mindRoot, { ...DEFAULTS, enabled: false }); + expect(out).toBe('mem'); + expect(out).not.toContain('turn:turn-0'); + expect(warn).not.toHaveBeenCalled(); + expect(info).not.toHaveBeenCalled(); + }); + + it('opted-out + unstructured log → log section omitted, no info, no warn (silence is the contract)', () => { + fs.writeFileSync(path.join(workingMemoryDir, 'log.md'), 'just freeform notes\nnot structured\n', 'utf-8'); + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'mem', 'utf-8'); + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + const out = composer.compose(mindRoot, { ...DEFAULTS, enabled: false }); + expect(out).toBe('mem'); + expect(out).not.toContain('freeform'); + expect(warn).not.toHaveBeenCalled(); + expect(info).not.toHaveBeenCalled(); + }); + + it('opted-out + sentinel-only log → log section omitted, no info, no warn', () => { + fs.writeFileSync( + path.join(workingMemoryDir, 'log.md'), + STRUCTURED_LOG_SENTINEL + '\n\n', + 'utf-8', + ); + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'mem', 'utf-8'); + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + const out = composer.compose(mindRoot, { ...DEFAULTS, enabled: false }); + expect(out).toBe('mem'); + expect(warn).not.toHaveBeenCalled(); + expect(info).not.toHaveBeenCalled(); + }); + + it('opted-in + unstructured log → info fires AT MOST ONCE per composer instance, even across many compose() calls', () => { + fs.writeFileSync(path.join(workingMemoryDir, 'log.md'), 'unstructured\n', 'utf-8'); + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'mem', 'utf-8'); + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + + composer.compose(mindRoot, DEFAULTS); + composer.compose(mindRoot, DEFAULTS); + composer.compose(mindRoot, DEFAULTS); + + expect(warn).not.toHaveBeenCalled(); + // Three reads of the same unstructured log.md must produce ONE info line. + // The composer keeps a per-instance Set of already-warned paths. + expect(info).toHaveBeenCalledTimes(1); + expect(info.mock.calls[0][0]).not.toMatch(/^WorkingMemoryComposer:/); + }); + + it('opted-in + unstructured logs across DIFFERENT mind paths each fire info once on the same composer', () => { + // The dedupe is keyed by mindPath, not "any mind". Two different opted-in + // minds with unstructured logs must each get their own info line. + const otherMind = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-wmc-other-')); + const otherWmDir = path.join(otherMind, '.working-memory'); + fs.mkdirSync(otherWmDir, { recursive: true }); + try { + fs.writeFileSync(path.join(workingMemoryDir, 'log.md'), 'unstructured-a\n', 'utf-8'); + fs.writeFileSync(path.join(otherWmDir, 'log.md'), 'unstructured-b\n', 'utf-8'); + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + + composer.compose(mindRoot, DEFAULTS); + composer.compose(otherMind, DEFAULTS); + composer.compose(mindRoot, DEFAULTS); + composer.compose(otherMind, DEFAULTS); + + expect(warn).not.toHaveBeenCalled(); + expect(info).toHaveBeenCalledTimes(2); + } finally { + fs.rmSync(otherMind, { recursive: true, force: true }); + } + }); + + it('opted-out is the default for missing/incomplete config (defensive)', () => { + // If a caller forgets to thread enabled through, composer must not leak + // log section content. Pass a config object missing `enabled`. + fs.writeFileSync(path.join(workingMemoryDir, 'log.md'), 'unstructured\n', 'utf-8'); + fs.writeFileSync(path.join(workingMemoryDir, 'memory.md'), 'mem', 'utf-8'); + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + const noFlag = { lastKTurns: 10, perTurnMaxBytes: 2048, memoryMaxBytes: 8192 } as unknown as WorkingMemoryComposerConfig; + const out = composer.compose(mindRoot, noFlag); + expect(out).toBe('mem'); + expect(warn).not.toHaveBeenCalled(); + expect(info).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/services/src/chat/WorkingMemoryComposer.ts b/packages/services/src/chat/WorkingMemoryComposer.ts new file mode 100644 index 00000000..8454df02 --- /dev/null +++ b/packages/services/src/chat/WorkingMemoryComposer.ts @@ -0,0 +1,224 @@ +/** + * WorkingMemoryComposer — assembles the working-memory section of a mind's + * system prompt from `/.working-memory/{memory.md, rules.md, log.md}`. + * + * Phase 12 scope (locked by plan): + * - `memory.md` → full content, hard-capped at `memoryMaxBytes` (defense-in- + * depth; the consolidator already caps at write time). + * - `rules.md` → full content (small file, no cap). + * - `log.md` → only included when the file's first non-blank line is the + * `chamber-structured-log/v1` sentinel. The composer takes the last + * `lastKTurns` parsed turns and renders each, truncating any rendered + * turn that exceeds `perTurnMaxBytes`. Unstructured / missing logs + * contribute NOTHING and emit a warning (sentinel detection is owned by + * this composer, not by DailyLogWriter — a mind that never ran the + * writer must not leak its legacy log into the prompt). + * - `log.legacy.md` → never included. + * + * Section order: memory → rules → log. Sections are joined by the same + * `\n\n---\n\n` separator IdentityLoader uses for its top-level parts so + * the resulting string can be slotted into the system prompt as a single + * element without disturbing the existing layout. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { Logger } from '../logger'; +import { + parseLog, + type ParsedTurn, +} from '../mindMemory/StructuredLogFormat'; + +const WORKING_MEMORY_DIRNAME = '.working-memory'; +const SECTION_SEPARATOR = '\n\n---\n\n'; + +export interface WorkingMemoryComposerConfig { + /** + * Strict opt-in for the dream-daemon log section. When `true` (the user + * enabled consolidation in `.chamber.json` or via the agent profile UI), + * the composer reads `log.md`, validates the sentinel, and includes the + * last-K turns. When `false` (the default for new minds), the log section + * is omitted entirely — no read, no info, no warn. Silence is the + * contract: a freshly-genesis'd mind that hasn't opted in must not yield + * "log.md is unstructured" tray noise. + * + * Threaded through from `IdentityLoader.resolveComposerConfig`, which + * sources it from `loadChamberMindConfig(mindPath).workingMemory.consolidation.enabled`. + */ + readonly enabled: boolean; + /** Max number of structured turns to include from `log.md`. */ + readonly lastKTurns: number; + /** Max bytes per rendered turn frame; over-budget turns get a truncation marker. */ + readonly perTurnMaxBytes: number; + /** Hard cap on `memory.md` bytes (defense-in-depth over the consolidator). */ + readonly memoryMaxBytes: number; +} + +export interface ComposerLogger { + warn(message: string, ...args: unknown[]): void; + info(message: string, ...args: unknown[]): void; +} + +export interface WorkingMemoryComposerDeps { + readonly logger?: ComposerLogger; +} + +export interface WorkingMemoryComposer { + /** Build the working-memory section. Returns `''` when nothing applies. */ + compose(mindPath: string, config: WorkingMemoryComposerConfig): string; +} + +export function createWorkingMemoryComposer( + deps: WorkingMemoryComposerDeps = {}, +): WorkingMemoryComposer { + const log: ComposerLogger = deps.logger ?? Logger.create('WorkingMemoryComposer'); + + // Per-instance dedupe of the unstructured-log info line, keyed by mindPath. + // Lives on the closure so each composer instance gets fresh state — Uncle + // Bob's plan-review (finding 6) rejected module-scope state because tests + // would leak between cases. Two opted-in minds with unstructured logs will + // each get one info line; calling compose() three times for the same mind + // produces ONE info line. + const unstructuredWarned = new Set(); + + return { + compose(mindPath, config) { + const dir = path.join(mindPath, WORKING_MEMORY_DIRNAME); + if (!safeExists(dir)) return ''; + + const sections: string[] = []; + + const memory = readMemory(dir, config.memoryMaxBytes, log); + if (memory) sections.push(memory); + + const rules = readSimple(dir, 'rules.md'); + if (rules) sections.push(rules); + + // Strict opt-in gate. The log section is omitted entirely when the mind + // has not enabled dream-daemon consolidation. No read, no info, no warn + // — see the field doc on WorkingMemoryComposerConfig.enabled. + if (config.enabled === true) { + const logSection = readLog(mindPath, dir, config, log, unstructuredWarned); + if (logSection) sections.push(logSection); + } + + return sections.join(SECTION_SEPARATOR); + }, + }; +} + +function safeExists(p: string): boolean { + try { + return fs.existsSync(p); + } catch { + return false; + } +} + +function readSimple(dir: string, name: string): string { + const filePath = path.join(dir, name); + if (!safeExists(filePath)) return ''; + try { + return fs.readFileSync(filePath, 'utf-8').trim(); + } catch { + return ''; + } +} + +function readMemory(dir: string, maxBytes: number, log: ComposerLogger): string { + const raw = readSimple(dir, 'memory.md'); + if (!raw) return ''; + return truncateToBytes(raw, maxBytes, log, 'memory.md'); +} + +function readLog( + mindPath: string, + dir: string, + config: WorkingMemoryComposerConfig, + log: ComposerLogger, + unstructuredWarned: Set, +): string { + const filePath = path.join(dir, 'log.md'); + if (!safeExists(filePath)) return ''; + + let raw: string; + try { + raw = fs.readFileSync(filePath, 'utf-8'); + } catch (err) { + log.warn(`failed to read log.md; skipping log section`, err); + return ''; + } + + if (raw.trim().length === 0) return ''; + + const parsed = parseLog(raw); + if (!parsed.sentinel) { + // Migration-window log level: pre-existing minds may still hold an + // unstructured log.md until DailyLogWriter rotates it on the first turn. + // Use info (not warn) so SRE dashboards don't flag this benign state. + // Dedupe per-mindPath so we emit at most one line per process per mind. + if (!unstructuredWarned.has(mindPath)) { + unstructuredWarned.add(mindPath); + log.info( + `log.md is unstructured (no chamber-structured-log/v1 sentinel); skipping log section`, + ); + } + return ''; + } + + if (parsed.turns.length === 0) return ''; + + const k = Math.max(0, config.lastKTurns | 0); + if (k === 0) return ''; + + const tail = parsed.turns.slice(-k); + const rendered = tail.map((t) => truncateToBytes(renderTurn(t), config.perTurnMaxBytes, log, `turn ${t.turnId}`)); + return rendered.join('\n\n'); +} + +function renderTurn(turn: ParsedTurn): string { + return [ + `## ${turn.timestamp} turn:${turn.turnId} status:${turn.status}`, + `session: ${turn.sessionId}`, + `model: ${turn.model}`, + '', + '### user', + turn.prompt, + '', + '### assistant', + turn.assistant, + ].join('\n'); +} + +function truncateToBytes( + s: string, + maxBytes: number, + log: ComposerLogger, + label: string, +): string { + const originalBytes = Buffer.byteLength(s, 'utf-8'); + if (originalBytes <= maxBytes) return s; + + const originalKb = Math.max(1, Math.round(originalBytes / 1024)); + const marker = `\n[…truncated, originally ${originalKb} KB]`; + const markerBytes = Buffer.byteLength(marker, 'utf-8'); + + if (markerBytes >= maxBytes) { + log.warn( + `${label} exceeds ${maxBytes}B and the truncation marker alone (${markerBytes}B) does not fit; emitting marker only`, + ); + return marker.slice(0, maxBytes); + } + + const room = maxBytes - markerBytes; + let truncated = s; + while (Buffer.byteLength(truncated, 'utf-8') > room && truncated.length > 0) { + truncated = truncated.slice(0, -1); + } + + log.info( + `truncated ${label} from ${originalBytes}B to ${Buffer.byteLength(truncated + marker, 'utf-8')}B`, + ); + return truncated + marker; +} diff --git a/packages/services/src/chat/index.ts b/packages/services/src/chat/index.ts index c3571b1f..933dcf35 100644 --- a/packages/services/src/chat/index.ts +++ b/packages/services/src/chat/index.ts @@ -1,3 +1,8 @@ export { ChatService } from './ChatService'; export { IdentityLoader } from './IdentityLoader'; export { TurnQueue } from './TurnQueue'; +export { + createWorkingMemoryComposer, + type WorkingMemoryComposer, + type WorkingMemoryComposerConfig, +} from './WorkingMemoryComposer'; diff --git a/packages/services/src/genesis/MindScaffold.test.ts b/packages/services/src/genesis/MindScaffold.test.ts index cfd9d24c..dac43d4d 100644 --- a/packages/services/src/genesis/MindScaffold.test.ts +++ b/packages/services/src/genesis/MindScaffold.test.ts @@ -7,6 +7,7 @@ import * as os from 'os'; import { execSync } from 'child_process'; import type { CopilotClientFactory } from '../sdk/CopilotClientFactory'; import type { GitHubRegistryClient } from './GitHubRegistryClient'; +import { STRUCTURED_LOG_SENTINEL } from '../mindMemory/StructuredLogFormat'; describe('MindScaffold.slugify', () => { it('lowercases and replaces spaces with hyphens', () => { @@ -314,3 +315,320 @@ describe('MindScaffold constructor', () => { expect(scaffold).toBeDefined(); }); }); + +// The upgrade skill lives in ianphil/genesis-frontier@main as of 2026-04-24 +// (Epic #67). Calling against the legacy ianphil/genesis repo silently throws +// "Upgrade skill not found in genesis repo" and leaves new minds without the +// bootloader. The tests below lock the source coordinate so a future rename or +// typo is caught at the unit level. +describe('MindScaffold.bootstrapCapabilities — registry source', () => { + function makeFakeRegistryClient() { + const calls: { fetchTree: Array<[string, string, string]>; fetchJsonContent: Array<[string, string, string, string]> } = { + fetchTree: [], + fetchJsonContent: [], + }; + const tree = [ + { path: '.github/skills/upgrade/upgrade.js', type: 'blob', sha: 'sha-upgrade-js' }, + { path: '.github/skills/upgrade/skill.json', type: 'blob', sha: 'sha-upgrade-json' }, + ]; + const client = { + fetchTree: vi.fn(async (owner: string, repo: string, branch: string) => { + calls.fetchTree.push([owner, repo, branch]); + return tree; + }), + fetchBlob: vi.fn(async () => Buffer.from('// stub upgrade content', 'utf8')), + fetchJsonContent: vi.fn(async (owner: string, repo: string, file: string, ref: string) => { + calls.fetchJsonContent.push([owner, repo, file, ref]); + return { skills: { upgrade: { version: '1.0.0', description: 'stub' } } }; + }), + } as unknown as GitHubRegistryClient; + return { client, calls }; + } + + it('pullUpgradeSkill fetches the tree from ianphil/genesis-frontier@main', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-source-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + fs.mkdirSync(path.join(mindPath, '.github'), { recursive: true }); + fs.writeFileSync( + path.join(mindPath, '.github', 'registry.json'), + JSON.stringify({ version: '0.0.0', source: 'placeholder', channel: 'main', extensions: {}, skills: {}, prompts: {}, packages: [] }, null, 2), + ); + + const { client, calls } = makeFakeRegistryClient(); + const scaffold = new MindScaffold(client, {} as unknown as CopilotClientFactory); + + const internal = scaffold as unknown as { pullUpgradeSkill(mp: string): Promise }; + await internal.pullUpgradeSkill(mindPath); + + expect(calls.fetchTree).toHaveLength(1); + expect(calls.fetchTree[0]).toEqual(['ianphil', 'genesis-frontier', 'main']); + expect(calls.fetchJsonContent[0]?.slice(0, 3)).toEqual(['ianphil', 'genesis-frontier', '.github/registry.json']); + expect(calls.fetchJsonContent[0]?.[3]).toBe('main'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('seedRegistry writes source: ianphil/genesis-frontier', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-source-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + fs.mkdirSync(path.join(mindPath, '.github'), { recursive: true }); + + const { client } = makeFakeRegistryClient(); + const scaffold = new MindScaffold(client, {} as unknown as CopilotClientFactory); + + const internal = scaffold as unknown as { bootstrapCapabilities(mp: string): Promise }; + // bootstrapCapabilities will fail at the execSync step (upgrade.js exec), but + // by then seedRegistry has already written the registry.json. We only care + // about that file's contents here. + await internal.bootstrapCapabilities(mindPath).catch(() => { /* expected */ }); + + const reg = JSON.parse(fs.readFileSync(path.join(mindPath, '.github', 'registry.json'), 'utf8')); + expect(reg.source).toBe('ianphil/genesis-frontier'); + expect(reg.channel).toBe('main'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('pullUpgradeSkill error message names the searched owner/repo/branch when the skill is missing', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-source-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + fs.mkdirSync(path.join(mindPath, '.github'), { recursive: true }); + fs.writeFileSync( + path.join(mindPath, '.github', 'registry.json'), + JSON.stringify({ version: '0.0.0', source: 'placeholder', channel: 'main', extensions: {}, skills: {}, prompts: {}, packages: [] }, null, 2), + ); + + const emptyTreeClient = { + fetchTree: vi.fn(async () => [ + { path: '.github/skills/commit/commit.js', type: 'blob', sha: 'sha-commit' }, + ]), + fetchBlob: vi.fn(async () => Buffer.from('')), + fetchJsonContent: vi.fn(async () => ({})), + } as unknown as GitHubRegistryClient; + + const scaffold = new MindScaffold(emptyTreeClient, {} as unknown as CopilotClientFactory); + const internal = scaffold as unknown as { pullUpgradeSkill(mp: string): Promise }; + + await expect(internal.pullUpgradeSkill(mindPath)).rejects.toThrow(/ianphil\/genesis-frontier@main/); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); + +// v0.60.0 Phase 2: sentinel-seed becomes strict opt-in. The dream-daemon Switch +// in the Genesis wizard threads `enableDreamDaemon` through GenesisConfig → +// MindScaffold.createStructure. Opt-in seeds the sentinel exactly as before; +// opt-out leaves log.md as an empty placeholder so the WorkingMemoryComposer +// short-circuits cleanly (no read, no warn, no info — see Phase 1 enabled +// gate). The on-disk shape of a mind is owned by createStructure regardless +// of which Genesis path created the mind. +describe('MindScaffold.createStructure — log.md sentinel seed (opt-in)', () => { + it('opt-in (enableDreamDaemon=true): seeds log.md with the chamber-structured-log/v1 sentinel as its first non-blank line', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-sentinel-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + const scaffold = new MindScaffold( + {} as unknown as GitHubRegistryClient, + {} as unknown as CopilotClientFactory, + ); + + const internal = scaffold as unknown as { + createStructure(mp: string, opts: { enableDreamDaemon: boolean }): void; + }; + internal.createStructure(mindPath, { enableDreamDaemon: true }); + + const logPath = path.join(mindPath, '.working-memory', 'log.md'); + expect(fs.existsSync(logPath)).toBe(true); + const content = fs.readFileSync(logPath, 'utf-8'); + const firstNonBlank = content.split('\n').find((l) => l.trim() !== ''); + expect(firstNonBlank).toBe(STRUCTURED_LOG_SENTINEL); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('opt-in: leaves the seeded sentinel intact even though WORKING_MEMORY_FILES still iterates log.md', () => { + // Guard against accidental regression: if the WORKING_MEMORY_FILES loop + // ran AFTER the seed without the existsSync guard, log.md would be blanked. + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-sentinel-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + const scaffold = new MindScaffold( + {} as unknown as GitHubRegistryClient, + {} as unknown as CopilotClientFactory, + ); + + const internal = scaffold as unknown as { + createStructure(mp: string, opts: { enableDreamDaemon: boolean }): void; + }; + internal.createStructure(mindPath, { enableDreamDaemon: true }); + + const logPath = path.join(mindPath, '.working-memory', 'log.md'); + const content = fs.readFileSync(logPath, 'utf-8'); + expect(content.length).toBeGreaterThan(0); + expect(content).toContain(STRUCTURED_LOG_SENTINEL); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('opt-out (enableDreamDaemon=false): log.md exists but is empty (no sentinel)', () => { + // When the user does NOT opt in, the structured-log sentinel must NOT be + // written. Otherwise a never-opted-in mind would have a sentinel byte + // sitting on disk that would activate DailyLogWriter's structured path on + // the first turn — defeating the opt-in. The placeholder loop still + // creates log.md (so paths are valid) but its content is exactly empty. + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-sentinel-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + const scaffold = new MindScaffold( + {} as unknown as GitHubRegistryClient, + {} as unknown as CopilotClientFactory, + ); + + const internal = scaffold as unknown as { + createStructure(mp: string, opts: { enableDreamDaemon: boolean }): void; + }; + internal.createStructure(mindPath, { enableDreamDaemon: false }); + + const logPath = path.join(mindPath, '.working-memory', 'log.md'); + expect(fs.existsSync(logPath)).toBe(true); + const content = fs.readFileSync(logPath, 'utf-8'); + expect(content).toBe(''); + expect(content).not.toContain(STRUCTURED_LOG_SENTINEL); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('opt-out: omitting `enableDreamDaemon` defaults to OFF (defense-in-depth)', () => { + // The Genesis IPC schema forwards the field explicitly, but if a future + // refactor or a programmatic call drops the flag we must default to the + // safer (off) state. Strict opt-in: anything other than `true` means OFF. + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-sentinel-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + const scaffold = new MindScaffold( + {} as unknown as GitHubRegistryClient, + {} as unknown as CopilotClientFactory, + ); + + const internal = scaffold as unknown as { + createStructure(mp: string, opts?: { enableDreamDaemon?: boolean }): void; + }; + internal.createStructure(mindPath); + + const logPath = path.join(mindPath, '.working-memory', 'log.md'); + const content = fs.readFileSync(logPath, 'utf-8'); + expect(content).toBe(''); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); + +// v0.60.0 Phase 2: opt-in must persist the choice into `.chamber.json` so +// MindMemoryService.activateMind can read it back on the very next mind +// load. Opt-out is the default (no consolidation block) so existing minds +// upgrading into this release stay opted-out without a migration. We deep- +// merge in case future Genesis features write other fields. +describe('MindScaffold.create — `.chamber.json` consolidation block (opt-in)', () => { + it('opt-in: writes .chamber.json with workingMemory.consolidation.enabled=true', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-chamberjson-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + const scaffold = new MindScaffold( + {} as unknown as GitHubRegistryClient, + {} as unknown as CopilotClientFactory, + ); + + const internal = scaffold as unknown as { + createStructure(mp: string, opts: { enableDreamDaemon: boolean }): void; + }; + internal.createStructure(mindPath, { enableDreamDaemon: true }); + + const chamberJsonPath = path.join(mindPath, '.chamber.json'); + expect(fs.existsSync(chamberJsonPath)).toBe(true); + const parsed = JSON.parse(fs.readFileSync(chamberJsonPath, 'utf-8')) as { + workingMemory?: { consolidation?: { enabled?: unknown } }; + }; + expect(parsed.workingMemory?.consolidation?.enabled).toBe(true); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('opt-out: does NOT write .chamber.json (defaults are off → file is absent)', () => { + // No file = chamberMindConfig.loadChamberMindConfig returns the default + // shape with `consolidation.enabled: false`. Writing an empty marker file + // would be wasted I/O AND signal intent the user never expressed. + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-chamberjson-')); + try { + const mindPath = path.join(tmpDir, 'mind'); + const scaffold = new MindScaffold( + {} as unknown as GitHubRegistryClient, + {} as unknown as CopilotClientFactory, + ); + + const internal = scaffold as unknown as { + createStructure(mp: string, opts: { enableDreamDaemon: boolean }): void; + }; + internal.createStructure(mindPath, { enableDreamDaemon: false }); + + const chamberJsonPath = path.join(mindPath, '.chamber.json'); + expect(fs.existsSync(chamberJsonPath)).toBe(false); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); + +// Fix 2: the genesis prompt must not instruct the LLM to write to log.md any +// more — that file is reserved for structured CompletedTurn frames produced by +// DailyLogWriter. The "I am born" observation in genesis is deliberately +// dropped (recorded by the "Genesis" git commit and SOUL.md instead). +describe('MindScaffold.generateSoul — genesis prompt no longer references log.md', () => { + it('does not pass log.md as a write target to buildGenesisPrompt', async () => { + const session = { + send: vi.fn<(_: { prompt: string }) => Promise>(async () => undefined), + disconnect: vi.fn(async () => undefined), + on: vi.fn((event: string, callback: () => void) => { + if (event === 'session.idle') setTimeout(callback, 0); + return vi.fn(); + }), + rpc: { permissions: { setApproveAll: vi.fn(async () => ({ success: true })) } }, + }; + const client = { createSession: vi.fn(async () => session) }; + const clientFactory = { + createClient: vi.fn(async () => client), + destroyClient: vi.fn(async () => undefined), + } as unknown as CopilotClientFactory; + const scaffold = new MindScaffold( + {} as unknown as GitHubRegistryClient, + clientFactory, + ); + + const generateSoul = scaffold as unknown as { + generateSoul(mindPath: string, config: Parameters[0], slug: string): Promise; + }; + await generateSoul.generateSoul('/tmp/minds/seed', { + name: 'Seed', + role: 'reviewer', + voice: 'plain', + voiceDescription: 'plain', + basePath: '/tmp/minds', + }, 'seed'); + + const sentPrompt = session.send.mock.calls[0]?.[0]?.prompt ?? ''; + expect(sentPrompt).not.toContain('log.md'); + expect(sentPrompt).toContain('SOUL.md'); + expect(sentPrompt).toContain('memory.md'); + expect(sentPrompt).toContain('rules.md'); + }); +}); diff --git a/packages/services/src/genesis/MindScaffold.ts b/packages/services/src/genesis/MindScaffold.ts index 3ddf95e7..b4cac21f 100644 --- a/packages/services/src/genesis/MindScaffold.ts +++ b/packages/services/src/genesis/MindScaffold.ts @@ -10,13 +10,14 @@ import { approveForSessionCompat } from '../sdk/approveForSessionCompat'; import { getCurrentDateTimeContext, injectCurrentDateTimeContext } from '../chat/currentDateTimeContext'; import { buildGenesisPrompt } from './genesisPrompt'; import { GitHubRegistryClient } from './GitHubRegistryClient'; +import { STRUCTURED_LOG_SENTINEL } from '../mindMemory/StructuredLogFormat'; const log = Logger.create('MindScaffold'); const IDEA_FOLDERS = ['inbox', 'domains', 'expertise', 'initiatives', 'Archive']; const WORKING_MEMORY_FILES = ['memory.md', 'rules.md', 'log.md']; -const GENESIS_SOURCE = 'ianphil/genesis'; +const GENESIS_SOURCE = 'ianphil/genesis-frontier'; const GENESIS_CHANNEL = 'main'; const CHAMBER_GITIGNORE_ENTRIES = ['runs/', 'cron-runs.json', 'cron-runs.json.migrated-*'] as const; const CHAMBER_GITIGNORE_CONTENT = `${CHAMBER_GITIGNORE_ENTRIES.join('\n')}\n`; @@ -27,6 +28,16 @@ export interface GenesisConfig { voice: string; voiceDescription: string; basePath: string; + /** + * Strict opt-in for the dream daemon (v0.60.0). When `true`, MindScaffold + * seeds the chamber-structured-log/v1 sentinel into log.md AND writes + * `.chamber.json` with `workingMemory.consolidation.enabled: true` so + * MindMemoryService.activateMind starts the daemon on the first mind load. + * When `false` or omitted, log.md is left empty and `.chamber.json` is + * not written — the mind operates without consolidation, matching the + * default for every existing user upgrading into this release. + */ + enableDreamDaemon?: boolean; } export interface GenesisProgress { @@ -87,7 +98,7 @@ export class MindScaffold { // 1. Create deterministic structure this.emit('structure', 'Creating mind structure...'); - this.createStructure(mindPath); + this.createStructure(mindPath, { enableDreamDaemon: config.enableDreamDaemon === true }); // 2. Generate soul via agent this.emit('soul', `Writing SOUL.md...`); @@ -117,7 +128,10 @@ export class MindScaffold { return mindPath; } - private createStructure(mindPath: string): void { + private createStructure( + mindPath: string, + opts: { enableDreamDaemon?: boolean } = {}, + ): void { // IDEA folders for (const folder of IDEA_FOLDERS) { fs.mkdirSync(path.join(mindPath, folder), { recursive: true }); @@ -131,6 +145,19 @@ export class MindScaffold { const wmDir = path.join(mindPath, '.working-memory'); fs.mkdirSync(wmDir, { recursive: true }); + const enableDreamDaemon = opts.enableDreamDaemon === true; + + // v0.60.0 Phase 2: log.md sentinel seed is now strict opt-in. When the + // user toggles the dream-daemon Switch in Genesis we seed the sentinel + // exactly as before (byte-for-byte parity with DailyLogWriter.seedFreshLog + // — `SENTINEL + '\n\n'`). When the user opts out (default), log.md is + // left empty by the WORKING_MEMORY_FILES placeholder loop below — no + // sentinel byte means DailyLogWriter never engages even if a future bug + // somehow registers a writer for this mind. + if (enableDreamDaemon) { + fs.writeFileSync(path.join(wmDir, 'log.md'), STRUCTURED_LOG_SENTINEL + '\n\n'); + } + // Create placeholder files so the agent has targets for (const file of WORKING_MEMORY_FILES) { const filePath = path.join(wmDir, file); @@ -138,6 +165,22 @@ export class MindScaffold { fs.writeFileSync(filePath, ''); } } + + // v0.60.0 Phase 2: persist the opt-in choice into `.chamber.json` so + // MindMemoryService.activateMind reads it back on the very next mind + // load. We only WRITE this file on opt-in — opt-out is the default + // shape returned by chamberMindConfig when the file is absent, so an + // empty marker file would be wasted I/O AND signal intent the user + // never expressed. + if (enableDreamDaemon) { + const chamberJsonPath = path.join(mindPath, '.chamber.json'); + const chamberConfig = { + workingMemory: { + consolidation: { enabled: true }, + }, + }; + fs.writeFileSync(chamberJsonPath, JSON.stringify(chamberConfig, null, 2) + '\n'); + } } private async generateSoul(mindPath: string, config: GenesisConfig, slug: string): Promise { @@ -147,14 +190,16 @@ export class MindScaffold { const agentPath = path.join(mindPath, '.github', 'agents', `${slug}.agent.md`); const memoryPath = path.join(mindPath, '.working-memory', 'memory.md'); const rulesPath = path.join(mindPath, '.working-memory', 'rules.md'); - const logPath = path.join(mindPath, '.working-memory', 'log.md'); const indexPath = path.join(mindPath, 'mind-index.md'); + // log.md is intentionally NOT a prompt target. It is pre-seeded with the + // chamber-structured-log/v1 sentinel by createStructure() and reserved for + // structured CompletedTurn frames written by DailyLogWriter. const prompt = buildGenesisPrompt({ name: config.name, role: config.role, voiceDescription: config.voiceDescription, - paths: { soul: soulPath, agent: agentPath, memory: memoryPath, rules: rulesPath, log: logPath, index: indexPath }, + paths: { soul: soulPath, agent: agentPath, memory: memoryPath, rules: rulesPath, index: indexPath }, }); const sessionConfig: Record = { @@ -309,7 +354,7 @@ export class MindScaffold { } if (upgradeFiles.length === 0) { - throw new Error('Upgrade skill not found in genesis repo'); + throw new Error(`Upgrade skill not found in ${GENESIS_SOURCE}@${GENESIS_CHANNEL}`); } // Download and write each file diff --git a/packages/services/src/genesis/genesisPrompt.test.ts b/packages/services/src/genesis/genesisPrompt.test.ts index 18dc3f48..6183afab 100644 --- a/packages/services/src/genesis/genesisPrompt.test.ts +++ b/packages/services/src/genesis/genesisPrompt.test.ts @@ -11,7 +11,6 @@ describe('buildGenesisPrompt', () => { agent: '/test/.github/agents/test.agent.md', memory: '/test/.working-memory/memory.md', rules: '/test/.working-memory/rules.md', - log: '/test/.working-memory/log.md', index: '/test/mind-index.md', }, }; @@ -26,13 +25,34 @@ describe('buildGenesisPrompt', () => { expect(prompt).toContain('calm and precise'); }); - it('includes all six file paths', () => { + it('includes the five user-visible identity file paths', () => { const prompt = buildGenesisPrompt(input); expect(prompt).toContain('SOUL.md'); expect(prompt).toContain('memory.md'); expect(prompt).toContain('rules.md'); - expect(prompt).toContain('log.md'); expect(prompt).toContain('mind-index.md'); expect(prompt).toContain('.agent.md'); }); + + // log.md is reserved for structured CompletedTurn frames written by + // DailyLogWriter. The genesis prompt must not instruct the LLM to write + // there or it poisons the chamber-structured-log/v1 contract before the + // first turn ever runs. + it('does not instruct the LLM to write to log.md', () => { + expect(buildGenesisPrompt(input)).not.toContain('log.md'); + }); + + it('still accepts an input with paths.log set for backward compatibility', () => { + const withLog = { + ...input, + paths: { ...input.paths, log: '/test/.working-memory/log.md' }, + }; + expect(() => buildGenesisPrompt(withLog)).not.toThrow(); + expect(buildGenesisPrompt(withLog)).not.toContain('log.md'); + }); + + it('accepts an input that omits paths.log entirely', () => { + expect(() => buildGenesisPrompt(input)).not.toThrow(); + expect(buildGenesisPrompt(input)).not.toContain('log.md'); + }); }); diff --git a/packages/services/src/genesis/genesisPrompt.ts b/packages/services/src/genesis/genesisPrompt.ts index 79d7b072..3aef19f4 100644 --- a/packages/services/src/genesis/genesisPrompt.ts +++ b/packages/services/src/genesis/genesisPrompt.ts @@ -7,7 +7,12 @@ export interface GenesisPromptInput { agent: string; memory: string; rules: string; - log: string; + /** + * @deprecated log.md is reserved for structured CompletedTurn frames + * written by DailyLogWriter. The genesis prompt no longer references this + * path. Kept optional for backward-compatible call-site shape only. + */ + log?: string; index: string; }; } @@ -45,10 +50,11 @@ Write to: ${paths.soul} [One paragraph on how you communicate. Your tone, your style, your energy.] ## Continuity -You maintain memory across sessions through three files: +You maintain memory across sessions through these working-memory files: - \`.working-memory/memory.md\` — curated long-term reference - \`.working-memory/rules.md\` — operational rules learned from experience -- \`.working-memory/log.md\` — raw chronological observations + +Your turn-by-turn history is preserved automatically; you do not write to it. --- Write to: ${paths.agent} @@ -76,12 +82,6 @@ Write to: ${paths.rules} [One starter rule that fits your character voice.] --- -Write to: ${paths.log} ---- -# Log -- ${new Date().toISOString()}: Genesis. I am ${name}. My purpose is ${role}. Let's begin. ---- - Write to: ${paths.index} --- # Mind Index @@ -93,8 +93,7 @@ Write to: ${paths.index} ## Working Memory - \`.working-memory/memory.md\` — curated long-term reference - \`.working-memory/rules.md\` — operational rules -- \`.working-memory/log.md\` — chronological observations --- -Write all six files now.`; +Write all five files now.`; } diff --git a/packages/services/src/index.ts b/packages/services/src/index.ts index 11cd212d..a8581cca 100644 --- a/packages/services/src/index.ts +++ b/packages/services/src/index.ts @@ -14,6 +14,7 @@ export * from './genesis'; export * from './lens'; export * from './ledger'; export * from './mind'; +export * from './mindMemory'; export * from './mindProfile'; export * from './ports'; export * from './sdk'; diff --git a/packages/services/src/mind/MindManager.test.ts b/packages/services/src/mind/MindManager.test.ts index 1fc720ee..fc5f43f0 100644 --- a/packages/services/src/mind/MindManager.test.ts +++ b/packages/services/src/mind/MindManager.test.ts @@ -7,7 +7,7 @@ import type { IdentityLoader } from '../chat/IdentityLoader'; import type { ChamberToolProvider } from '../chamberTools'; import type { ConfigService } from '../config/ConfigService'; import type { ViewDiscovery } from '../lens/ViewDiscovery'; -import type { AppConfig, LensViewManifest } from '@chamber/shared/types'; +import type { AppConfig, LensViewManifest, MindContext } from '@chamber/shared/types'; import { MindScaffold } from '../genesis/MindScaffold'; import type { ManagedSkillSyncResult } from '../skills'; @@ -19,6 +19,8 @@ vi.mock('fs', () => ({ readdirSync: vi.fn(() => []), readFileSync: vi.fn(), writeFileSync: vi.fn(), + renameSync: vi.fn(), + rmSync: vi.fn(), realpathSync: Object.assign(vi.fn((candidate: string) => candidate), { native: vi.fn((candidate: string) => candidate), }), @@ -28,8 +30,26 @@ vi.mock('../lens/MindBootstrap', () => ({ bootstrapMindCapabilities: vi.fn(), })); +vi.mock('./chamberMindConfig', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + patchChamberMindConfig: vi.fn(), + }; +}); + +vi.mock('../mindMemory/rollback', () => ({ + rollbackToUnstructured: vi.fn().mockResolvedValue({ + framesConverted: 0, + legacyExisted: false, + outcome: 'no-op-missing', + }), +})); + import * as fs from 'fs'; import { bootstrapMindCapabilities } from '../lens/MindBootstrap'; +import { patchChamberMindConfig } from './chamberMindConfig'; +import { rollbackToUnstructured } from '../mindMemory/rollback'; const mockStart = vi.fn(); const mockStop = vi.fn(); @@ -458,6 +478,7 @@ describe('MindManager', () => { mockViewDiscovery as unknown as ViewDiscovery, () => null, () => undefined, + () => true, managedSkillService, ); @@ -1768,6 +1789,209 @@ describe('MindManager', () => { }); }); + describe('enableDreamDaemon / disableDreamDaemon', () => { + beforeEach(() => { + vi.mocked(patchChamberMindConfig).mockReset(); + vi.mocked(rollbackToUnstructured).mockReset(); + vi.mocked(rollbackToUnstructured).mockResolvedValue({ + framesConverted: 0, + legacyExisted: false, + outcome: 'no-op-missing', + }); + }); + + it('enableDreamDaemon patches .chamber.json with consolidation.enabled=true and reloads the mind', async () => { + const mind = await manager.loadMind('/tmp/agents/q'); + const reloadSpy = vi.spyOn(manager, 'reloadMind'); + + await manager.enableDreamDaemon(mind.mindId); + + expect(patchChamberMindConfig).toHaveBeenCalledWith('/tmp/agents/q', { + workingMemory: { consolidation: { enabled: true } }, + }); + expect(reloadSpy).toHaveBeenCalledWith(mind.mindId); + }); + + it('disableDreamDaemon patches .chamber.json with consolidation.enabled=false and reloads the mind', async () => { + const mind = await manager.loadMind('/tmp/agents/q'); + const reloadSpy = vi.spyOn(manager, 'reloadMind'); + + await manager.disableDreamDaemon(mind.mindId); + + expect(patchChamberMindConfig).toHaveBeenCalledWith('/tmp/agents/q', { + workingMemory: { consolidation: { enabled: false } }, + }); + expect(reloadSpy).toHaveBeenCalledWith(mind.mindId); + }); + + it('enableDreamDaemon throws when the mind is not loaded', async () => { + await expect(manager.enableDreamDaemon('does-not-exist')).rejects.toThrow(/not found/); + expect(patchChamberMindConfig).not.toHaveBeenCalled(); + }); + + it('disableDreamDaemon throws when the mind is not loaded', async () => { + await expect(manager.disableDreamDaemon('does-not-exist')).rejects.toThrow(/not found/); + expect(patchChamberMindConfig).not.toHaveBeenCalled(); + }); + + it('enableDreamDaemon resolves to the reloaded MindContext', async () => { + const mind = await manager.loadMind('/tmp/agents/q'); + const result = await manager.enableDreamDaemon(mind.mindId); + expect(result.mindId).toBe(mind.mindId); + expect(result.mindPath).toBe('/tmp/agents/q'); + }); + + it('serializes concurrent toggle calls for the same mindId', async () => { + const mind = await manager.loadMind('/tmp/agents/q'); + + // Hold reloadMind open so the second call observes an in-flight toggle. + let releaseReload: (() => void) | undefined; + const reloadGate = new Promise((resolve) => { releaseReload = resolve; }); + const reloadSpy = vi.spyOn(manager, 'reloadMind').mockImplementation(async () => { + await reloadGate; + // Return a synthetic context that mirrors the mind we created so + // assertions on the resolved value remain meaningful. + return { ...mind } as MindContext; + }); + + const first = manager.enableDreamDaemon(mind.mindId); + const second = manager.enableDreamDaemon(mind.mindId); + + // Same in-flight promise must be returned to the second caller, so + // patchChamberMindConfig fires exactly once for the pair. + expect(second).toBe(first); + + releaseReload!(); + const [a, b] = await Promise.all([first, second]); + expect(a).toBe(b); + expect(reloadSpy).toHaveBeenCalledTimes(1); + expect(patchChamberMindConfig).toHaveBeenCalledTimes(1); + }); + + it('disableDreamDaemon calls rollbackToUnstructured AFTER patch + reload', async () => { + const mind = await manager.loadMind('/tmp/agents/q'); + const callOrder: string[] = []; + vi.mocked(patchChamberMindConfig).mockImplementation(() => { callOrder.push('patch'); }); + const reloadSpy = vi.spyOn(manager, 'reloadMind').mockImplementation(async () => { + callOrder.push('reload'); + return { ...mind } as MindContext; + }); + vi.mocked(rollbackToUnstructured).mockImplementation(async () => { + callOrder.push('rollback'); + return { framesConverted: 2, legacyExisted: false, outcome: 'rolled-back' }; + }); + + await manager.disableDreamDaemon(mind.mindId); + + expect(callOrder).toEqual(['patch', 'reload', 'rollback']); + expect(rollbackToUnstructured).toHaveBeenCalledWith('/tmp/agents/q'); + expect(reloadSpy).toHaveBeenCalledWith(mind.mindId); + }); + + it('enableDreamDaemon does NOT call rollbackToUnstructured', async () => { + const mind = await manager.loadMind('/tmp/agents/q'); + await manager.enableDreamDaemon(mind.mindId); + expect(rollbackToUnstructured).not.toHaveBeenCalled(); + }); + + it('disableDreamDaemon resolves to the reloaded MindContext even when rollback fails', async () => { + const mind = await manager.loadMind('/tmp/agents/q'); + vi.mocked(rollbackToUnstructured).mockRejectedValueOnce(new Error('rollback boom')); + + const result = await manager.disableDreamDaemon(mind.mindId); + + // Toggle is non-fatal: config has been flipped + reload completed. + // A failed rollback only logs a warning; the user-visible toggle + // succeeds so the UI state matches the on-disk config. + expect(result.mindId).toBe(mind.mindId); + expect(patchChamberMindConfig).toHaveBeenCalledWith('/tmp/agents/q', { + workingMemory: { consolidation: { enabled: false } }, + }); + }); + + describe('feature-flag gate (dreamDaemonFeatureEnabled)', () => { + // Defense-in-depth: the IPC layer is the first line of defense, but + // any internal caller (test harness, data migration, future helper) + // must also be gated. Constructing a manager with `() => false` + // exercises the gate inside `doToggleDreamDaemon` directly. + const buildGatedManager = () => { + const mgr = new MindManager( + mockClientFactory as unknown as CopilotClientFactory, + mockIdentityLoader as unknown as IdentityLoader, + mockConfigService as unknown as ConfigService, + mockViewDiscovery as unknown as ViewDiscovery, + undefined, + undefined, + () => false, + ); + mgr.setProviders([mockProvider as unknown as ChamberToolProvider]); + return mgr; + }; + + it('enableDreamDaemon throws when the feature accessor returns false', async () => { + const mgr = buildGatedManager(); + const mind = await mgr.loadMind('/tmp/agents/q'); + + await expect(mgr.enableDreamDaemon(mind.mindId)).rejects.toThrow( + /Dream Daemon is not available in this build/, + ); + expect(patchChamberMindConfig).not.toHaveBeenCalled(); + }); + + it('sequential enableDreamDaemon calls both reject when the feature is off', async () => { + // Regression guard for the daemonToggling in-flight map: if a stale + // allow ever leaked through, the SECOND call could silently resolve + // without going through doToggleDreamDaemon. We need both to throw. + const mgr = buildGatedManager(); + const mind = await mgr.loadMind('/tmp/agents/q'); + + await expect(mgr.enableDreamDaemon(mind.mindId)).rejects.toThrow( + /Dream Daemon is not available in this build/, + ); + await expect(mgr.enableDreamDaemon(mind.mindId)).rejects.toThrow( + /Dream Daemon is not available in this build/, + ); + expect(patchChamberMindConfig).not.toHaveBeenCalled(); + }); + + it('disableDreamDaemon is allowed even when the feature accessor returns false', async () => { + // A stable build that picks up a mind enabled under insiders must + // still be able to clean up the persisted opt-in. Only `enable` is + // gated; `disable` is always permitted. + const mgr = buildGatedManager(); + const mind = await mgr.loadMind('/tmp/agents/q'); + + await mgr.disableDreamDaemon(mind.mindId); + + expect(patchChamberMindConfig).toHaveBeenCalledWith('/tmp/agents/q', { + workingMemory: { consolidation: { enabled: false } }, + }); + }); + + it('enableDreamDaemon proceeds when the feature accessor returns true', async () => { + // Sanity check: explicit `() => true` matches the default-on + // behavior the rest of the test file relies on. + const mgr = new MindManager( + mockClientFactory as unknown as CopilotClientFactory, + mockIdentityLoader as unknown as IdentityLoader, + mockConfigService as unknown as ConfigService, + mockViewDiscovery as unknown as ViewDiscovery, + undefined, + undefined, + () => true, + ); + mgr.setProviders([mockProvider as unknown as ChamberToolProvider]); + const mind = await mgr.loadMind('/tmp/agents/q'); + + await mgr.enableDreamDaemon(mind.mindId); + + expect(patchChamberMindConfig).toHaveBeenCalledWith('/tmp/agents/q', { + workingMemory: { consolidation: { enabled: true } }, + }); + }); + }); + }); + describe('BYO LLM provider config integration (SDK-native)', () => { it('BVT-MM01: passes provider into createSession only when a BYO model is selected', async () => { const provider = { type: 'openai' as const, baseUrl: 'https://example.com/v1', apiKey: 'lm-studio' }; diff --git a/packages/services/src/mind/MindManager.ts b/packages/services/src/mind/MindManager.ts index 4a009844..d3eb7af1 100644 --- a/packages/services/src/mind/MindManager.ts +++ b/packages/services/src/mind/MindManager.ts @@ -13,7 +13,8 @@ import { Logger } from '../logger'; import type { InternalMindContext, CopilotClient, CopilotSession, Tool, UserInputHandler } from './types'; import { generateMindId } from './generateMindId'; import { loadMcpServersFromMindPath } from './mcpConfig'; -import { loadChamberMindConfig } from './chamberMindConfig'; +import { loadChamberMindConfig, patchChamberMindConfig } from './chamberMindConfig'; +import { rollbackToUnstructured } from '../mindMemory/rollback'; import type { CopilotClientFactory } from '../sdk/CopilotClientFactory'; import { approveForSessionCompat } from '../sdk/approveForSessionCompat'; import type { IdentityLoader } from '../chat/IdentityLoader'; @@ -97,6 +98,13 @@ export class MindManager extends EventEmitter { private reloading = false; private providers: ChamberToolProvider[] = []; private modelUpdates = new Map>(); + // Per-mindId serialization for dream-daemon toggle. Two concurrent + // calls (typical: rapid clicks bypassing the UI's `togglingDreamDaemon` + // guard, or a programmatic IPC caller) must not race `reloadMind` — + // the second `this.minds.get(mindId)` would return undefined while the + // first call is mid-reload (delete-then-loadMind). Same shape as the + // `loading` Map: in-flight promises are returned to subsequent callers. + private daemonToggling = new Map>(); constructor( private readonly clientFactory: CopilotClientFactory, @@ -124,6 +132,18 @@ export class MindManager extends EventEmitter { * the SDK rejects createSession({provider}) without a model argument. */ private readonly byoDefaultModelProvider: () => string | undefined = () => undefined, + /** + * Returns the current value of the app-level `dreamDaemon` feature flag. + * Defense-in-depth gate for `enableDreamDaemon`: when this returns false, + * a call to `enableDreamDaemon(mindId)` throws "Dream Daemon is not + * available in this build" rather than patching `.chamber.json` and + * reloading the mind. `disableDreamDaemon` is intentionally NOT gated + * (a stable build must still be able to clean up the persisted opt-in + * for a mind that was opted-in under an insiders build). Defaults to + * always-on so the services package stays decoupled from app-shell types + * and existing test fixtures continue to work without modification. + */ + private readonly dreamDaemonFeatureEnabled: () => boolean = () => true, private readonly managedSkillService?: Pick, ) { super(); @@ -395,6 +415,73 @@ export class MindManager extends EventEmitter { return reloaded; } + // Flip the dream-daemon opt-in for an existing mind. Patches `.chamber.json` + // to set `workingMemory.consolidation.enabled = true`, then reloads the + // mind so providers (notably MindMemoryService) re-read the opt-in gate + // and `migrateIfNeeded` runs against any pre-existing unstructured `log.md`. + // Per-mindId serialization via `daemonToggling` ensures concurrent calls + // for the same mind return the same in-flight promise rather than racing + // (the second call would otherwise hit a stale `this.minds.get` mid-reload). + enableDreamDaemon(mindId: string): Promise { + return this.toggleDreamDaemon(mindId, true); + } + + // Counterpart to `enableDreamDaemon`. In Phase 3 this only flips the flag + // and reloads — it does NOT roll back structured frames in `log.md` back + // to unstructured markdown. That rollback path is the subject of Phase 4. + disableDreamDaemon(mindId: string): Promise { + return this.toggleDreamDaemon(mindId, false); + } + + private toggleDreamDaemon(mindId: string, enabled: boolean): Promise { + const inflight = this.daemonToggling.get(mindId); + if (inflight) return inflight; + const promise = this.doToggleDreamDaemon(mindId, enabled); + this.daemonToggling.set(mindId, promise); + promise.finally(() => { + // Only clear if still the same promise — guards against a race where + // the entry was somehow replaced (defensive; not currently possible). + if (this.daemonToggling.get(mindId) === promise) this.daemonToggling.delete(mindId); + }).catch(() => { /* swallowed — caller still receives the rejection */ }); + return promise; + } + + private async doToggleDreamDaemon(mindId: string, enabled: boolean): Promise { + // Defense-in-depth: the IPC layer already rejects `enable` calls when the + // app-level feature flag is off, but a future internal caller (data + // migration, test harness reaching past IPC) must not be able to flip the + // opt-in either. `disable` is intentionally permitted regardless so the + // service still has a path to clean up persisted opt-in state for minds + // that were enabled under an insiders build. + if (enabled && !this.dreamDaemonFeatureEnabled()) { + throw new Error('Dream Daemon is not available in this build'); + } + const context = this.minds.get(mindId); + if (!context) throw new Error(`Mind ${mindId} not found`); + const mindPath = context.mindPath; + patchChamberMindConfig(mindPath, { + workingMemory: { consolidation: { enabled } }, + }); + const reloaded = await this.reloadMind(mindId); + + if (!enabled) { + // Phase 4 — at this point the mind has been reloaded with the + // opted-out config: MindMemoryService skipped activation, so the + // DailyLogWriter is gone and no observer is attached. Safe to + // rewrite log.md without racing in-flight structured writes. + // Failure is non-fatal to the toggle: the config is already + // flipped, the next app launch (or another rollback attempt) + // can retry. Surfacing as a warning keeps the user-visible + // toggle from breaking on a transient fs error. + try { + await rollbackToUnstructured(mindPath); + } catch (err) { + log.warn(`disableDreamDaemon: rollback failed for ${mindId}`, err); + } + } + return reloaded; + } + listMinds(): MindContext[] { return Array.from(this.minds.values()).map(m => this.toExternalContext(m)); } diff --git a/packages/services/src/mind/chamberMindConfig.test.ts b/packages/services/src/mind/chamberMindConfig.test.ts index a351de8c..86785db0 100644 --- a/packages/services/src/mind/chamberMindConfig.test.ts +++ b/packages/services/src/mind/chamberMindConfig.test.ts @@ -2,7 +2,16 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { loadChamberMindConfig, CHAMBER_MIND_CONFIG_FILENAME } from './chamberMindConfig'; +import { + loadChamberMindConfig, + patchChamberMindConfig, + CHAMBER_MIND_CONFIG_FILENAME, + DEFAULT_WORKING_MEMORY_CONSOLIDATION, +} from './chamberMindConfig'; + +const defaultWorkingMemory = () => ({ + consolidation: { ...DEFAULT_WORKING_MEMORY_CONSOLIDATION }, +}); describe('loadChamberMindConfig', () => { let tmpDir: string; @@ -15,8 +24,10 @@ describe('loadChamberMindConfig', () => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it('returns an empty config when no .chamber.json file is present', () => { - expect(loadChamberMindConfig(tmpDir)).toEqual({}); + it('returns workingMemory defaults when no .chamber.json file is present', () => { + expect(loadChamberMindConfig(tmpDir)).toEqual({ + workingMemory: defaultWorkingMemory(), + }); }); it('reads excludedTools when present', () => { @@ -26,6 +37,7 @@ describe('loadChamberMindConfig', () => { ); expect(loadChamberMindConfig(tmpDir)).toEqual({ excludedTools: ['shell', 'str_replace'], + workingMemory: defaultWorkingMemory(), }); }); @@ -34,13 +46,17 @@ describe('loadChamberMindConfig', () => { path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), JSON.stringify({ excludedTools: [] }), ); - expect(loadChamberMindConfig(tmpDir)).toEqual({}); + expect(loadChamberMindConfig(tmpDir)).toEqual({ + workingMemory: defaultWorkingMemory(), + }); }); - it('returns empty config for invalid JSON without throwing', () => { + it('returns defaults for invalid JSON without throwing', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); fs.writeFileSync(path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), '{ not valid json'); - expect(loadChamberMindConfig(tmpDir)).toEqual({}); + expect(loadChamberMindConfig(tmpDir)).toEqual({ + workingMemory: defaultWorkingMemory(), + }); warn.mockRestore(); }); @@ -50,7 +66,9 @@ describe('loadChamberMindConfig', () => { path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), JSON.stringify({ excludedTools: [1, 2, 3] }), ); - expect(loadChamberMindConfig(tmpDir)).toEqual({}); + expect(loadChamberMindConfig(tmpDir)).toEqual({ + workingMemory: defaultWorkingMemory(), + }); warn.mockRestore(); }); @@ -64,6 +82,314 @@ describe('loadChamberMindConfig', () => { ); expect(loadChamberMindConfig(tmpDir)).toEqual({ excludedTools: ['shell'], + workingMemory: defaultWorkingMemory(), + }); + }); +}); + +describe('chamberMindConfig — workingMemory.consolidation', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mind-config-wm-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function writeConfig(value: unknown): void { + fs.writeFileSync(path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), JSON.stringify(value)); + } + + describe('defaults', () => { + it('parsing an empty config yields the consolidation defaults', () => { + writeConfig({}); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation.enabled).toBe(false); + expect(cfg.workingMemory.consolidation.cron).toBe('0 3 * * *'); + expect(cfg.workingMemory.consolidation.lastKTurns).toBe(10); + expect(cfg.workingMemory.consolidation.perTurnMaxBytes).toBe(2048); + expect(cfg.workingMemory.consolidation.memoryMaxBytes).toBe(8192); + }); + + it('exposes the defaults as a frozen constant for downstream consumers', () => { + expect(DEFAULT_WORKING_MEMORY_CONSOLIDATION).toEqual({ + enabled: false, + cron: '0 3 * * *', + lastKTurns: 10, + perTurnMaxBytes: 2048, + memoryMaxBytes: 8192, + }); + }); + }); + + describe('override', () => { + it('returns user-provided values verbatim when all five fields are set', () => { + writeConfig({ + workingMemory: { + consolidation: { + enabled: true, + cron: '*/15 * * * *', + lastKTurns: 25, + perTurnMaxBytes: 4096, + memoryMaxBytes: 16384, + }, + }, + }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation).toEqual({ + enabled: true, + cron: '*/15 * * * *', + lastKTurns: 25, + perTurnMaxBytes: 4096, + memoryMaxBytes: 16384, + }); + }); + + it('honors the opt-in path when only enabled: true is set', () => { + writeConfig({ workingMemory: { consolidation: { enabled: true } } }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation.enabled).toBe(true); + expect(cfg.workingMemory.consolidation.cron).toBe('0 3 * * *'); + expect(cfg.workingMemory.consolidation.lastKTurns).toBe(10); + expect(cfg.workingMemory.consolidation.perTurnMaxBytes).toBe(2048); + expect(cfg.workingMemory.consolidation.memoryMaxBytes).toBe(8192); + }); + }); + + describe('invalid → warn + ignore', () => { + let warn: ReturnType; + + beforeEach(() => { + warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warn.mockRestore(); + }); + + function expectWarnedAbout(field: string): void { + const matched = warn.mock.calls.some((call: unknown[]) => + call.some((arg: unknown) => typeof arg === 'string' && arg.includes(`workingMemory.consolidation.${field}`)), + ); + expect(matched, `expected a warning mentioning workingMemory.consolidation.${field}`).toBe(true); + } + + it('falls back to default false when enabled is a string', () => { + writeConfig({ workingMemory: { consolidation: { enabled: 'yes' } } }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation.enabled).toBe(false); + expectWarnedAbout('enabled'); + }); + + it('falls back to default cron when cron is a number', () => { + writeConfig({ workingMemory: { consolidation: { cron: 42 } } }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation.cron).toBe('0 3 * * *'); + expectWarnedAbout('cron'); + }); + + it('falls back to default lastKTurns when value is negative', () => { + writeConfig({ workingMemory: { consolidation: { lastKTurns: -5 } } }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation.lastKTurns).toBe(10); + expectWarnedAbout('lastKTurns'); + }); + + it('falls back to default memoryMaxBytes when value is zero', () => { + writeConfig({ workingMemory: { consolidation: { memoryMaxBytes: 0 } } }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation.memoryMaxBytes).toBe(8192); + expectWarnedAbout('memoryMaxBytes'); + }); + + it('falls back to default perTurnMaxBytes when value is a non-integer float', () => { + writeConfig({ workingMemory: { consolidation: { perTurnMaxBytes: 12.5 } } }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation.perTurnMaxBytes).toBe(2048); + expectWarnedAbout('perTurnMaxBytes'); + }); + + it('keeps valid sibling fields when one field is invalid', () => { + writeConfig({ + workingMemory: { + consolidation: { + enabled: true, + cron: 42, + lastKTurns: 20, + }, + }, + }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation.enabled).toBe(true); + expect(cfg.workingMemory.consolidation.cron).toBe('0 3 * * *'); + expect(cfg.workingMemory.consolidation.lastKTurns).toBe(20); + expectWarnedAbout('cron'); + }); + + it('falls back to defaults when workingMemory.consolidation is not an object', () => { + writeConfig({ workingMemory: { consolidation: 'nope' } }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation).toEqual({ ...DEFAULT_WORKING_MEMORY_CONSOLIDATION }); + const matched = warn.mock.calls.some((call: unknown[]) => + call.some((arg: unknown) => typeof arg === 'string' && arg.includes('workingMemory.consolidation')), + ); + expect(matched).toBe(true); + }); + + it('falls back to defaults when workingMemory itself is not an object', () => { + writeConfig({ workingMemory: 'nope' }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation).toEqual({ ...DEFAULT_WORKING_MEMORY_CONSOLIDATION }); + const matched = warn.mock.calls.some((call: unknown[]) => + call.some((arg: unknown) => typeof arg === 'string' && arg.includes('workingMemory')), + ); + expect(matched).toBe(true); }); }); + + describe('backward compat', () => { + it('a pre-Phase-4 config with no workingMemory key returns full consolidation defaults', () => { + writeConfig({ excludedTools: ['shell'] }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation).toEqual({ ...DEFAULT_WORKING_MEMORY_CONSOLIDATION }); + }); + + it('preserves existing top-level fields byte-identically when adding consolidation defaults', () => { + const baseline = { excludedTools: ['shell', 'str_replace'] }; + writeConfig(baseline); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.excludedTools).toEqual(baseline.excludedTools); + expect(cfg.workingMemory.consolidation).toEqual({ ...DEFAULT_WORKING_MEMORY_CONSOLIDATION }); + }); + + it('an empty workingMemory: {} block returns full consolidation defaults', () => { + writeConfig({ workingMemory: {} }); + const cfg = loadChamberMindConfig(tmpDir); + expect(cfg.workingMemory.consolidation).toEqual({ ...DEFAULT_WORKING_MEMORY_CONSOLIDATION }); + }); + }); + + describe('idempotence', () => { + it('re-loading the same config yields identical results', () => { + writeConfig({ + excludedTools: ['shell'], + workingMemory: { + consolidation: { + enabled: true, + lastKTurns: 7, + }, + }, + }); + const first = loadChamberMindConfig(tmpDir); + const second = loadChamberMindConfig(tmpDir); + expect(second).toEqual(first); + }); + + it('mutating the returned consolidation does not affect future loads', () => { + writeConfig({}); + const first = loadChamberMindConfig(tmpDir); + first.workingMemory.consolidation.enabled = true; + first.workingMemory.consolidation.lastKTurns = 99; + const second = loadChamberMindConfig(tmpDir); + expect(second.workingMemory.consolidation.enabled).toBe(false); + expect(second.workingMemory.consolidation.lastKTurns).toBe(10); + }); + }); +}); + +describe('patchChamberMindConfig', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mind-config-patch-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('creates the .chamber.json file when it does not yet exist and applies the patch', () => { + patchChamberMindConfig(tmpDir, { + workingMemory: { consolidation: { enabled: true } }, + }); + const onDisk = JSON.parse(fs.readFileSync(path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), 'utf-8')); + expect(onDisk).toEqual({ + workingMemory: { consolidation: { enabled: true } }, + }); + }); + + it('deep-merges into an existing workingMemory.consolidation block', () => { + fs.writeFileSync( + path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), + JSON.stringify({ + workingMemory: { + consolidation: { enabled: false, cron: '*/5 * * * *', lastKTurns: 5 }, + }, + }, null, 2) + '\n', + ); + + patchChamberMindConfig(tmpDir, { + workingMemory: { consolidation: { enabled: true } }, + }); + + const onDisk = JSON.parse(fs.readFileSync(path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), 'utf-8')); + expect(onDisk.workingMemory.consolidation).toEqual({ + enabled: true, + cron: '*/5 * * * *', + lastKTurns: 5, + }); + }); + + it('preserves existing top-level passthrough fields like excludedTools', () => { + fs.writeFileSync( + path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), + JSON.stringify({ + excludedTools: ['shell', 'str_replace'], + somethingFromAFutureVersion: { keepMe: true }, + }, null, 2) + '\n', + ); + + patchChamberMindConfig(tmpDir, { + workingMemory: { consolidation: { enabled: true } }, + }); + + const onDisk = JSON.parse(fs.readFileSync(path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), 'utf-8')); + expect(onDisk.excludedTools).toEqual(['shell', 'str_replace']); + expect(onDisk.somethingFromAFutureVersion).toEqual({ keepMe: true }); + expect(onDisk.workingMemory.consolidation.enabled).toBe(true); + }); + + it('writes pretty-printed JSON ending with a newline', () => { + patchChamberMindConfig(tmpDir, { + workingMemory: { consolidation: { enabled: true } }, + }); + const raw = fs.readFileSync(path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME), 'utf-8'); + expect(raw.endsWith('\n')).toBe(true); + expect(raw).toContain('\n '); + }); + + it('leaves the original .chamber.json byte-identical when the rename step fails', () => { + // Trigger a real OS-level rename failure without monkey-patching fs + // (which is impossible for ESM-imported `node:fs.renameSync`). We make + // `.chamber.json` a non-empty directory so: + // * readRawChamberConfig() sees existsSync=true but readFileSync throws + // (EISDIR) → caught → returns {} + // * writeFileSync(tmpPath) succeeds (tmp lives next to the dir) + // * renameSync(tmp, dir) fails → caught → tmp removed → rethrow + const filePath = path.join(tmpDir, CHAMBER_MIND_CONFIG_FILENAME); + fs.mkdirSync(filePath); + const sentinelPath = path.join(filePath, 'marker.txt'); + fs.writeFileSync(sentinelPath, 'untouched'); + + expect(() => patchChamberMindConfig(tmpDir, { + workingMemory: { consolidation: { enabled: true } }, + })).toThrow(); + + expect(fs.statSync(filePath).isDirectory()).toBe(true); + expect(fs.readFileSync(sentinelPath, 'utf-8')).toBe('untouched'); + const lingering = fs.readdirSync(tmpDir).filter((entry) => entry.endsWith('.tmp')); + expect(lingering).toEqual([]); + }); }); diff --git a/packages/services/src/mind/chamberMindConfig.ts b/packages/services/src/mind/chamberMindConfig.ts index f4a3cfac..c68d31b6 100644 --- a/packages/services/src/mind/chamberMindConfig.ts +++ b/packages/services/src/mind/chamberMindConfig.ts @@ -1,11 +1,21 @@ // Reads a mind's `.chamber.json` and returns the chamber-managed slice of // session config that needs per-mind customization. Missing file → empty -// config; invalid JSON / failing schema → warn + empty (consistent with +// config (with `workingMemory.consolidation` defaults applied); invalid +// JSON / failing top-level schema → warn + defaults (consistent with // `mcpConfig.ts`, so a typo in one file never bricks a mind). // // Schema (intentionally small — extend additively): // { -// "excludedTools": ["shell", "str_replace"] +// "excludedTools": ["shell", "str_replace"], +// "workingMemory": { +// "consolidation": { +// "enabled": false, +// "cron": "0 3 * * *", +// "lastKTurns": 10, +// "perTurnMaxBytes": 2048, +// "memoryMaxBytes": 8192 +// } +// } // } // // `excludedTools` maps directly onto `SessionConfig.excludedTools` @@ -13,6 +23,17 @@ // `copilot help tools` for the full list. This is per-agent in chamber // terms because each mind runs its own CopilotClient + session. // +// `workingMemory.consolidation` is the per-mind opt-in for the Dream +// Daemon (issue: dream-daemon spike, Phase 4). Defaults are OFF — the +// daemon never activates unless `enabled: true`. Cron validation is +// deferred to the InternalScheduler (Phase 10); here we only enforce +// `z.string()`. Numeric fields are positive integers; composer (Phase +// 12) enforces hard caps as defense in depth. +// +// Validation strategy mirrors the rest of this loader: per-field +// `safeParse`. An invalid field falls back to the default and emits a +// `log.warn` — never throws, never bricks the mind. +// // Issue #131 checklist 6. import * as fs from 'node:fs'; @@ -28,20 +49,89 @@ const chamberMindConfigSchema = z.object({ export const CHAMBER_MIND_CONFIG_FILENAME = '.chamber.json'; +export interface WorkingMemoryConsolidationConfig { + enabled: boolean; + cron: string; + lastKTurns: number; + perTurnMaxBytes: number; + memoryMaxBytes: number; +} + +export interface WorkingMemoryConfig { + consolidation: WorkingMemoryConsolidationConfig; +} + export interface ChamberMindConfig { excludedTools?: string[]; + workingMemory: WorkingMemoryConfig; +} + +export const DEFAULT_WORKING_MEMORY_CONSOLIDATION: Readonly = Object.freeze({ + enabled: false, + cron: '0 3 * * *', + lastKTurns: 10, + perTurnMaxBytes: 2048, + memoryMaxBytes: 8192, +}); + +const consolidationFieldSchemas: { [K in keyof WorkingMemoryConsolidationConfig]: z.ZodType } = { + enabled: z.boolean(), + cron: z.string(), + lastKTurns: z.number().int().positive(), + perTurnMaxBytes: z.number().int().positive(), + memoryMaxBytes: z.number().int().positive(), +}; + +function defaultWorkingMemory(): WorkingMemoryConfig { + return { consolidation: { ...DEFAULT_WORKING_MEMORY_CONSOLIDATION } }; +} + +function parseWorkingMemory(raw: unknown, filePath: string): WorkingMemoryConfig { + const out = defaultWorkingMemory(); + if (raw === undefined) return out; + if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) { + log.warn(`Invalid workingMemory in ${filePath}; expected object, falling back to defaults.`); + return out; + } + + const wm = raw as Record; + const consolidationRaw = wm.consolidation; + if (consolidationRaw === undefined) return out; + if (consolidationRaw === null || typeof consolidationRaw !== 'object' || Array.isArray(consolidationRaw)) { + log.warn(`Invalid workingMemory.consolidation in ${filePath}; expected object, falling back to defaults.`); + return out; + } + + const consolidation = consolidationRaw as Record; + for (const key of Object.keys(consolidationFieldSchemas) as Array) { + if (!(key in consolidation)) continue; + const schema = consolidationFieldSchemas[key]; + const result = schema.safeParse(consolidation[key]); + if (result.success) { + // Type-safe write via per-key narrowing. + (out.consolidation[key] as WorkingMemoryConsolidationConfig[typeof key]) = result.data; + } else { + log.warn( + `Invalid workingMemory.consolidation.${key} in ${filePath}; using default ${JSON.stringify(DEFAULT_WORKING_MEMORY_CONSOLIDATION[key])}.`, + result.error.issues, + ); + } + } + return out; } export function loadChamberMindConfig(mindPath: string): ChamberMindConfig { const filePath = path.join(mindPath, CHAMBER_MIND_CONFIG_FILENAME); - if (!fs.existsSync(filePath)) return {}; + if (!fs.existsSync(filePath)) { + return { workingMemory: defaultWorkingMemory() }; + } let raw: string; try { raw = fs.readFileSync(filePath, 'utf-8'); } catch (err) { log.warn(`Failed to read ${filePath}; skipping chamber mind config:`, err); - return {}; + return { workingMemory: defaultWorkingMemory() }; } let parsed: unknown; @@ -49,18 +139,93 @@ export function loadChamberMindConfig(mindPath: string): ChamberMindConfig { parsed = JSON.parse(raw); } catch (err) { log.warn(`Invalid JSON in ${filePath}; skipping chamber mind config:`, err); - return {}; + return { workingMemory: defaultWorkingMemory() }; } const result = chamberMindConfigSchema.safeParse(parsed); if (!result.success) { log.warn(`Schema validation failed for ${filePath}; skipping chamber mind config:`, result.error.issues); - return {}; + return { workingMemory: defaultWorkingMemory() }; } - const out: ChamberMindConfig = {}; + const out: ChamberMindConfig = { + workingMemory: parseWorkingMemory((result.data as Record).workingMemory, filePath), + }; if (result.data.excludedTools && result.data.excludedTools.length > 0) { out.excludedTools = [...result.data.excludedTools]; } return out; } + +// Atomically merge a partial patch into the mind's `.chamber.json`. Reads +// the current raw JSON (preserving unknown top-level passthrough fields per +// `chamberMindConfigSchema.passthrough()`), deep-merges the patch into +// `workingMemory.consolidation`, then writes the result via tmp-file + +// rename. If any step fails, the original file is left untouched and the +// tmp file is removed. Used by `MindManager.enableDreamDaemon` / +// `disableDreamDaemon` so flipping the toggle never half-writes the file. +export interface ChamberMindConfigPatch { + workingMemory?: { + consolidation?: Partial; + }; +} + +function readRawChamberConfig(filePath: string): Record { + if (!fs.existsSync(filePath)) return {}; + let raw: string; + try { + raw = fs.readFileSync(filePath, 'utf-8'); + } catch (err) { + log.warn(`Failed to read ${filePath} during patch; treating as empty:`, err); + return {}; + } + try { + const parsed = JSON.parse(raw); + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + log.warn(`Existing ${filePath} is not a JSON object during patch; treating as empty.`); + return {}; + } + return parsed as Record; + } catch (err) { + log.warn(`Invalid JSON in ${filePath} during patch; treating as empty:`, err); + return {}; + } +} + +export function patchChamberMindConfig(mindPath: string, patch: ChamberMindConfigPatch): void { + const filePath = path.join(mindPath, CHAMBER_MIND_CONFIG_FILENAME); + const current = readRawChamberConfig(filePath); + + const currentWorkingMemory = (current.workingMemory && typeof current.workingMemory === 'object' && !Array.isArray(current.workingMemory)) + ? current.workingMemory as Record + : {}; + const currentConsolidation = (currentWorkingMemory.consolidation && typeof currentWorkingMemory.consolidation === 'object' && !Array.isArray(currentWorkingMemory.consolidation)) + ? currentWorkingMemory.consolidation as Record + : {}; + + const merged: Record = { ...current }; + if (patch.workingMemory) { + // Only `workingMemory.consolidation` is deep-merged; sibling subkeys + // (e.g. future `workingMemory.archival`) survive via the spread of + // `currentWorkingMemory`. Extend this branch when introducing new + // subkeys that themselves need a deep-merge rather than a clobber. + merged.workingMemory = { + ...currentWorkingMemory, + ...(patch.workingMemory.consolidation + ? { consolidation: { ...currentConsolidation, ...patch.workingMemory.consolidation } } + : {}), + }; + } + + const serialized = `${JSON.stringify(merged, null, 2)}\n`; + const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; + fs.writeFileSync(tmpPath, serialized, 'utf-8'); + try { + fs.renameSync(tmpPath, filePath); + } catch (err) { + if (fs.existsSync(tmpPath)) { + try { fs.rmSync(tmpPath, { force: true }); } catch { /* best-effort cleanup */ } + } + throw err; + } +} diff --git a/packages/services/src/mindMemory/CopilotLLMClient.integration.test.ts b/packages/services/src/mindMemory/CopilotLLMClient.integration.test.ts new file mode 100644 index 00000000..3a4fd5f4 --- /dev/null +++ b/packages/services/src/mindMemory/CopilotLLMClient.integration.test.ts @@ -0,0 +1,107 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { CopilotClient, RuntimeConnection } from '@github/copilot-sdk'; + +import { createCopilotLLMClient } from './CopilotLLMClient'; +import { buildOneShotSession } from './oneShotSession'; +import { + getPlatformCopilotBinaryPath, + resolveNodeModulesDir, +} from '../sdk'; + +/** + * Phase 8 / 13 — CopilotLLMClient × Copilot SDK contract test. + * + * Hermetic by default. Set `CHAMBER_LIVE_SDK=1` (and ensure the Copilot + * CLI runtime + valid keychain credentials are present) to exercise the + * adapter against a real one-shot session. + * + * The unit-test contract for `buildOneShotSession` lives in + * `oneShotSession.test.ts` (hermetic, fakes only). This file proves the + * production adapter actually drives the SDK end-to-end with the + * documented contract: + * + * - NO tools registered (tools = []) + * - NO config discovery (`enableConfigDiscovery: false`) + * - PermissionHandler refuses any request that leaks through + * - `synthesize` returns the final assistant text + * - the underlying CLI process is torn down by `close()` + */ + +const liveSdk = process.env.CHAMBER_LIVE_SDK === '1'; + +describe.skipIf(!liveSdk)('CopilotLLMClient — live SDK', () => { + const mindId = 'chamber-llm-integration-mind'; + let mindPath: string; + let logDir: string; + let client: CopilotClient; + + beforeAll(async () => { + const modulesDir = resolveNodeModulesDir(); + const cliPath = getPlatformCopilotBinaryPath(modulesDir); + + mindPath = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-llm-int-')); + // SDK --log-dir requires the directory to pre-exist; we don't read logDir again. + logDir = path.join(os.homedir(), '.chamber', 'logs'); + fs.mkdirSync(logDir, { recursive: true }); + + client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ + path: cliPath, + args: [ + '--log-dir', logDir, + '--allow-all-tools', + '--allow-all-paths', + '--allow-all-urls', + ], + }), + workingDirectory: mindPath, + logLevel: 'all', + }); + await client.start(); + }, 60_000); + + afterAll(async () => { + if (client) { + await client.stop().catch(() => undefined); + } + if (mindPath) { + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + fs.rmSync(mindPath, { recursive: true, force: true }); + break; + } catch { + await new Promise((resolve) => setTimeout(resolve, 250)); + } + } + } + }, 30_000); + + it( + 'returns assistant text containing "pong" for the canonical smoke prompt', + async () => { + const llm = createCopilotLLMClient({ + mindId, + mindPath, + deps: { + createOneShotSession: ({ signal }) => + buildOneShotSession({ + client, + workingDirectory: mindPath, + signal, + }), + }, + }); + + const response = await llm.synthesize({ + prompt: "Reply with the word 'pong' and nothing else.", + timeoutMs: 60_000, + }); + + expect(response.toLowerCase()).toContain('pong'); + }, + 120_000, + ); +}); diff --git a/packages/services/src/mindMemory/CopilotLLMClient.test.ts b/packages/services/src/mindMemory/CopilotLLMClient.test.ts new file mode 100644 index 00000000..fa166df5 --- /dev/null +++ b/packages/services/src/mindMemory/CopilotLLMClient.test.ts @@ -0,0 +1,215 @@ +/** + * Phase 8 — CopilotLLMClient adapter unit tests. + * + * The adapter is decoupled from the real Copilot SDK: it consumes a + * `createOneShotSession` factory through `deps`. These tests drive a fake + * factory and assert the five locked behaviours from the Phase 8 brief: + * 1. Tools / permission surface left to the factory (adapter never + * installs an approval handler and never asks the factory to register + * tools — it simply forwards mindId / mindPath / signal). + * 2. Timeout-bounded via an internal AbortController; on expiry rejects + * with `Error('LLM synthesis timed out after Xms')` and aborts the + * session signal. + * 3. No conversation history mutation — fresh session per call, never + * reused. + * 4. Error propagation — non-timeout errors propagate verbatim. + * 5. Always closes the session, even on timeout / synthesis failure. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + createCopilotLLMClient, + type CreateOneShotSessionArgs, + type OneShotSession, +} from './CopilotLLMClient'; + +interface FakeSession extends OneShotSession { + closed: boolean; + signal: AbortSignal; +} + +interface FakeFactoryOptions { + send?: (prompt: string, signal: AbortSignal) => Promise; + closeError?: Error; + factoryError?: Error; +} + +interface FakeFactoryHandle { + factory: (args: CreateOneShotSessionArgs) => Promise; + calls: CreateOneShotSessionArgs[]; + sessions: FakeSession[]; +} + +function makeFakeFactory(options: FakeFactoryOptions = {}): FakeFactoryHandle { + const calls: CreateOneShotSessionArgs[] = []; + const sessions: FakeSession[] = []; + const factory = async (args: CreateOneShotSessionArgs): Promise => { + calls.push(args); + if (options.factoryError) throw options.factoryError; + const session: FakeSession = { + closed: false, + signal: args.signal, + async send(prompt: string): Promise { + if (options.send) return options.send(prompt, args.signal); + return `echo:${prompt}`; + }, + async close(): Promise { + this.closed = true; + if (options.closeError) throw options.closeError; + }, + }; + sessions.push(session); + return session; + }; + return { factory, calls, sessions }; +} + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe('createCopilotLLMClient', () => { + it('forwards mindId, mindPath, and an AbortSignal to the session factory', async () => { + const handle = makeFakeFactory(); + const client = createCopilotLLMClient({ + mindId: 'mind-alpha', + mindPath: '/tmp/minds/alpha', + deps: { createOneShotSession: handle.factory }, + }); + + const result = await client.synthesize({ prompt: 'hello', timeoutMs: 1_000 }); + + expect(result).toBe('echo:hello'); + expect(handle.calls).toHaveLength(1); + expect(handle.calls[0].mindId).toBe('mind-alpha'); + expect(handle.calls[0].mindPath).toBe('/tmp/minds/alpha'); + expect(handle.calls[0].signal).toBeInstanceOf(AbortSignal); + expect(handle.calls[0].signal.aborted).toBe(false); + }); + + it('does not surface a permission handler or tool registration to the factory', async () => { + // The adapter's args interface only carries mindId, mindPath, signal. + // Asserting the keys explicitly catches accidental drift that would + // open a back-door for tools or approval flow. + const handle = makeFakeFactory(); + const client = createCopilotLLMClient({ + mindId: 'mind-alpha', + mindPath: '/tmp/minds/alpha', + deps: { createOneShotSession: handle.factory }, + }); + + await client.synthesize({ prompt: 'p', timeoutMs: 1_000 }); + + const args = handle.calls[0] as unknown as Record; + expect(Object.keys(args).sort()).toEqual(['mindId', 'mindPath', 'signal']); + expect(args.tools).toBeUndefined(); + expect(args.onPermissionRequest).toBeUndefined(); + }); + + it('creates a fresh session for every synthesize call (no history mutation)', async () => { + const handle = makeFakeFactory(); + const client = createCopilotLLMClient({ + mindId: 'm', mindPath: '/m', deps: { createOneShotSession: handle.factory }, + }); + + await client.synthesize({ prompt: 'one', timeoutMs: 1_000 }); + await client.synthesize({ prompt: 'two', timeoutMs: 1_000 }); + + expect(handle.sessions).toHaveLength(2); + expect(handle.sessions[0]).not.toBe(handle.sessions[1]); + expect(handle.sessions[0].closed).toBe(true); + expect(handle.sessions[1].closed).toBe(true); + }); + + it('rejects with a timeout error and aborts the signal when timeoutMs elapses', async () => { + const handle = makeFakeFactory({ + send: (_prompt, signal) => + new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => reject(new Error('aborted')), { once: true }); + }), + }); + const client = createCopilotLLMClient({ + mindId: 'm', mindPath: '/m', deps: { createOneShotSession: handle.factory }, + }); + + const promise = client.synthesize({ prompt: 'p', timeoutMs: 250 }); + // Attach catch handler before advancing timers to avoid an + // unhandled-rejection race when the timer fires synchronously. + const settled = promise.catch((e: unknown) => e as Error); + + await vi.advanceTimersByTimeAsync(250); + const err = await settled; + + expect(err).toBeInstanceOf(Error); + expect((err as Error).message).toBe('LLM synthesis timed out after 250ms'); + expect(handle.sessions[0].signal.aborted).toBe(true); + expect(handle.sessions[0].closed).toBe(true); + }); + + it('propagates non-timeout SDK errors verbatim', async () => { + const handle = makeFakeFactory({ + send: async () => { throw new Error('SDK exploded'); }, + }); + const client = createCopilotLLMClient({ + mindId: 'm', mindPath: '/m', deps: { createOneShotSession: handle.factory }, + }); + + await expect(client.synthesize({ prompt: 'p', timeoutMs: 1_000 })) + .rejects.toThrow('SDK exploded'); + + expect(handle.sessions[0].closed).toBe(true); + }); + + it('propagates errors from the factory itself and does not try to close a missing session', async () => { + const handle = makeFakeFactory({ + factoryError: new Error('cannot start CLI'), + }); + const client = createCopilotLLMClient({ + mindId: 'm', mindPath: '/m', deps: { createOneShotSession: handle.factory }, + }); + + await expect(client.synthesize({ prompt: 'p', timeoutMs: 1_000 })) + .rejects.toThrow('cannot start CLI'); + expect(handle.sessions).toHaveLength(0); + }); + + it('always closes the session in `finally`, even when send throws', async () => { + const handle = makeFakeFactory({ + send: async () => { throw new Error('boom'); }, + }); + const client = createCopilotLLMClient({ + mindId: 'm', mindPath: '/m', deps: { createOneShotSession: handle.factory }, + }); + + await expect(client.synthesize({ prompt: 'p', timeoutMs: 1_000 })).rejects.toThrow('boom'); + expect(handle.sessions[0].closed).toBe(true); + }); + + it('swallows close() failures so they do not mask synthesis success', async () => { + const handle = makeFakeFactory({ closeError: new Error('close bombed') }); + const client = createCopilotLLMClient({ + mindId: 'm', mindPath: '/m', deps: { createOneShotSession: handle.factory }, + }); + + await expect(client.synthesize({ prompt: 'p', timeoutMs: 1_000 })).resolves.toBe('echo:p'); + expect(handle.sessions[0].closed).toBe(true); + }); + + it('clears the timeout timer on the success path', async () => { + const handle = makeFakeFactory(); + const client = createCopilotLLMClient({ + mindId: 'm', mindPath: '/m', deps: { createOneShotSession: handle.factory }, + }); + + await client.synthesize({ prompt: 'p', timeoutMs: 5_000 }); + // If the timer were still pending, advancing the clock would flip + // the aborted bit on the session signal we kept a reference to. + await vi.advanceTimersByTimeAsync(10_000); + expect(handle.sessions[0].signal.aborted).toBe(false); + }); +}); diff --git a/packages/services/src/mindMemory/CopilotLLMClient.ts b/packages/services/src/mindMemory/CopilotLLMClient.ts new file mode 100644 index 00000000..25e9106a --- /dev/null +++ b/packages/services/src/mindMemory/CopilotLLMClient.ts @@ -0,0 +1,98 @@ +/** + * CopilotLLMClient — `LLMClient` adapter that calls the mind's own Copilot + * SDK as a one-shot, side-effect-free language model for the Dream Daemon. + * + * The adapter intentionally does NOT own SDK lifecycle. It receives a + * `createOneShotSession` factory through `deps`; that factory (supplied + * by the composition root in Phase 13) is responsible for constructing a + * Copilot session with: + * - NO tools registered (empty tool surface). + * - NO permission handler / no approval flow. + * - The provided `AbortSignal` threaded into the SDK so the adapter's + * timeout actually cancels the underlying CLI process. + * + * The adapter contract enforces the rest: + * - One fresh session per `synthesize` call (no history mutation). + * - Internal `AbortController` with a `setTimeout(timeoutMs)` deadline. + * - The session is closed in `finally`, even on timeout / synthesis + * failure, so a stuck CLI cannot leak. + * - On timeout, rejects with `Error('LLM synthesis timed out after Xms')` + * so callers can distinguish it from SDK / network failures. + * + * v1 deliberately does NOT accept an external `AbortSignal` — there is a + * single internal controller. Callers wanting cancellation should compose + * a shorter `timeoutMs`. + */ + +import type { LLMClient, SynthesizeRequest } from './LLMClient'; + +export interface OneShotSession { + /** Resolves with the final assistant text for the prompt. */ + send(prompt: string): Promise; + /** Best-effort teardown; called in `finally` and must not throw. */ + close(): Promise; +} + +export interface CreateOneShotSessionArgs { + readonly mindId: string; + readonly mindPath: string; + readonly signal: AbortSignal; +} + +export interface CopilotLLMClientDeps { + /** + * Factory that constructs a one-shot Copilot session for the target + * mind. Implementations MUST disable tools and the permission handler + * and MUST honor `signal` (abort the underlying CLI on cancel). + */ + readonly createOneShotSession: (args: CreateOneShotSessionArgs) => Promise; +} + +export interface CopilotLLMClientOptions { + readonly mindId: string; + readonly mindPath: string; + readonly deps: CopilotLLMClientDeps; +} + +export function createCopilotLLMClient(opts: CopilotLLMClientOptions): LLMClient { + const { mindId, mindPath, deps } = opts; + + return { + async synthesize(req: SynthesizeRequest): Promise { + const controller = new AbortController(); + let timedOut = false; + const timer = setTimeout(() => { + timedOut = true; + controller.abort(); + }, req.timeoutMs); + + let session: OneShotSession | null = null; + try { + session = await deps.createOneShotSession({ + mindId, + mindPath, + signal: controller.signal, + }); + const result = await session.send(req.prompt); + if (timedOut) { + throw new Error(`LLM synthesis timed out after ${req.timeoutMs}ms`); + } + return result; + } catch (err) { + if (timedOut) { + throw new Error(`LLM synthesis timed out after ${req.timeoutMs}ms`, { cause: err }); + } + throw err; + } finally { + clearTimeout(timer); + if (session) { + try { + await session.close(); + } catch { + // best-effort teardown + } + } + } + }, + }; +} diff --git a/packages/services/src/mindMemory/DailyLogWriter.test.ts b/packages/services/src/mindMemory/DailyLogWriter.test.ts new file mode 100644 index 00000000..085badcd --- /dev/null +++ b/packages/services/src/mindMemory/DailyLogWriter.test.ts @@ -0,0 +1,543 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { createDailyLogWriter } from './DailyLogWriter'; +import { STRUCTURED_LOG_SENTINEL, parseLog, serializeTurn, type CompletedTurn } from './StructuredLogFormat'; + +let mindRoot: string; +let logPath: string; +let legacyPath: string; + +beforeEach(() => { + mindRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-dlw-')); + logPath = path.join(mindRoot, '.working-memory', 'log.md'); + legacyPath = path.join(mindRoot, '.working-memory', 'log.legacy.md'); +}); + +afterEach(() => { + fs.rmSync(mindRoot, { recursive: true, force: true }); +}); + +function makeTurn(i: number, status: 'completed' | 'aborted' | 'error' = 'completed'): CompletedTurn { + const ts = `2026-05-12T17:${String(20 + i).padStart(2, '0')}:00Z`; + return { + turnId: `turn-${i}`, + sessionId: `sess-${i}`, + model: 'claude-opus-4.7', + status, + startedAt: ts, + endedAt: ts, + prompt: `prompt body ${i}`, + finalAssistantMessage: `assistant body ${i}`, + }; +} + +function makeWriter(extras: { logger?: { info: (msg: string) => void; warn?: (msg: string, ...args: unknown[]) => void }; rename?: (from: string, to: string) => Promise } = {}) { + return createDailyLogWriter({ + mindId: 'mind-x', + mindPath: mindRoot, + deps: { + logger: extras.logger, + rename: extras.rename, + }, + }); +} + +describe('DailyLogWriter — round-trip', () => { + it('writes three turns that parse back via StructuredLogFormat.parseLog', async () => { + const writer = makeWriter(); + const t1 = makeTurn(1); + const t2 = makeTurn(2, 'aborted'); + const t3 = makeTurn(3, 'error'); + + await writer.write(t1); + await writer.write(t2); + await writer.write(t3); + + const content = fs.readFileSync(logPath, 'utf-8'); + const parsed = parseLog(content); + + expect(parsed.sentinel).toBe(true); + expect(parsed.malformed).toBe(0); + expect(parsed.turns).toHaveLength(3); + expect(parsed.turns.map((t) => t.turnId)).toEqual(['turn-1', 'turn-2', 'turn-3']); + expect(parsed.turns[0].prompt).toBe('prompt body 1'); + expect(parsed.turns[0].assistant).toBe('assistant body 1'); + expect(parsed.turns[1].status).toBe('aborted'); + expect(parsed.turns[2].status).toBe('error'); + }); +}); + +describe('DailyLogWriter — first-call migration (rotation)', () => { + it('rotates unstructured log.md to log.legacy.md and seeds a fresh structured log', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const legacyContent = '# notes\n\nrandom freeform content\n'; + fs.writeFileSync(logPath, legacyContent); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.write(makeTurn(1)); + + expect(fs.readFileSync(legacyPath, 'utf-8')).toBe(legacyContent); + const fresh = fs.readFileSync(logPath, 'utf-8'); + expect(fresh.startsWith(STRUCTURED_LOG_SENTINEL)).toBe(true); + const parsed = parseLog(fresh); + expect(parsed.sentinel).toBe(true); + expect(parsed.turns).toHaveLength(1); + expect(info).toHaveBeenCalledWith( + 'Rotated unstructured log.md to log.legacy.md for mind mind-x', + ); + }); + + it('idempotent: second write does not re-rotate', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.writeFileSync(logPath, 'unstructured original\n'); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.write(makeTurn(1)); + const legacyAfterFirst = fs.readFileSync(legacyPath, 'utf-8'); + + await writer.write(makeTurn(2)); + + expect(fs.readFileSync(legacyPath, 'utf-8')).toBe(legacyAfterFirst); + expect(info).toHaveBeenCalledTimes(1); + + const parsed = parseLog(fs.readFileSync(logPath, 'utf-8')); + expect(parsed.sentinel).toBe(true); + expect(parsed.turns).toHaveLength(2); + }); + + it('collision rule: when log.legacy.md already exists, rotates to log.legacy..md', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const priorLegacy = '# previous legacy\n'; + const todayBad = '# today is bad\n'; + fs.writeFileSync(legacyPath, priorLegacy); + fs.writeFileSync(logPath, todayBad); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.write(makeTurn(1)); + + // Original legacy untouched + expect(fs.readFileSync(legacyPath, 'utf-8')).toBe(priorLegacy); + + // A timestamped legacy file containing today's bad content was created + const all = fs.readdirSync(path.dirname(logPath)); + const stamped = all.filter( + (n) => /^log\.legacy\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z\.md$/.test(n), + ); + expect(stamped).toHaveLength(1); + expect(fs.readFileSync(path.join(path.dirname(logPath), stamped[0]), 'utf-8')).toBe(todayBad); + + // Fresh log.md is structured + const parsed = parseLog(fs.readFileSync(logPath, 'utf-8')); + expect(parsed.sentinel).toBe(true); + expect(parsed.turns).toHaveLength(1); + + expect(info).toHaveBeenCalledWith( + `Rotated unstructured log.md to ${stamped[0]} for mind mind-x`, + ); + }); + + it('empty log.md is treated as already-structured: no rotation', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.writeFileSync(logPath, ''); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.write(makeTurn(1)); + + expect(fs.existsSync(legacyPath)).toBe(false); + expect(info).not.toHaveBeenCalled(); + + const parsed = parseLog(fs.readFileSync(logPath, 'utf-8')); + expect(parsed.sentinel).toBe(true); + expect(parsed.turns).toHaveLength(1); + }); + + it('structured log.md (sentinel present) is left in place', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const seed = `${STRUCTURED_LOG_SENTINEL}\n\n`; + fs.writeFileSync(logPath, seed); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.write(makeTurn(1)); + + expect(fs.existsSync(legacyPath)).toBe(false); + expect(info).not.toHaveBeenCalled(); + + const content = fs.readFileSync(logPath, 'utf-8'); + expect(content.startsWith(seed)).toBe(true); + const parsed = parseLog(content); + expect(parsed.turns).toHaveLength(1); + }); + + it('creates the directory and log.md when none exist (no rotation)', async () => { + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.write(makeTurn(1)); + + expect(fs.existsSync(legacyPath)).toBe(false); + expect(info).not.toHaveBeenCalled(); + const parsed = parseLog(fs.readFileSync(logPath, 'utf-8')); + expect(parsed.turns).toHaveLength(1); + }); +}); + +describe('DailyLogWriter — concurrency', () => { + it('serializes concurrent appends without interleaving', async () => { + const writer = makeWriter(); + + const turns = Array.from({ length: 10 }, (_, i) => makeTurn(i + 1)); + await Promise.all(turns.map((t) => writer.write(t))); + + const content = fs.readFileSync(logPath, 'utf-8'); + + // Sentinel appears exactly once. + const sentinelCount = content.split(STRUCTURED_LOG_SENTINEL).length - 1; + expect(sentinelCount).toBe(1); + + const parsed = parseLog(content); + expect(parsed.sentinel).toBe(true); + expect(parsed.malformed).toBe(0); + expect(parsed.turns).toHaveLength(10); + + const ids = new Set(parsed.turns.map((t) => t.turnId)); + expect(ids.size).toBe(10); + for (let i = 1; i <= 10; i++) { + expect(ids.has(`turn-${i}`)).toBe(true); + } + + // Spot-check no body interleaving: each parsed prompt/assistant matches its turn id. + for (const t of parsed.turns) { + const i = t.turnId.replace('turn-', ''); + expect(t.prompt).toBe(`prompt body ${i}`); + expect(t.assistant).toBe(`assistant body ${i}`); + } + }); +}); + +describe('DailyLogWriter — onTurnRecorded hook', () => { + it('invokes onTurnRecorded once per successful write with the same CompletedTurn payload', async () => { + const onTurnRecorded = vi.fn(); + const writer = createDailyLogWriter({ + mindId: 'mind-x', + mindPath: mindRoot, + deps: { onTurnRecorded }, + }); + + const t1 = makeTurn(1); + const t2 = makeTurn(2); + await writer.write(t1); + await writer.write(t2); + + expect(onTurnRecorded).toHaveBeenCalledTimes(2); + expect(onTurnRecorded).toHaveBeenNthCalledWith(1, t1); + expect(onTurnRecorded).toHaveBeenNthCalledWith(2, t2); + }); + + it('does NOT invoke onTurnRecorded when the underlying write fails', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.writeFileSync(logPath, '# unstructured original\n'); + + const onTurnRecorded = vi.fn(); + const failingRename = vi.fn(async () => { + throw new Error('synthetic rename failure'); + }); + const writer = createDailyLogWriter({ + mindId: 'mind-x', + mindPath: mindRoot, + deps: { onTurnRecorded, rename: failingRename }, + }); + + await expect(writer.write(makeTurn(1))).rejects.toThrow(/synthetic rename failure/); + expect(onTurnRecorded).not.toHaveBeenCalled(); + }); + + it('a throwing onTurnRecorded does not roll back the on-disk write but propagates the error', async () => { + const onTurnRecorded = vi.fn(() => { + throw new Error('observer threw'); + }); + const writer = createDailyLogWriter({ + mindId: 'mind-x', + mindPath: mindRoot, + deps: { onTurnRecorded }, + }); + + await expect(writer.write(makeTurn(1))).rejects.toThrow(/observer threw/); + + // The structured log was still written before the hook was called. + const parsed = parseLog(fs.readFileSync(logPath, 'utf-8')); + expect(parsed.sentinel).toBe(true); + expect(parsed.turns).toHaveLength(1); + }); +}); + +describe('DailyLogWriter — error path safety', () => { + it('rotation rename failure leaves log.md byte-equal and rejects the write', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const original = '# unstructured original content\nline two\n'; + fs.writeFileSync(logPath, original); + + const failingRename = vi.fn(async () => { + throw new Error('synthetic rename failure'); + }); + const info = vi.fn(); + const writer = makeWriter({ logger: { info }, rename: failingRename }); + + await expect(writer.write(makeTurn(1))).rejects.toThrow(/synthetic rename failure/); + + expect(fs.readFileSync(logPath, 'utf-8')).toBe(original); + expect(fs.existsSync(legacyPath)).toBe(false); + expect(info).not.toHaveBeenCalled(); + }); + + it('after a failed rotation, a subsequent successful write rotates and recovers', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const original = '# unstructured original\n'; + fs.writeFileSync(logPath, original); + + let calls = 0; + const flakyRename = vi.fn(async (from: string, to: string) => { + calls++; + if (calls === 1) throw new Error('first attempt fails'); + await fsp.rename(from, to); + }); + const writer = makeWriter({ rename: flakyRename }); + + await expect(writer.write(makeTurn(1))).rejects.toThrow(/first attempt fails/); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(original); + + await writer.write(makeTurn(2)); + + expect(fs.readFileSync(legacyPath, 'utf-8')).toBe(original); + const parsed = parseLog(fs.readFileSync(logPath, 'utf-8')); + expect(parsed.sentinel).toBe(true); + expect(parsed.turns).toHaveLength(1); + expect(parsed.turns[0].turnId).toBe('turn-2'); + }); +}); + +// --------------------------------------------------------------------------- +// v0.60.0 — migrateIfNeeded + flush (Phase 1) +// +// `migrateIfNeeded(mindPath)` is the eager-migration hook MindMemoryService +// invokes when a previously-opted-out mind flips to opted-in. It reproduces +// the rotation-and-seed half of doWrite() WITHOUT requiring a turn to seed. +// The seed bytes MUST match MindScaffold.createStructure byte-for-byte +// (`SENTINEL + '\n\n'`) so on-disk content is uniform regardless of which +// path created it. +// +// `flush()` returns the writer's chain promise so callers (e.g. Phase 4 +// rollbackToUnstructured) can wait for in-flight writes to settle before +// reading log.md back. Without this, an observer-removal-then-read race +// could miss the last frame. +// --------------------------------------------------------------------------- + +describe('DailyLogWriter — migrateIfNeeded', () => { + it('no log.md → no-op (no rotation, no log.legacy.md, no seed)', async () => { + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.migrateIfNeeded(); + + expect(fs.existsSync(logPath)).toBe(false); + expect(fs.existsSync(legacyPath)).toBe(false); + expect(info).not.toHaveBeenCalled(); + }); + + it('empty log.md (0 bytes) → no rotation (treated as already-structured)', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.writeFileSync(logPath, ''); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.migrateIfNeeded(); + + expect(fs.existsSync(legacyPath)).toBe(false); + expect(info).not.toHaveBeenCalled(); + // Empty → seed sentinel-only so subsequent reads are valid structured. + const content = fs.readFileSync(logPath, 'utf-8'); + expect(content).toBe(STRUCTURED_LOG_SENTINEL + '\n\n'); + }); + + it('sentinel-prefixed log.md → no-op (idempotent, no rotation)', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const original = STRUCTURED_LOG_SENTINEL + '\n\n'; + fs.writeFileSync(logPath, original); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.migrateIfNeeded(); + + expect(fs.existsSync(legacyPath)).toBe(false); + expect(info).not.toHaveBeenCalled(); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(original); + }); + + it('sentinel-prefixed log.md WITH frames → no-op (idempotent, frames preserved)', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const original = STRUCTURED_LOG_SENTINEL + '\n\n' + makeWriterRaw([makeTurn(1), makeTurn(2)]); + fs.writeFileSync(logPath, original); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.migrateIfNeeded(); + + expect(fs.existsSync(legacyPath)).toBe(false); + expect(info).not.toHaveBeenCalled(); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(original); + }); + + it('unstructured log.md → rotates to log.legacy.md and seeds sentinel-only log.md (NO frame)', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const legacyContent = '# notes\n\nrandom freeform content\n'; + fs.writeFileSync(logPath, legacyContent); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.migrateIfNeeded(); + + expect(fs.readFileSync(legacyPath, 'utf-8')).toBe(legacyContent); + + // Byte-for-byte parity with MindScaffold.createStructure. + const fresh = fs.readFileSync(logPath, 'utf-8'); + expect(fresh).toBe(STRUCTURED_LOG_SENTINEL + '\n\n'); + + expect(info).toHaveBeenCalledWith( + 'Rotated unstructured log.md to log.legacy.md for mind mind-x', + ); + }); + + it('collision: log.legacy.md exists → rotates to log.legacy..md', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const priorLegacy = '# previous legacy\n'; + const todayBad = '# today is bad\n'; + fs.writeFileSync(legacyPath, priorLegacy); + fs.writeFileSync(logPath, todayBad); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.migrateIfNeeded(); + + expect(fs.readFileSync(legacyPath, 'utf-8')).toBe(priorLegacy); + + const all = fs.readdirSync(path.dirname(logPath)); + const stamped = all.filter( + (n) => /^log\.legacy\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z\.md$/.test(n), + ); + expect(stamped).toHaveLength(1); + expect(fs.readFileSync(path.join(path.dirname(logPath), stamped[0]), 'utf-8')).toBe(todayBad); + + expect(fs.readFileSync(logPath, 'utf-8')).toBe(STRUCTURED_LOG_SENTINEL + '\n\n'); + expect(info).toHaveBeenCalledWith( + `Rotated unstructured log.md to ${stamped[0]} for mind mind-x`, + ); + }); + + it('migrateIfNeeded then write(turn) → first frame appends to the seeded sentinel without re-rotating', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.writeFileSync(logPath, '# legacy\n'); + + const info = vi.fn(); + const writer = makeWriter({ logger: { info } }); + + await writer.migrateIfNeeded(); + await writer.write(makeTurn(1)); + + // Exactly one rotation event. + const rotates = info.mock.calls.filter((c) => /Rotated unstructured/.test(String(c[0]))); + expect(rotates).toHaveLength(1); + + const parsed = parseLog(fs.readFileSync(logPath, 'utf-8')); + expect(parsed.sentinel).toBe(true); + expect(parsed.turns).toHaveLength(1); + }); + + it('serializes through the same per-instance chain as write()', async () => { + // Concurrent write() and migrateIfNeeded() must serialize: never produces + // a doubled sentinel or a half-rotated state. + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.writeFileSync(logPath, '# legacy\n'); + + const writer = makeWriter(); + + // Kick both off "simultaneously" — they must serialize through the chain. + await Promise.all([ + writer.migrateIfNeeded(), + writer.write(makeTurn(1)), + writer.write(makeTurn(2)), + ]); + + const content = fs.readFileSync(logPath, 'utf-8'); + expect(content.split(STRUCTURED_LOG_SENTINEL).length - 1).toBe(1); + const parsed = parseLog(content); + expect(parsed.sentinel).toBe(true); + expect(parsed.malformed).toBe(0); + expect(parsed.turns).toHaveLength(2); + }); +}); + +describe('DailyLogWriter — flush', () => { + it('flush() resolves after all queued writes settle', async () => { + const writer = makeWriter(); + + // Fire many writes without awaiting. + const pending = Array.from({ length: 5 }, (_, i) => writer.write(makeTurn(i + 1))); + + // flush() must not resolve before the chain. + await writer.flush(); + + // After flush, every write is observable on disk. + const parsed = parseLog(fs.readFileSync(logPath, 'utf-8')); + expect(parsed.turns).toHaveLength(5); + + // The original promises also settle. + await Promise.all(pending); + }); + + it('flush() is safe to call when no writes are pending (no-op resolves)', async () => { + const writer = makeWriter(); + await expect(writer.flush()).resolves.toBeUndefined(); + }); + + it('flush() does not throw even if a queued write rejects', async () => { + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + fs.writeFileSync(logPath, '# legacy\n'); + + const failingRename = vi.fn(async () => { + throw new Error('synthetic rename failure'); + }); + const writer = makeWriter({ rename: failingRename }); + + const pending = writer.write(makeTurn(1)); + + // The chain swallows rejections internally so flush stays clean. + await expect(writer.flush()).resolves.toBeUndefined(); + + // The original write() still rejects — error propagation contract preserved. + await expect(pending).rejects.toThrow(/synthetic rename failure/); + }); +}); + +// Helper: produce on-disk frame bytes the same way DailyLogWriter does, so +// "preserved" assertions don't depend on the writer's own behaviour. +function makeWriterRaw(turns: ReturnType[]): string { + return turns.map(serializeTurn).join(''); +} diff --git a/packages/services/src/mindMemory/DailyLogWriter.ts b/packages/services/src/mindMemory/DailyLogWriter.ts new file mode 100644 index 00000000..c42f480a --- /dev/null +++ b/packages/services/src/mindMemory/DailyLogWriter.ts @@ -0,0 +1,295 @@ +/** + * DailyLogWriter — appends completed-turn frames to + * `/.working-memory/log.md` in the chamber-structured-log/v1 format + * and rotates pre-existing unstructured logs out of the way on first touch. + * + * Phase 5 scope (locked by plan): + * - Append-only writer for structured turn frames. + * - First-call migration: any existing unstructured log.md is moved aside + * to `log.legacy.md` (or `log.legacy..md` on collision) before + * the writer seeds a fresh sentinel-prefixed log. + * - Per-instance mutex serializes concurrent appends so `Promise.all` + * fans-in produce non-interleaved frames. + * - Rotation uses `fs.rename` (atomic on a single filesystem) so a failed + * rotation leaves the original `log.md` byte-equal to its prior state. + * - Steady-state appends use `fsp.open(..., 'a')` + `handle.sync()`. Each + * frame is bounded by `perTurnMaxBytes` (well under PIPE_BUF), so the + * POSIX append is atomic in practice. + * + * Out of scope: TurnRecorder wiring (Phase 6), pruning after consolidation + * (Phase 9), composer integration (Phase 12). + */ + +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; + +import { Logger } from '../logger'; +import { + STRUCTURED_LOG_SENTINEL, + detectSentinel, + serializeTurn, + type CompletedTurn, +} from './StructuredLogFormat'; + +const WORKING_MEMORY_DIRNAME = '.working-memory'; +const LOG_FILENAME = 'log.md'; +const LEGACY_FILENAME = 'log.legacy.md'; + +export interface DailyLogWriterLogger { + info(message: string): void; +} + +export interface DailyLogWriterDeps { + /** Override `fs.rename` — used by tests to simulate rotation failures. */ + rename?: (from: string, to: string) => Promise; + /** Override the default `Logger.create('DailyLogWriter')`. */ + logger?: DailyLogWriterLogger; + /** + * Called once per successful structured turn write, after the on-disk + * write has fsync'd. Phase 11 wires this to + * `dream-state.incrementTurnCount` so the activity gate in dream-gates + * advances on real writes (not on SDK session start). + * + * If the hook throws (sync or async), the on-disk write is NOT rolled + * back — the structured frame remains in `log.md` — but the rejection + * propagates to the caller so the wiring layer can react. + */ + onTurnRecorded?: (turn: CompletedTurn) => void | Promise; +} + +export interface DailyLogWriterOptions { + readonly mindId: string; + readonly mindPath: string; + readonly deps?: DailyLogWriterDeps; +} + +export interface DailyLogWriter { + write(turn: CompletedTurn): Promise; + /** + * Eager-migration entry point. Performs the rotation-and-seed half of + * `write()` WITHOUT requiring a turn payload. Behaviour: + * + * - log.md absent → no-op (no rotation, no seed). + * - log.md empty (0 bytes) → seed sentinel-only. + * - log.md sentinel-prefixed → no-op (already structured). + * - log.md unstructured → rotate to log.legacy.md (or timestamped on + * collision) and seed `SENTINEL + '\n\n'` byte-for-byte matching + * `MindScaffold.createStructure`. + * + * Serializes through the same per-instance chain as `write()` so a + * concurrent `migrateIfNeeded` + `write` pair never produces a doubled + * sentinel or half-rotated state. + */ + migrateIfNeeded(): Promise; + /** + * Drain the per-instance write chain. Resolves only after every queued + * `write()` / `migrateIfNeeded()` settles (success OR failure). Phase 4 + * `rollbackToUnstructured` calls this after detaching the observer so it + * can read back log.md without missing in-flight frames. + * + * Errors from queued operations are surfaced through their original + * promises (write returns a rejecting Promise). `flush()` itself never + * rejects — its job is purely to wait for quiescence. + */ + flush(): Promise; +} + +export function createDailyLogWriter(opts: DailyLogWriterOptions): DailyLogWriter { + const { mindId, mindPath } = opts; + const log: DailyLogWriterLogger = opts.deps?.logger ?? Logger.create('DailyLogWriter'); + const rename = opts.deps?.rename ?? fsp.rename; + const onTurnRecorded = opts.deps?.onTurnRecorded; + + const workingMemoryDir = path.resolve(mindPath, WORKING_MEMORY_DIRNAME); + const logPath = path.join(workingMemoryDir, LOG_FILENAME); + const legacyPath = path.join(workingMemoryDir, LEGACY_FILENAME); + + // Per-instance mutex chain. Every write awaits the prior link before issuing + // its own read-modify-write cycle, eliminating intra-process interleaving. + let chain: Promise = Promise.resolve(); + + async function readOrNull(absPath: string): Promise { + try { + return await fsp.readFile(absPath, 'utf-8'); + } catch (err) { + if (isErrnoCode(err, 'ENOENT')) return null; + throw err; + } + } + + async function existsPath(absPath: string): Promise { + try { + await fsp.access(absPath, fs.constants.F_OK); + return true; + } catch (err) { + if (isErrnoCode(err, 'ENOENT')) return false; + throw err; + } + } + + async function rotate(currentContent: string): Promise { + let target = legacyPath; + let targetName = LEGACY_FILENAME; + if (await existsPath(legacyPath)) { + targetName = `log.legacy.${isoStamp()}.md`; + target = path.join(workingMemoryDir, targetName); + } + + // `rename` is atomic on a single filesystem: either log.md is at its old + // path (failure) or at the target path (success). On failure we surface + // the error to the caller; the file content is unchanged. We deliberately + // do NOT fall back to copy+unlink, which would risk a half-rotated state. + await rename(logPath, target); + + log.info(`Rotated unstructured log.md to ${targetName} for mind ${mindId}`); + + // Sanity check: rotation succeeded, so `currentContent` is now under + // `target`. We don't re-read here — the migration is complete and the + // seed step below creates a fresh log.md. + void currentContent; + } + + async function seedFreshLog(turn: CompletedTurn | null): Promise { + // INVARIANT: when turn is null (eager migration via migrateIfNeeded), + // the seed bytes MUST exactly match MindScaffold.createStructure + // (`SENTINEL + '\n\n'`) so on-disk content is uniform regardless of + // which path created it. Composer + parseLog tolerate trailing blank + // lines, but the byte-for-byte parity is what makes the migration + // path reversible. + const content = turn === null + ? `${STRUCTURED_LOG_SENTINEL}\n\n` + : `${STRUCTURED_LOG_SENTINEL}\n\n${serializeTurn(turn)}`; + // Atomic write so a partial seed never lands on disk. + const tmp = `${logPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`; + const handle = await fsp.open(tmp, 'wx'); + try { + await handle.writeFile(content); + await handle.sync(); + } finally { + await handle.close(); + } + try { + await fsp.rename(tmp, logPath); + } catch (err) { + try { + await fsp.unlink(tmp); + } catch { + // best-effort cleanup + } + throw err; + } + } + + async function appendFrame(turn: CompletedTurn): Promise { + const handle = await fsp.open(logPath, 'a'); + try { + await handle.write(serializeTurn(turn)); + await handle.sync(); + } finally { + await handle.close(); + } + } + + async function doWrite(turn: CompletedTurn): Promise { + await fsp.mkdir(workingMemoryDir, { recursive: true }); + + const existing = await readOrNull(logPath); + + // No file → seed a fresh structured log; no rotation event. + if (existing === null) { + await seedFreshLog(turn); + return; + } + + // Empty file is treated as already-structured (no rotation needed): + // we seed sentinel + frame in place. The atomic write replaces the + // empty file in one rename. + if (existing.length === 0) { + await seedFreshLog(turn); + return; + } + + if (detectSentinel(existing)) { + await appendFrame(turn); + return; + } + + // Unstructured content present — rotate before seeding. If rotation + // fails the original log.md is left intact and the error propagates. + await rotate(existing); + await seedFreshLog(turn); + } + + async function doMigrateIfNeeded(): Promise { + // Eager-migration variant of doWrite. Same rotation rules, but the seed + // is sentinel-only (no turn frame). Idempotent: missing or already- + // structured logs are no-ops. Reused by MindMemoryService.activateMind + // so a user who flips the dream-daemon switch sees their pre-existing + // unstructured log preserved as log.legacy.md immediately, without + // waiting for the next chat turn. + await fsp.mkdir(workingMemoryDir, { recursive: true }); + + const existing = await readOrNull(logPath); + + // No log.md → no-op. The first write() will seed. + if (existing === null) return; + + // Empty file → seed sentinel-only so subsequent reads see a valid log. + if (existing.length === 0) { + await seedFreshLog(null); + return; + } + + // Already structured → idempotent no-op. + if (detectSentinel(existing)) return; + + // Unstructured → rotate, then seed sentinel-only. + await rotate(existing); + await seedFreshLog(null); + } + + function write(turn: CompletedTurn): Promise { + const next = chain.then(async () => { + await doWrite(turn); + if (onTurnRecorded) { + await onTurnRecorded(turn); + } + }); + // Swallow rejections on the chain itself so a failed write does not + // poison the queue for subsequent callers; the rejection still reaches + // the caller via `next`. + chain = next.catch(() => undefined); + return next; + } + + function migrateIfNeeded(): Promise { + const next = chain.then(() => doMigrateIfNeeded()); + chain = next.catch(() => undefined); + return next; + } + + function flush(): Promise { + // Wait for the current chain tail to settle. The chain itself swallows + // rejections, so awaiting it never throws — flush is purely a quiescence + // barrier. Callers who care about per-write errors observe them via the + // promises returned by `write()` / `migrateIfNeeded()`. + return chain.then(() => undefined); + } + + return { write, migrateIfNeeded, flush }; +} + +function isoStamp(): string { + // 2026-05-12T17:21:45.123Z → 2026-05-12T17-21-45Z + return new Date().toISOString().replace(/\.\d+Z$/, 'Z').replace(/:/g, '-'); +} + +function isErrnoCode(err: unknown, code: string): boolean { + return ( + typeof err === 'object' && + err !== null && + 'code' in err && + (err as NodeJS.ErrnoException).code === code + ); +} diff --git a/packages/services/src/mindMemory/DreamDaemon.test.ts b/packages/services/src/mindMemory/DreamDaemon.test.ts new file mode 100644 index 00000000..c5dbf94e --- /dev/null +++ b/packages/services/src/mindMemory/DreamDaemon.test.ts @@ -0,0 +1,747 @@ +/** + * Tests for DreamDaemon — Phase 9 orchestrator. + * + * Covers the full cycle (gates → lock → snapshot → extract via LLM → + * consolidate → write memory.md → prune log.md preserving tail → archive → + * tiered rollups → record run → release lock) plus the negative paths + * (gate skip, lock skip, force bypass, mid-run append survival, LLM + * failure, idempotent close). + * + * Fakes / fixtures: + * - in-memory better-sqlite3 (`:memory:`) + `migrate(db)` from Phase 7 + * - real tmp-dir vault + archive (Phase 3 modules) + * - `createFakeLLMClient` from Phase 8 `__fakes__` + * - injected clock for deterministic tiered-rollup gates + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import Database from 'better-sqlite3'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { migrate } from './dream-schema'; +import { + acquireLock, + getLock, + incrementTurnCount, + listRuns, + markPhaseComplete, + readState, + setLastConsolidatedTurnId, +} from './dream-state'; +import { createMindMemoryVault } from './MindMemoryVault'; +import { createMindArchiveStore } from './MindArchiveStore'; +import { STRUCTURED_LOG_SENTINEL, serializeTurn } from './StructuredLogFormat'; +import type { CompletedTurn } from '@chamber/shared/turn-observer'; +import { createFakeLLMClient, type FakeLLMClient } from './__fakes__/FakeLLMClient'; +import { + __resetMindMutexForTesting, +} from './consolidation-scheduler'; +import { + createDreamDaemon, + type DreamDaemon, + type DreamDaemonConfig, + type DreamRunResult, +} from './DreamDaemon'; + +const MS_PER_DAY = 24 * 60 * 60 * 1000; +const MS_PER_WEEK = 7 * MS_PER_DAY; + +const MIND_ID = 'test-mind'; +const FROZEN_NOW_MS = Date.parse('2026-05-12T15:00:00Z'); + +let mindRoot: string; +let db: Database.Database; +let llmClient: FakeLLMClient; +let now: number; + +const baseConfig: DreamDaemonConfig = { + memoryMaxBytes: 8192, + llmTimeoutMs: 60_000, + lockTtlMs: 300_000, + minTurnsBetweenRuns: 1, + minDailyIntervalMs: 0, // disabled by default; tests opt in by overriding + weeklyRollupAfterDailies: 7, + monthlyRollupAfterWeeklies: 4, + weeklyMinIntervalMs: MS_PER_WEEK, + monthlyMinIntervalMs: 30 * MS_PER_DAY, +}; + +beforeEach(() => { + mindRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-daemon-')); + db = new Database(':memory:'); + migrate(db); + llmClient = createFakeLLMClient({ + defaultResponse: + '## 12:00:00\n**[user-prompt]** I prefer kebab-case file names.\n', + }); + now = FROZEN_NOW_MS; + __resetMindMutexForTesting(); +}); + +afterEach(() => { + db.close(); + fs.rmSync(mindRoot, { recursive: true, force: true }); + __resetMindMutexForTesting(); +}); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function clock(): Date { + return new Date(now); +} + +function makeTurn(overrides: Partial = {}): CompletedTurn { + const turnId = overrides.turnId ?? `turn-${Math.random().toString(36).slice(2, 10)}`; + const ts = overrides.endedAt ?? new Date(now).toISOString(); + return { + turnId, + sessionId: overrides.sessionId ?? 'session-1', + model: overrides.model ?? 'gpt-test', + status: overrides.status ?? 'completed', + startedAt: overrides.startedAt ?? ts, + endedAt: ts, + prompt: overrides.prompt ?? `prompt for ${turnId}`, + finalAssistantMessage: + overrides.finalAssistantMessage ?? `assistant for ${turnId}`, + }; +} + +async function seedLog(turns: CompletedTurn[]): Promise { + await fsp.mkdir(path.join(mindRoot, '.working-memory'), { recursive: true }); + const body = `${STRUCTURED_LOG_SENTINEL}\n\n${turns.map(serializeTurn).join('')}`; + await fsp.writeFile(path.join(mindRoot, '.working-memory', 'log.md'), body); +} + +async function readLog(): Promise { + return fsp.readFile(path.join(mindRoot, '.working-memory', 'log.md'), 'utf-8'); +} + +function makeDaemon(configOverrides: Partial = {}): DreamDaemon { + const vault = createMindMemoryVault(mindRoot); + const archiveStore = createMindArchiveStore(mindRoot); + return createDreamDaemon({ + mindId: MIND_ID, + mindPath: mindRoot, + llmClient, + vault, + archiveStore, + db, + config: { ...baseConfig, ...configOverrides }, + clock, + }); +} + +function isoOf(ms: number): string { + return new Date(ms).toISOString(); +} + +async function waitForRelease( + getRelease: () => (() => void) | null, + timeoutMs = 2000, +): Promise<() => void> { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const r = getRelease(); + if (r !== null) return r; + await new Promise((resolve) => setTimeout(resolve, 5)); + } + throw new Error('timed out waiting for LLM synthesize to be invoked'); +} + +// --------------------------------------------------------------------------- +// Public surface +// --------------------------------------------------------------------------- + +describe('DreamDaemon — public surface', () => { + it('exposes run, forceRun, getStatus, notifyTurnCompleted, close', () => { + const daemon = makeDaemon(); + expect(typeof daemon.run).toBe('function'); + expect(typeof daemon.forceRun).toBe('function'); + expect(typeof daemon.getStatus).toBe('function'); + expect(typeof daemon.notifyTurnCompleted).toBe('function'); + expect(typeof daemon.close).toBe('function'); + }); + + it('initial getStatus reports phase=idle, locked=false, lastRunAt=null', () => { + const daemon = makeDaemon(); + const s = daemon.getStatus(); + expect(s.phase).toBe('idle'); + expect(s.locked).toBe(false); + expect(s.lastRunAt).toBeNull(); + expect(s.lastResult).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// Happy path +// --------------------------------------------------------------------------- + +describe('DreamDaemon — happy path cycle', () => { + it('runs end-to-end: writes memory.md, prunes log.md, archives, records run, advances cutoff', async () => { + const t1 = makeTurn({ turnId: 'turn-A' }); + const t2 = makeTurn({ turnId: 'turn-B' }); + await seedLog([t1, t2]); + incrementTurnCount(db, 2); + + const daemon = makeDaemon(); + const result = await daemon.run(); + + expect(result.status).toBe('success'); + if (result.status !== 'success') return; + expect(result.fromTurnId).toBeNull(); + expect(result.toTurnId).toBe('turn-B'); + expect(result.consolidatedCount).toBeGreaterThan(0); + expect(result.archivedCount).toBe(2); + + // memory.md exists and is non-empty + const memoryMd = await fsp.readFile( + path.join(mindRoot, '.working-memory', 'memory.md'), + 'utf-8', + ); + expect(memoryMd.length).toBeGreaterThan(0); + expect(memoryMd).toMatch(/kebab-case/i); + + // log.md retains sentinel; both consolidated turns are pruned + const log = await readLog(); + expect(log.startsWith(STRUCTURED_LOG_SENTINEL)).toBe(true); + expect(log).not.toContain('turn-A'); + expect(log).not.toContain('turn-B'); + + // archive/consolidated/ has 2 files + const archiveDir = path.join(mindRoot, '.working-memory', 'archive', 'consolidated'); + const files = await fsp.readdir(archiveDir); + expect(files).toHaveLength(2); + + // state advanced + const state = readState(db); + expect(state.lastConsolidatedTurnId).toBe('turn-B'); + expect(state.turnsSinceLastRun).toBe(0); + expect(state.lastDailyAt).not.toBeNull(); + + // dream_runs has a success row + const runs = listRuns(db, { phase: 'daily' }); + expect(runs).toHaveLength(1); + expect(runs[0]!.status).toBe('success'); + expect(runs[0]!.toTurnId).toBe('turn-B'); + + // lock released + expect(getLock(db, 'daily')).toBeNull(); + const status = daemon.getStatus(); + expect(status.locked).toBe(false); + expect(status.phase).toBe('idle'); + expect(status.lastRunAt).toBe(now); + }); + + it('only consolidates turns AFTER lastConsolidatedTurnId', async () => { + const t1 = makeTurn({ turnId: 'turn-old' }); + const t2 = makeTurn({ turnId: 'turn-new' }); + await seedLog([t1, t2]); + setLastConsolidatedTurnId(db, 'turn-old'); + incrementTurnCount(db, 1); + + const daemon = makeDaemon(); + const result = await daemon.run(); + + expect(result.status).toBe('success'); + if (result.status !== 'success') return; + expect(result.fromTurnId).toBe('turn-old'); + expect(result.toTurnId).toBe('turn-new'); + expect(result.archivedCount).toBe(1); + + const archiveFiles = await fsp.readdir( + path.join(mindRoot, '.working-memory', 'archive', 'consolidated'), + ); + expect(archiveFiles).toHaveLength(1); + expect(archiveFiles[0]).toContain('turn-new'); + }); + + it('returns skipped/no-turns when log.md has no turns past the cutoff', async () => { + const t1 = makeTurn({ turnId: 'turn-already-consolidated' }); + await seedLog([t1]); + setLastConsolidatedTurnId(db, 'turn-already-consolidated'); + incrementTurnCount(db, 1); + + const daemon = makeDaemon(); + const result = await daemon.run(); + expect(result).toEqual({ status: 'skipped', reason: 'no-turns' }); + + expect(llmClient.calls).toHaveLength(0); + expect(getLock(db, 'daily')).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// Gate / lock skip +// --------------------------------------------------------------------------- + +describe('DreamDaemon — gate skip', () => { + it('returns skipped/no-activity when activity counter is below threshold', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + // turnsSinceLastRun stays at 0 + const daemon = makeDaemon(); + const result = await daemon.run(); + expect(result).toEqual({ status: 'skipped', reason: 'no-activity' }); + expect(llmClient.calls).toHaveLength(0); + expect(getLock(db, 'daily')).toBeNull(); + // state untouched + expect(readState(db).lastConsolidatedTurnId).toBeNull(); + }); + + it('returns skipped/too-soon when last daily run was within minDailyIntervalMs', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + markPhaseComplete(db, 'daily', now - 1000); + + const daemon = makeDaemon({ minDailyIntervalMs: MS_PER_DAY }); + const result = await daemon.run(); + expect(result).toEqual({ status: 'skipped', reason: 'too-soon' }); + expect(llmClient.calls).toHaveLength(0); + }); +}); + +describe('DreamDaemon — lock skip', () => { + it('returns skipped/locked when another holder owns the daily lock', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + + acquireLock(db, { + phase: 'daily', + mindId: 'other-process', + now, + ttlMs: 60_000, + }); + + const daemon = makeDaemon(); + const result = await daemon.run(); + expect(result).toEqual({ status: 'skipped', reason: 'locked' }); + expect(llmClient.calls).toHaveLength(0); + + // The daemon must NOT have stolen / released the held lock + const lock = getLock(db, 'daily'); + expect(lock).not.toBeNull(); + expect(lock!.holder).not.toMatch(new RegExp(`:${MIND_ID}:`)); + }); + + it('concurrent forceRun calls: second call returns locked while first is in flight', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + + let releaseLLM: (() => void) | null = null; + llmClient = { + calls: [], + synthesize: (req) => { + (llmClient.calls as unknown as typeof llmClient.calls[number][]).push(req); + return new Promise((resolve) => { + releaseLLM = () => + resolve('## 12:00:00\n**[user-prompt]** I prefer kebab-case.\n'); + }); + }, + } as FakeLLMClient; + + const daemon = makeDaemon(); + const first = daemon.forceRun(); + // Wait until the first call actually enters synthesize. + const release = await waitForRelease(() => releaseLLM); + + const second = await daemon.forceRun(); + expect(second).toEqual({ status: 'skipped', reason: 'locked' }); + + release(); + const firstResult = await first; + expect(firstResult.status).toBe('success'); + }); +}); + +// --------------------------------------------------------------------------- +// forceRun +// --------------------------------------------------------------------------- + +describe('DreamDaemon — forceRun', () => { + it('bypasses the activity gate', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + // turnsSinceLastRun = 0 → would be no-activity for run() + + const daemon = makeDaemon(); + const result = await daemon.forceRun(); + expect(result.status).toBe('success'); + }); + + it('bypasses the time gate', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + markPhaseComplete(db, 'daily', now - 1000); + + const daemon = makeDaemon({ minDailyIntervalMs: MS_PER_DAY }); + const result = await daemon.forceRun(); + expect(result.status).toBe('success'); + }); + + it('still respects an externally held lock', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + acquireLock(db, { + phase: 'daily', + mindId: 'other-process', + now, + ttlMs: 60_000, + }); + const daemon = makeDaemon(); + const result = await daemon.forceRun(); + expect(result).toEqual({ status: 'skipped', reason: 'locked' }); + }); +}); + +// --------------------------------------------------------------------------- +// Mid-run append +// --------------------------------------------------------------------------- + +describe('DreamDaemon — mid-run append', () => { + it('preserves a turn appended to log.md AFTER the snapshot is taken', async () => { + const t1 = makeTurn({ turnId: 'turn-pre' }); + await seedLog([t1]); + incrementTurnCount(db, 1); + + let releaseLLM: (() => void) | null = null; + llmClient = { + calls: [] as unknown as FakeLLMClient['calls'], + synthesize: (req) => { + (llmClient.calls as unknown as typeof llmClient.calls[number][]).push(req); + return new Promise((resolve) => { + releaseLLM = () => + resolve( + '## 12:00:00\n**[user-prompt]** I prefer kebab-case.\n', + ); + }); + }, + } as FakeLLMClient; + + const daemon = makeDaemon(); + const runPromise = daemon.run(); + + // Wait until the daemon actually enters the synthesize await. + const release = await waitForRelease(() => releaseLLM); + + // Append a new frame to log.md while the LLM call is paused. + const tail = makeTurn({ turnId: 'turn-tail', endedAt: isoOf(now + 1000) }); + await fsp.appendFile( + path.join(mindRoot, '.working-memory', 'log.md'), + serializeTurn(tail), + ); + + release(); + const result = await runPromise; + expect(result.status).toBe('success'); + if (result.status !== 'success') return; + + // turn-pre was consolidated and pruned; turn-tail must remain. + const log = await readLog(); + expect(log).toContain('turn-tail'); + expect(log).not.toContain('turn-pre'); + + // cutoff advanced only to turn-pre, NOT past the tail entry. + expect(result.toTurnId).toBe('turn-pre'); + expect(readState(db).lastConsolidatedTurnId).toBe('turn-pre'); + }); +}); + +// --------------------------------------------------------------------------- +// E2E mid-cycle sleep shim (test-only) +// --------------------------------------------------------------------------- + +describe('DreamDaemon — E2E mid-cycle sleep shim', () => { + it('is a no-op when CHAMBER_E2E is unset, even if CHAMBER_DREAM_TEST_SLEEP_MS is set', async () => { + vi.stubEnv('CHAMBER_E2E', ''); + vi.stubEnv('CHAMBER_DREAM_TEST_SLEEP_MS', '5000'); + try { + await seedLog([makeTurn({ turnId: 'turn-noop' })]); + incrementTurnCount(db, 1); + const daemon = makeDaemon(); + const started = Date.now(); + const result = await daemon.run(); + const elapsed = Date.now() - started; + expect(result.status).toBe('success'); + // Without the env gate, the sleep is bypassed entirely. + expect(elapsed).toBeLessThan(2000); + } finally { + vi.unstubAllEnvs(); + } + }); + + it('pauses between the snapshot and the prune when CHAMBER_E2E=1 AND CHAMBER_DREAM_TEST_SLEEP_MS is positive', async () => { + vi.stubEnv('CHAMBER_E2E', '1'); + vi.stubEnv('CHAMBER_DREAM_TEST_SLEEP_MS', '120'); + try { + await seedLog([makeTurn({ turnId: 'turn-sleep' })]); + incrementTurnCount(db, 1); + const daemon = makeDaemon(); + const started = Date.now(); + const result = await daemon.run(); + const elapsed = Date.now() - started; + expect(result.status).toBe('success'); + expect(elapsed).toBeGreaterThanOrEqual(100); + } finally { + vi.unstubAllEnvs(); + } + }); + + it('ignores a non-finite or non-positive CHAMBER_DREAM_TEST_SLEEP_MS value', async () => { + vi.stubEnv('CHAMBER_E2E', '1'); + vi.stubEnv('CHAMBER_DREAM_TEST_SLEEP_MS', 'banana'); + try { + await seedLog([makeTurn({ turnId: 'turn-bad-env' })]); + incrementTurnCount(db, 1); + const daemon = makeDaemon(); + const started = Date.now(); + const result = await daemon.run(); + const elapsed = Date.now() - started; + expect(result.status).toBe('success'); + expect(elapsed).toBeLessThan(2000); + } finally { + vi.unstubAllEnvs(); + } + }); +}); + +// --------------------------------------------------------------------------- +// LLM failures +// --------------------------------------------------------------------------- + +describe('DreamDaemon — error handling', () => { + it('LLM error: returns failed result, releases lock, leaves state untouched', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + + llmClient = createFakeLLMClient({ error: new Error('boom') }); + const daemon = makeDaemon(); + + const result = await daemon.run(); + expect(result.status).toBe('failed'); + if (result.status === 'failed') { + expect(result.reason).toContain('boom'); + } + + // State did NOT advance; cutoff still null + const state = readState(db); + expect(state.lastConsolidatedTurnId).toBeNull(); + expect(state.turnsSinceLastRun).toBe(1); // not reset on failure + + // Lock was released + expect(getLock(db, 'daily')).toBeNull(); + + // memory.md was NOT written + expect( + fs.existsSync(path.join(mindRoot, '.working-memory', 'memory.md')), + ).toBe(false); + + // log.md was NOT pruned + const log = await readLog(); + expect(log).toContain('turn-1'); + + // dream_runs has a failed row + const runs = listRuns(db, { phase: 'daily' }); + expect(runs).toHaveLength(1); + expect(runs[0]!.status).toBe('failed'); + }); + + it('LLM timeout: returns failed result with timeout reason, releases lock', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + + llmClient = createFakeLLMClient({ latencyMs: 1000 }); + const daemon = makeDaemon({ llmTimeoutMs: 5 }); + const result = await daemon.run(); + expect(result.status).toBe('failed'); + if (result.status === 'failed') { + expect(result.reason).toMatch(/timed out/i); + } + expect(getLock(db, 'daily')).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// Tiered rollups +// --------------------------------------------------------------------------- + +describe('DreamDaemon — tiered weekly rollup', () => { + async function preSeedDailyArchives(count: number): Promise { + const archive = createMindArchiveStore(mindRoot); + for (let i = 0; i < count; i++) { + await archive.writeConsolidated({ + turnId: `seed-${i}`, + timestamp: isoOf(now - (count - i) * 60_000), + content: `seed body ${i} I prefer kebab-case file names.`, + }); + } + } + + it('triggers when daily-archive count meets threshold and weekly is due', async () => { + await preSeedDailyArchives(7); + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + // lastWeeklyAt unset → due + const daemon = makeDaemon(); + const result = await daemon.run(); + expect(result.status).toBe('success'); + if (result.status !== 'success') return; + expect(result.weeklyArchived).toBe(true); + + const weeklyDir = path.join(mindRoot, '.working-memory', 'archive', 'weekly'); + const weeklyFiles = await fsp.readdir(weeklyDir); + expect(weeklyFiles.length).toBeGreaterThan(0); + + expect(readState(db).lastWeeklyAt).not.toBeNull(); + }); + + it('does NOT trigger when fewer than threshold daily archives exist', async () => { + await preSeedDailyArchives(3); + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + + const daemon = makeDaemon(); + const result = await daemon.run(); + expect(result.status).toBe('success'); + if (result.status !== 'success') return; + expect(result.weeklyArchived).toBe(false); + + const weeklyDir = path.join(mindRoot, '.working-memory', 'archive', 'weekly'); + expect(fs.existsSync(weeklyDir)).toBe(false); + expect(readState(db).lastWeeklyAt).toBeNull(); + }); + + it('does NOT trigger when weekly was already done within weeklyMinIntervalMs', async () => { + await preSeedDailyArchives(7); + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + markPhaseComplete(db, 'weekly', now - 1000); + + const daemon = makeDaemon(); + const result = await daemon.run(); + expect(result.status).toBe('success'); + if (result.status !== 'success') return; + expect(result.weeklyArchived).toBe(false); + }); +}); + +describe('DreamDaemon — tiered monthly rollup', () => { + async function preSeedDailyArchives(count: number): Promise { + const archive = createMindArchiveStore(mindRoot); + for (let i = 0; i < count; i++) { + await archive.writeConsolidated({ + turnId: `seed-${i}`, + timestamp: isoOf(now - (count - i) * 60_000), + content: `seed body ${i} I prefer kebab-case.`, + }); + } + } + + async function preSeedWeeklyArchives(count: number): Promise { + const archive = createMindArchiveStore(mindRoot); + for (let i = 0; i < count; i++) { + await archive.writeWeekly( + `2026-W0${i + 1}`, + `weekly summary ${i} I prefer kebab-case.`, + ); + } + } + + it('triggers when weekly-archive count meets threshold and monthly is due', async () => { + await preSeedDailyArchives(7); + await preSeedWeeklyArchives(4); + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + + const daemon = makeDaemon(); + const result = await daemon.run(); + expect(result.status).toBe('success'); + if (result.status !== 'success') return; + expect(result.monthlyArchived).toBe(true); + + const monthlyDir = path.join(mindRoot, '.working-memory', 'archive', 'monthly'); + const monthlyFiles = await fsp.readdir(monthlyDir); + expect(monthlyFiles.length).toBeGreaterThan(0); + expect(readState(db).lastMonthlyAt).not.toBeNull(); + }); + + it('does NOT trigger when fewer than threshold weeklies exist', async () => { + await preSeedWeeklyArchives(2); + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + + const daemon = makeDaemon(); + const result = await daemon.run(); + expect(result.status).toBe('success'); + if (result.status !== 'success') return; + expect(result.monthlyArchived).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// notifyTurnCompleted + close +// --------------------------------------------------------------------------- + +describe('DreamDaemon — notifyTurnCompleted', () => { + it('is a no-op (does not throw, does not mutate state)', () => { + const daemon = makeDaemon(); + const before = readState(db); + daemon.notifyTurnCompleted(makeTurn({ turnId: 'turn-noop' })); + const after = readState(db); + expect(after).toEqual(before); + }); +}); + +describe('DreamDaemon — close', () => { + it('is idempotent and rejects subsequent run() calls', async () => { + const daemon = makeDaemon(); + await daemon.close(); + await daemon.close(); + + const result = await daemon.run(); + expect(result.status).toBe('failed'); + if (result.status === 'failed') { + expect(result.reason).toMatch(/closed/i); + } + }); +}); + +// --------------------------------------------------------------------------- +// Status reflects current phase +// --------------------------------------------------------------------------- + +describe('DreamDaemon — getStatus mid-run', () => { + it('reports locked=true while a run is in progress', async () => { + await seedLog([makeTurn({ turnId: 'turn-1' })]); + incrementTurnCount(db, 1); + + let releaseLLM: (() => void) | null = null; + llmClient = { + calls: [] as unknown as FakeLLMClient['calls'], + synthesize: (req) => { + (llmClient.calls as unknown as typeof llmClient.calls[number][]).push(req); + return new Promise((resolve) => { + releaseLLM = () => + resolve('## 12:00:00\n**[user-prompt]** I prefer kebab-case.\n'); + }); + }, + } as FakeLLMClient; + + const daemon = makeDaemon(); + const runPromise = daemon.run(); + const release = await waitForRelease(() => releaseLLM); + + const midStatus = daemon.getStatus(); + expect(midStatus.locked).toBe(true); + expect(midStatus.phase).not.toBe('idle'); + + release(); + const result = await runPromise; + expect(result.status).toBe('success'); + + const finalStatus = daemon.getStatus(); + expect(finalStatus.locked).toBe(false); + expect(finalStatus.phase).toBe('idle'); + expect(finalStatus.lastResult).toEqual(result as DreamRunResult); + }); +}); diff --git a/packages/services/src/mindMemory/DreamDaemon.ts b/packages/services/src/mindMemory/DreamDaemon.ts new file mode 100644 index 00000000..740f1989 --- /dev/null +++ b/packages/services/src/mindMemory/DreamDaemon.ts @@ -0,0 +1,607 @@ +/** + * DreamDaemon — per-mind background memory consolidation orchestrator. + * + * Composes the Phase 1-8 modules into the canonical cycle: + * + * 1. Gate check (lock + activity + time) + * 2. Acquire lock (in-process mutex layered with DB lock) + * 3. Snapshot turns from log.md UP TO the cutoff (last turn id at + * snapshot time) — anything appended later survives the prune + * 4. Drive `llmClient.synthesize` to extract memorable items from the + * snapshot + * 5. Run the four-phase consolidation pipeline + * 6. Atomically write `memory.md` capped at `memoryMaxBytes` + * 7. Re-read log.md, prune ONLY the snapshot turn ids, write back + * 8. Archive each consolidated source turn + * 9. Tiered weekly / monthly rollups when archive thresholds are met + * 10. Record the run, advance state, release the lock + * + * Critical correctness rules (enforced by tests): + * - Mid-run append must NOT lose turns (re-read before prune). + * - Errors during the cycle MUST release the lock in `finally`. + * - `forceRun` bypasses gates but NOT the lock. + * - Lock skip vs gate skip vs no-turns are distinguishable in the + * returned `DreamRunResult`. + * - All vault/archive/db are injected; tests use real tmp dirs + + * in-memory sqlite + the `__fakes__/FakeLLMClient`. + */ + +import type Database from 'better-sqlite3'; + +import type { CompletedTurn } from '@chamber/shared/turn-observer'; + +import { Logger } from '../logger'; +import { runConsolidation } from './consolidation'; +import { + __resetMindMutexForTesting, + withMindMutex, +} from './consolidation-scheduler'; +import { evaluateGates } from './dream-gates'; +import { + acquireLock, + buildLockHolder, + getLock, + markPhaseComplete, + recordRun, + releaseLock, + resetActivityCounter, + readState, + setLastConsolidatedTurnId, +} from './dream-state'; +import { extractFromLog } from './extraction'; +import type { LLMClient } from './LLMClient'; +import type { MindArchiveStore } from './MindArchiveStore'; +import type { MindMemoryVault } from './MindMemoryVault'; +import { + parseLog, + serializeTurn, + STRUCTURED_LOG_SENTINEL, + type ParsedTurn, +} from './StructuredLogFormat'; + +// --------------------------------------------------------------------------- +// Public surface +// --------------------------------------------------------------------------- + +export type DreamDaemonPhase = + | 'idle' + | 'gating' + | 'snapshot' + | 'extracting' + | 'consolidating' + | 'writing' + | 'pruning' + | 'archiving' + | 'rolling-up' + | 'recording'; + +export type DreamSkipReason = 'no-activity' | 'too-soon' | 'locked' | 'no-turns'; + +export type DreamRunResult = + | { + readonly status: 'success'; + readonly extractedCount: number; + readonly consolidatedCount: number; + readonly archivedCount: number; + readonly fromTurnId: string | null; + readonly toTurnId: string; + readonly weeklyArchived: boolean; + readonly monthlyArchived: boolean; + } + | { readonly status: 'skipped'; readonly reason: DreamSkipReason } + | { readonly status: 'failed'; readonly reason: string; readonly phase: DreamDaemonPhase }; + +export interface DreamStatus { + readonly phase: DreamDaemonPhase; + readonly locked: boolean; + readonly lastRunAt: number | null; + readonly lastResult: DreamRunResult | null; +} + +export interface DreamDaemonConfig { + readonly memoryMaxBytes: number; + readonly llmTimeoutMs: number; + readonly lockTtlMs: number; + readonly minTurnsBetweenRuns: number; + readonly minDailyIntervalMs: number; + readonly weeklyRollupAfterDailies: number; + readonly monthlyRollupAfterWeeklies: number; + readonly weeklyMinIntervalMs: number; + readonly monthlyMinIntervalMs: number; +} + +export interface DreamDaemonOptions { + readonly mindId: string; + readonly mindPath: string; + readonly llmClient: LLMClient; + readonly vault: MindMemoryVault; + readonly archiveStore: MindArchiveStore; + readonly db: Database.Database; + readonly config: DreamDaemonConfig; + readonly clock?: () => Date; + readonly logger?: Logger; +} + +export interface DreamDaemon { + run(): Promise; + forceRun(): Promise; + getStatus(): DreamStatus; + notifyTurnCompleted(turn: CompletedTurn): void; + close(): Promise; +} + +// --------------------------------------------------------------------------- +// Implementation +// --------------------------------------------------------------------------- + +const LOG_REL_PATH = 'log.md'; +const MEMORY_REL_PATH = 'memory.md'; + +export function createDreamDaemon(opts: DreamDaemonOptions): DreamDaemon { + const { + mindId, + llmClient, + vault, + archiveStore, + db, + config, + } = opts; + const clock = opts.clock ?? (() => new Date()); + const logger = opts.logger ?? Logger.create(`DreamDaemon[${mindId}]`); + + let phase: DreamDaemonPhase = 'idle'; + let lastRunAt: number | null = null; + let lastResult: DreamRunResult | null = null; + let closed = false; + + function getStatus(): DreamStatus { + return { + phase, + locked: phase !== 'idle', + lastRunAt, + lastResult, + }; + } + + function setLastResult(result: DreamRunResult): DreamRunResult { + lastResult = result; + return result; + } + + async function run(): Promise { + return executeCycle({ bypassGates: false }); + } + + async function forceRun(): Promise { + return executeCycle({ bypassGates: true }); + } + + async function executeCycle(args: { + readonly bypassGates: boolean; + }): Promise { + if (closed) { + return setLastResult({ + status: 'failed', + reason: 'daemon closed', + phase: 'idle', + }); + } + + const now = clock().getTime(); + + // Gate phase — only when not forced. The lock gate ALWAYS runs (force + // bypasses activity + time, never the lock). + phase = 'gating'; + const lockHeld = isLockHeld(now); + if (lockHeld) { + phase = 'idle'; + return setLastResult({ status: 'skipped', reason: 'locked' }); + } + + if (!args.bypassGates) { + const gate = evaluateGates( + { + phase: 'daily', + state: readState(db), + now, + lockHeld: false, + }, + { + minTurnsBetweenRuns: config.minTurnsBetweenRuns, + minIntervalMs: config.minDailyIntervalMs, + }, + ); + if (!gate.run) { + phase = 'idle'; + // `evaluateGates` only returns 'locked' when lockHeld=true, which we + // already handled above; the remaining reasons map 1:1 to skip. + if (gate.reason === 'ready' || gate.reason === 'locked') { + phase = 'idle'; + return setLastResult({ status: 'skipped', reason: 'locked' }); + } + return setLastResult({ status: 'skipped', reason: gate.reason }); + } + } + + // Try the in-process mutex first. Fail-fast on a concurrent caller so + // the second `run()` returns `locked` instead of queuing. + const mutexResult = await withMindMutex(mindId, async () => { + return runUnderLock(now); + }); + + if (!mutexResult.acquired) { + phase = 'idle'; + return setLastResult({ status: 'skipped', reason: 'locked' }); + } + + return mutexResult.value; + } + + function isLockHeld(now: number): boolean { + const existing = getLock(db, 'daily'); + if (existing === null) return false; + return existing.expiresAt > now; + } + + async function runUnderLock(now: number): Promise { + // Step 2 — acquire DB lock. + const lockHolder = buildLockHolder(mindId); + const lockResult = acquireLock(db, { + phase: 'daily', + mindId, + uuid: extractHolderUuid(lockHolder), + now, + ttlMs: config.lockTtlMs, + }); + + if (!lockResult.acquired) { + phase = 'idle'; + return setLastResult({ status: 'skipped', reason: 'locked' }); + } + + // From here on, the DB lock MUST be released no matter what. + let releasedByUs = false; + const acquiredHolder = lockResult.holder ?? lockHolder; + + try { + const cycleResult = await runCycleLocked(now); + lastRunAt = clock().getTime(); + return setLastResult(cycleResult); + } catch (err) { + logger.error('cycle threw', err); + phase = 'idle'; + lastRunAt = clock().getTime(); + const reason = err instanceof Error ? err.message : String(err); + // Record the failure even on unexpected throws so operators have a + // trail in dream_runs. + try { + recordRun(db, { + phase: 'daily', + startedAt: now, + endedAt: clock().getTime(), + status: 'failed', + reason, + }); + } catch (recordErr) { + logger.error('failed to record cycle failure', recordErr); + } + return setLastResult({ status: 'failed', reason, phase }); + } finally { + try { + releaseLock(db, 'daily', acquiredHolder); + releasedByUs = true; + } catch (releaseErr) { + logger.error('failed to release lock', releaseErr); + } + if (!releasedByUs) { + // Best-effort: if the typed release threw, attempt a direct delete + // so we never wedge the daemon. Swallow nested errors — the lock + // will be stolen on its TTL anyway. + try { + db.prepare( + 'DELETE FROM dream_locks WHERE phase = ? AND holder = ?', + ).run('daily', acquiredHolder); + } catch { + /* ignore */ + } + } + phase = 'idle'; + } + } + + async function runCycleLocked(now: number): Promise { + // Step 3 — snapshot turns from log.md. + phase = 'snapshot'; + const snapshot = await readSnapshot(); + const cutoffIndex = findCutoffIndex(snapshot.turns, snapshot.lastConsolidated); + const inScopeSnapshot = snapshot.turns.slice(cutoffIndex); + const consolidatedIds = new Set(inScopeSnapshot.map((t) => t.turnId)); + + if (inScopeSnapshot.length === 0) { + recordRun(db, { + phase: 'daily', + startedAt: now, + endedAt: clock().getTime(), + status: 'skipped', + reason: 'no-turns', + }); + return { status: 'skipped', reason: 'no-turns' }; + } + + // Step 4 — extract via LLM. + phase = 'extracting'; + const prompt = buildSynthesisPrompt(inScopeSnapshot); + const llmResponse = await llmClient.synthesize({ + prompt, + timeoutMs: config.llmTimeoutMs, + }); + const referenceDate = new Date(now); + const isoDate = referenceDate.toISOString().slice(0, 10); + const newEntries = extractFromLog(llmResponse, isoDate); + + // Step 5 — consolidate. + phase = 'consolidating'; + const currentMemoryMd = (await vault.read(MEMORY_REL_PATH)) ?? ''; + const consolidation = runConsolidation({ + currentMemoryMd, + newEntries, + referenceDate, + }); + const memoryMd = capBytes(consolidation.memoryMd, config.memoryMaxBytes); + + // Step 6 — atomic write of memory.md. + phase = 'writing'; + await vault.write(MEMORY_REL_PATH, memoryMd); + + // Test-only mid-cycle sleep used by M7 (mid-run append race). + // Honored ONLY when CHAMBER_E2E=1 so production builds never read the + // env var. Inserted between the memory.md write and the prune phase + // so a test can append a turn AFTER the snapshot was taken and assert + // the tail entry survives the prune. + await maybeTestSleep(); + + // Step 7 — re-read log.md and prune only the snapshot turn ids. Tail + // entries appended during the LLM call MUST survive. + phase = 'pruning'; + await prunePersistedLog(consolidatedIds); + + // Step 8 — archive each consolidated source turn. + phase = 'archiving'; + for (const turn of inScopeSnapshot) { + await archiveStore.writeConsolidated({ + turnId: turn.turnId, + timestamp: turn.timestamp, + content: serializeTurn({ + turnId: turn.turnId, + sessionId: turn.sessionId, + model: turn.model, + status: turn.status, + startedAt: turn.timestamp, + endedAt: turn.timestamp, + prompt: turn.prompt, + finalAssistantMessage: turn.assistant, + }), + }); + } + + // Step 9 — tiered rollups. + phase = 'rolling-up'; + const weeklyArchived = await maybeRollupWeekly(now); + const monthlyArchived = await maybeRollupMonthly(now); + + // Step 10 — record run + advance state. + phase = 'recording'; + const lastTurnId = inScopeSnapshot[inScopeSnapshot.length - 1]!.turnId; + setLastConsolidatedTurnId(db, lastTurnId); + resetActivityCounter(db); + markPhaseComplete(db, 'daily', now); + recordRun(db, { + phase: 'daily', + startedAt: now, + endedAt: clock().getTime(), + status: 'success', + fromTurnId: snapshot.lastConsolidated, + toTurnId: lastTurnId, + }); + + return { + status: 'success', + extractedCount: newEntries.length, + consolidatedCount: consolidation.entriesKept, + archivedCount: inScopeSnapshot.length, + fromTurnId: snapshot.lastConsolidated, + toTurnId: lastTurnId, + weeklyArchived, + monthlyArchived, + }; + } + + async function readSnapshot(): Promise<{ + readonly turns: readonly ParsedTurn[]; + readonly lastConsolidated: string | null; + }> { + const content = (await vault.read(LOG_REL_PATH)) ?? ''; + const parsed = parseLog(content); + const state = readState(db); + return { + turns: parsed.turns, + lastConsolidated: state.lastConsolidatedTurnId, + }; + } + + function findCutoffIndex( + turns: readonly ParsedTurn[], + lastConsolidated: string | null, + ): number { + if (lastConsolidated === null) return 0; + const idx = turns.findIndex((t) => t.turnId === lastConsolidated); + if (idx === -1) return 0; + return idx + 1; + } + + async function prunePersistedLog(consolidatedIds: Set): Promise { + const content = (await vault.read(LOG_REL_PATH)) ?? ''; + const parsed = parseLog(content); + const survivors = parsed.turns.filter((t) => !consolidatedIds.has(t.turnId)); + const body = survivors + .map((t) => + serializeTurn({ + turnId: t.turnId, + sessionId: t.sessionId, + model: t.model, + status: t.status, + startedAt: t.timestamp, + endedAt: t.timestamp, + prompt: t.prompt, + finalAssistantMessage: t.assistant, + }), + ) + .join(''); + const next = `${STRUCTURED_LOG_SENTINEL}\n\n${body}`; + await vault.write(LOG_REL_PATH, next); + } + + async function maybeRollupWeekly(now: number): Promise { + const dailies = await archiveStore.listConsolidated(); + if (dailies.length < config.weeklyRollupAfterDailies) return false; + const state = readState(db); + if ( + state.lastWeeklyAt !== null && + now - state.lastWeeklyAt < config.weeklyMinIntervalMs + ) { + return false; + } + const weekKey = isoWeekKey(new Date(now)); + const summary = `# Weekly Rollup ${weekKey}\n\n${dailies + .slice(-config.weeklyRollupAfterDailies) + .map((name) => `- ${name}`) + .join('\n')}\n`; + await archiveStore.writeWeekly(weekKey, summary); + markPhaseComplete(db, 'weekly', now); + return true; + } + + async function maybeRollupMonthly(now: number): Promise { + const weeklies = await archiveStore.listWeekly(); + if (weeklies.length < config.monthlyRollupAfterWeeklies) return false; + const state = readState(db); + if ( + state.lastMonthlyAt !== null && + now - state.lastMonthlyAt < config.monthlyMinIntervalMs + ) { + return false; + } + const monthKey = isoMonthKey(new Date(now)); + const summary = `# Monthly Rollup ${monthKey}\n\n${weeklies + .slice(-config.monthlyRollupAfterWeeklies) + .map((name) => `- ${name}`) + .join('\n')}\n`; + await archiveStore.writeMonthly(monthKey, summary); + markPhaseComplete(db, 'monthly', now); + return true; + } + + function notifyTurnCompleted(_turn: CompletedTurn): void { + // No-op by design: DailyLogWriter increments the activity counter in + // its own `onTurnRecorded` path so the daemon does not need to. Kept + // on the public surface so the InternalScheduler (Phase 10) can wire + // turn observers without branching. + void _turn; + } + + async function close(): Promise { + closed = true; + // Drop any in-process mutex this daemon may hold so a fresh daemon + // (e.g., after a restart in tests) can re-enter cleanly. + __resetMindMutexForTesting(); + } + + return { + run, + forceRun, + getStatus, + notifyTurnCompleted, + close, + }; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function buildSynthesisPrompt(turns: readonly ParsedTurn[]): string { + const lines: string[] = []; + lines.push( + 'Extract memorable items (preferences, decisions, prohibitions, references) from the following completed turns.', + ); + lines.push( + 'Respond using daily-log format: each item on a "## HH:MM:SS" header line followed by lines of "**[type]** content".', + ); + lines.push(''); + for (const t of turns) { + const time = t.timestamp.slice(11, 19); + lines.push(`## ${time}`); + lines.push(`**[user-prompt]** ${oneLine(t.prompt)}`); + lines.push(`**[assistant]** ${oneLine(t.assistant)}`); + lines.push(''); + } + return lines.join('\n'); +} + +function oneLine(text: string): string { + return text.replace(/\s+/g, ' ').trim(); +} + +function capBytes(text: string, maxBytes: number): string { + if (Buffer.byteLength(text, 'utf-8') <= maxBytes) return text; + // Trim from the tail until the encoded length fits. We avoid mid-codepoint + // truncation by stepping one character at a time. + let truncated = text; + while (Buffer.byteLength(truncated, 'utf-8') > maxBytes && truncated.length > 0) { + truncated = truncated.slice(0, -1); + } + return truncated; +} + +function extractHolderUuid(holder: string): string { + // holder is "dream-daemon:::". The uuid is the last + // colon-delimited field. + const idx = holder.lastIndexOf(':'); + return idx === -1 ? holder : holder.slice(idx + 1); +} + +function isoWeekKey(date: Date): string { + // Compute ISO 8601 week date (YYYY-Www) — Monday starts the week and the + // first week of the year contains January 4. + const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + const dayNum = target.getUTCDay() || 7; + target.setUTCDate(target.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1)); + const weekNum = Math.ceil(((target.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7); + return `${target.getUTCFullYear()}-W${String(weekNum).padStart(2, '0')}`; +} + +function isoMonthKey(date: Date): string { + const yyyy = date.getUTCFullYear(); + const mm = String(date.getUTCMonth() + 1).padStart(2, '0'); + return `${yyyy}-${mm}`; +} + +/** + * Test-only mid-cycle pause. Returns immediately unless BOTH: + * - `CHAMBER_E2E=1` (the global test-surface gate the rest of the app + * uses to expose `IPC.E2E.*` handlers and `electronAPI.e2e`) + * - `CHAMBER_DREAM_TEST_SLEEP_MS` is a positive finite number + * + * Production builds never see CHAMBER_E2E=1, so this function is a no-op + * by construction. Used by M7 to inject an append between the snapshot + * and the prune. + */ +async function maybeTestSleep(): Promise { + if (process.env.CHAMBER_E2E !== '1') return; + const raw = process.env.CHAMBER_DREAM_TEST_SLEEP_MS; + if (!raw) return; + const ms = Number(raw); + if (!Number.isFinite(ms) || ms <= 0) return; + await new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/packages/services/src/mindMemory/InternalScheduler.test.ts b/packages/services/src/mindMemory/InternalScheduler.test.ts new file mode 100644 index 00000000..cbd04dd1 --- /dev/null +++ b/packages/services/src/mindMemory/InternalScheduler.test.ts @@ -0,0 +1,333 @@ +/** + * Tests for InternalScheduler — Phase 10. + * + * Per-mind in-process croner job runner that drives DreamDaemon.run() on a + * configurable cadence. Deliberately NOT registered with the user-facing + * CronService so the user's cron list stays clean. + * + * croner is driven off `Date.now()` and `setTimeout`. Vitest 4's + * `vi.useFakeTimers()` fakes both by default, so deterministic assertions + * on cron firings work as long as we advance time with + * `vi.advanceTimersByTimeAsync`. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + createInternalScheduler, + type InternalScheduler, +} from './InternalScheduler'; + +const EVERY_SECOND = '* * * * * *'; + +let scheduler: InternalScheduler; + +beforeEach(() => { + vi.useFakeTimers(); + // Pin the wall clock so croner's "next second boundary" is predictable. + vi.setSystemTime(new Date('2026-05-12T15:00:00.000Z')); + scheduler = createInternalScheduler({ random: () => 0 }); +}); + +afterEach(() => { + scheduler.close(); + vi.useRealTimers(); +}); + +function deferred(): { + promise: Promise; + resolve: (value: T) => void; + reject: (err: unknown) => void; +} { + let resolve!: (value: T) => void; + let reject!: (err: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +describe('createInternalScheduler', () => { + describe('register', () => { + it('exposes the registered cron expression via list()', () => { + scheduler.register({ + mindId: 'mind-a', + cronExpr: '0 3 * * *', + fn: async () => undefined, + }); + + const entries = scheduler.list(); + expect(entries.size).toBe(1); + expect(entries.get('mind-a')).toBe('0 3 * * *'); + }); + + it('throws synchronously on an invalid cron expression', () => { + expect(() => + scheduler.register({ + mindId: 'mind-bad', + cronExpr: 'not-a-cron', + fn: async () => undefined, + }), + ).toThrow(); + expect(scheduler.list().has('mind-bad')).toBe(false); + }); + + it('replaces an existing entry for the same mindId — old fn no longer fires', async () => { + const oldCalls: number[] = []; + const newCalls: number[] = []; + + scheduler.register({ + mindId: 'mind-a', + cronExpr: EVERY_SECOND, + fn: async () => { + oldCalls.push(Date.now()); + }, + }); + + // Replace before the next tick fires. + scheduler.register({ + mindId: 'mind-a', + cronExpr: EVERY_SECOND, + fn: async () => { + newCalls.push(Date.now()); + }, + }); + + // Advance well past two cron boundaries; only the new fn should fire. + await vi.advanceTimersByTimeAsync(2_500); + + expect(oldCalls).toHaveLength(0); + expect(newCalls.length).toBeGreaterThanOrEqual(1); + }); + }); + + describe('unregister', () => { + it('is a no-op for an unknown mindId', () => { + expect(() => scheduler.unregister('never-registered')).not.toThrow(); + }); + + it('stops only the targeted mind — other minds keep firing', async () => { + const aCalls: number[] = []; + const bCalls: number[] = []; + + scheduler.register({ + mindId: 'mind-a', + cronExpr: EVERY_SECOND, + fn: async () => { + aCalls.push(Date.now()); + }, + }); + scheduler.register({ + mindId: 'mind-b', + cronExpr: EVERY_SECOND, + fn: async () => { + bCalls.push(Date.now()); + }, + }); + + // Let one tick happen for both, then drop mind-a. + await vi.advanceTimersByTimeAsync(1_500); + const aBefore = aCalls.length; + const bBefore = bCalls.length; + expect(aBefore).toBeGreaterThanOrEqual(1); + expect(bBefore).toBeGreaterThanOrEqual(1); + + scheduler.unregister('mind-a'); + expect(scheduler.list().has('mind-a')).toBe(false); + expect(scheduler.list().has('mind-b')).toBe(true); + + await vi.advanceTimersByTimeAsync(2_000); + + expect(aCalls.length).toBe(aBefore); + expect(bCalls.length).toBeGreaterThan(bBefore); + }); + }); + + describe('runNow', () => { + it('invokes the registered fn immediately, out of band', async () => { + let calls = 0; + scheduler.register({ + mindId: 'mind-a', + cronExpr: '0 3 * * *', // never naturally fires within the test window + fn: async () => { + calls += 1; + }, + }); + + await scheduler.runNow('mind-a'); + + expect(calls).toBe(1); + }); + + it('throws when the mind is not registered', async () => { + await expect(scheduler.runNow('nobody')).rejects.toThrow(/not registered/i); + }); + + it('returns the in-flight promise when a tick is already executing', async () => { + const gate = deferred(); + let calls = 0; + + scheduler.register({ + mindId: 'mind-a', + cronExpr: '0 3 * * *', + fn: async () => { + calls += 1; + await gate.promise; + }, + }); + + const first = scheduler.runNow('mind-a'); + // Yield so the fn body starts and registers its in-flight promise. + await Promise.resolve(); + const second = scheduler.runNow('mind-a'); + + gate.resolve(); + await first; + await second; + + expect(calls).toBe(1); + }); + + it('drops a re-entrant cron tick while a previous run is still in flight', async () => { + const gate = deferred(); + let calls = 0; + + scheduler.register({ + mindId: 'mind-a', + cronExpr: EVERY_SECOND, + fn: async () => { + calls += 1; + await gate.promise; + }, + }); + + // Advance across multiple cron boundaries while the first invocation is + // still pending. Subsequent ticks should see the in-flight guard and be + // dropped — fn must run exactly once until we release it. + await vi.advanceTimersByTimeAsync(3_500); + expect(calls).toBe(1); + + gate.resolve(); + // Drain the pending promise to release in-flight state. + await vi.advanceTimersByTimeAsync(0); + // Future ticks may now fire normally — no assertion needed beyond the + // invariant that the first run was not double-invoked. + }); + }); + + describe('jitter', () => { + it('delays the fn by random() * jitterMs before invoking', async () => { + const fireTimes: number[] = []; + const jitterScheduler = createInternalScheduler({ random: () => 0.5 }); + try { + jitterScheduler.register({ + mindId: 'mind-jitter', + cronExpr: EVERY_SECOND, + fn: async () => { + fireTimes.push(Date.now()); + }, + jitterMs: 100, + }); + + // Walk forward to the next 1-second cron boundary, then up to but not + // past the jitter window. + const start = Date.now(); + // Advance to first boundary. + await vi.advanceTimersByTimeAsync(1_000); + // Advance through 49 ms of jitter — fn should NOT have fired yet + // (random() * 100 = 50, fn fires at boundary + 50ms). + await vi.advanceTimersByTimeAsync(49); + expect(fireTimes).toHaveLength(0); + // Push past the 50 ms jitter — fn fires now. + await vi.advanceTimersByTimeAsync(2); + expect(fireTimes).toHaveLength(1); + // The fn fired ~1050 ms after start. + expect(fireTimes[0] - start).toBeGreaterThanOrEqual(1_050); + expect(fireTimes[0] - start).toBeLessThan(1_100); + } finally { + jitterScheduler.close(); + } + }); + }); + + describe('error handling', () => { + it('does not break the schedule when fn throws — subsequent ticks still fire', async () => { + let calls = 0; + scheduler.register({ + mindId: 'mind-a', + cronExpr: EVERY_SECOND, + fn: async () => { + calls += 1; + if (calls === 1) { + throw new Error('boom'); + } + }, + }); + + // First tick throws, should be caught. + await vi.advanceTimersByTimeAsync(1_500); + expect(calls).toBeGreaterThanOrEqual(1); + + // Second tick should still fire. + await vi.advanceTimersByTimeAsync(1_500); + expect(calls).toBeGreaterThanOrEqual(2); + }); + }); + + describe('close', () => { + it('stops every job and clears the registry', async () => { + let aCalls = 0; + let bCalls = 0; + scheduler.register({ + mindId: 'mind-a', + cronExpr: EVERY_SECOND, + fn: async () => { + aCalls += 1; + }, + }); + scheduler.register({ + mindId: 'mind-b', + cronExpr: EVERY_SECOND, + fn: async () => { + bCalls += 1; + }, + }); + + await vi.advanceTimersByTimeAsync(1_500); + const aSnap = aCalls; + const bSnap = bCalls; + expect(aSnap).toBeGreaterThanOrEqual(1); + expect(bSnap).toBeGreaterThanOrEqual(1); + + scheduler.close(); + expect(scheduler.list().size).toBe(0); + + await vi.advanceTimersByTimeAsync(3_000); + expect(aCalls).toBe(aSnap); + expect(bCalls).toBe(bSnap); + }); + + it('is idempotent', () => { + scheduler.register({ + mindId: 'mind-a', + cronExpr: EVERY_SECOND, + fn: async () => undefined, + }); + scheduler.close(); + expect(() => scheduler.close()).not.toThrow(); + expect(scheduler.list().size).toBe(0); + }); + + it('rejects register() after close to surface lifecycle bugs', () => { + scheduler.close(); + expect(() => + scheduler.register({ + mindId: 'mind-a', + cronExpr: '0 3 * * *', + fn: async () => undefined, + }), + ).toThrow(/closed/i); + }); + }); +}); diff --git a/packages/services/src/mindMemory/InternalScheduler.ts b/packages/services/src/mindMemory/InternalScheduler.ts new file mode 100644 index 00000000..d634083a --- /dev/null +++ b/packages/services/src/mindMemory/InternalScheduler.ts @@ -0,0 +1,197 @@ +/** + * InternalScheduler — Phase 10 of Dream Daemon. + * + * A per-mind in-process croner job runner that drives DreamDaemon.run() on a + * configurable cadence. Deliberately NOT registered with the user-facing + * CronService — `mind-memory.consolidate` is invisible to the user's cron + * list by design (keeps the surfaced cron table clean). + * + * Three correctness invariants: + * + * 1. Re-registering the same mindId atomically replaces the previous Cron + * (stop the old one before swapping in the new one). + * + * 2. One in-flight invocation per mind. Both cron-driven ticks AND + * `runNow` calls share the same per-mind in-flight promise. A + * re-entrant tick or a parallel `runNow` while a previous run is + * still executing receives the existing promise rather than starting + * a second concurrent invocation. This complements (and pre-empts) + * the per-mind mutex inside DreamDaemon. + * + * 3. Errors thrown by `fn` MUST NOT propagate across the croner + * boundary — uncaught throws inside croner silently kill the + * schedule. Wrap and log instead. + * + * Jitter: when many minds activate simultaneously (e.g. on app start at + * 03:00 with N minds all configured for daily 03:00 consolidation) we + * stagger their first fires by a uniform random delay so they don't all + * hit the LLM in the same second. The random source is injectable so + * tests are deterministic. + */ + +import { Cron } from 'croner'; +import { Logger } from '../logger'; + +export interface InternalSchedulerOptions { + readonly logger?: Logger; + /** Defaults to `Math.random()`. Inject for deterministic jitter in tests. */ + readonly random?: () => number; +} + +export interface RegisterOptions { + readonly mindId: string; + readonly cronExpr: string; + readonly fn: () => Promise; + /** + * Optional uniform random delay (in ms) added before each fire to + * prevent thundering-herd when many minds activate simultaneously. + * The actual delay is `random() * jitterMs`. + */ + readonly jitterMs?: number; +} + +export interface InternalScheduler { + /** + * Register a mind's consolidation cron. Replaces any existing entry for + * the same mindId. Cron parses via `croner` — invalid expressions throw + * synchronously. Throws if the scheduler has been closed. + */ + register(opts: RegisterOptions): void; + + /** Stop and forget the entry. No-op if not registered. */ + unregister(mindId: string): void; + + /** + * Fire the registered fn immediately, OUT OF BAND of the cron schedule. + * If a previous invocation (cron-driven or runNow) is still executing, + * returns the in-flight promise so all callers observe the same outcome. + * Throws if the mindId is not registered. + */ + runNow(mindId: string): Promise; + + /** Returns the registered cron expressions keyed by mindId. */ + list(): ReadonlyMap; + + /** Stop every job and clear the registry. Idempotent. */ + close(): void; +} + +interface Entry { + readonly cronExpr: string; + readonly fn: () => Promise; + readonly cron: Cron; + inFlight: Promise | null; +} + +export function createInternalScheduler( + opts: InternalSchedulerOptions = {}, +): InternalScheduler { + const log = opts.logger ?? Logger.create('InternalScheduler'); + const random = opts.random ?? Math.random; + const entries = new Map(); + let closed = false; + + function invokeGuarded(mindId: string, entry: Entry): Promise { + if (entry.inFlight !== null) { + return entry.inFlight; + } + const promise = (async () => { + try { + await entry.fn(); + } catch (err) { + log.error(`consolidation fn threw for mind ${mindId}:`, err); + } + })(); + entry.inFlight = promise.finally(() => { + entry.inFlight = null; + }); + return entry.inFlight; + } + + function stopEntry(entry: Entry): void { + try { + entry.cron.stop(); + } catch (err) { + // croner.stop() is documented as safe to call repeatedly, but if it + // ever throws (e.g. during shutdown races) we don't want to leak it + // out of close()/unregister() and break callers. + log.warn('failed to stop cron:', err); + } + } + + return { + register({ mindId, cronExpr, fn, jitterMs }: RegisterOptions): void { + if (closed) { + throw new Error('InternalScheduler is closed'); + } + + // Stop and remove the previous entry BEFORE constructing the new + // Cron so a parse error on the new expression doesn't leave the + // mind un-scheduled silently — it just leaves the previous schedule + // in place. Wait — actually we DO want replacement semantics: + // remove first so register() with an invalid cron makes it obvious + // the mind has no schedule. Croner throws synchronously on a bad + // expression below, which propagates to the caller. + const previous = entries.get(mindId); + if (previous) { + stopEntry(previous); + entries.delete(mindId); + } + + // Construct the Cron. Invalid expressions throw synchronously here; + // we let the error propagate to the caller. `protect: true` is a + // belt-and-braces measure that prevents croner from launching a + // second handler if the first hasn't returned — our own in-flight + // guard is the source of truth, but `protect` keeps croner's own + // bookkeeping consistent. + const cron = new Cron( + cronExpr, + { protect: true }, + async () => { + const current = entries.get(mindId); + if (!current) return; + if (jitterMs !== undefined && jitterMs > 0) { + const delay = Math.floor(random() * jitterMs); + if (delay > 0) { + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + await invokeGuarded(mindId, current); + }, + ); + + entries.set(mindId, { cronExpr, fn, cron, inFlight: null }); + }, + + unregister(mindId: string): void { + const entry = entries.get(mindId); + if (!entry) return; + stopEntry(entry); + entries.delete(mindId); + }, + + async runNow(mindId: string): Promise { + const entry = entries.get(mindId); + if (!entry) { + throw new Error(`Mind ${mindId} is not registered with InternalScheduler`); + } + return invokeGuarded(mindId, entry); + }, + + list(): ReadonlyMap { + const snapshot = new Map(); + for (const [mindId, entry] of entries) { + snapshot.set(mindId, entry.cronExpr); + } + return snapshot; + }, + + close(): void { + closed = true; + for (const entry of entries.values()) { + stopEntry(entry); + } + entries.clear(); + }, + }; +} diff --git a/packages/services/src/mindMemory/LLMClient.ts b/packages/services/src/mindMemory/LLMClient.ts new file mode 100644 index 00000000..1418f81a --- /dev/null +++ b/packages/services/src/mindMemory/LLMClient.ts @@ -0,0 +1,33 @@ +/** + * LLMClient — minimal language-model synthesis port used by the Dream + * Daemon (Phase 9) to call a one-shot, side-effect-free LLM. + * + * Why an interface (not a concrete class)? + * - The daemon's unit tests inject canned responses via a fake + * implementation (see `FakeLLMClient`). + * - The production wiring (Phase 13) supplies `CopilotLLMClient`, which + * uses the mind's own Copilot SDK with tools disabled and no + * permission handler so the synthesis cannot pollute the user's + * conversation, mutate session history, or trigger UI approvals. + * + * Public surface deliberately stays tiny: a single `synthesize` call + * returning the final assistant text. Streaming, retries, and token + * accounting are caller concerns and live above this seam. + */ + +export interface SynthesizeRequest { + /** Full prompt text. The adapter does not prepend a system message. */ + readonly prompt: string; + /** Soft cap; adapters may translate to an SDK-specific limit or ignore. */ + readonly maxTokens?: number; + /** + * Hard cap enforced by the adapter via an internal `AbortController`. + * On expiry, `synthesize` rejects with an Error whose message starts + * with "LLM synthesis timed out after". + */ + readonly timeoutMs: number; +} + +export interface LLMClient { + synthesize(req: SynthesizeRequest): Promise; +} diff --git a/packages/services/src/mindMemory/MindArchiveStore.test.ts b/packages/services/src/mindMemory/MindArchiveStore.test.ts new file mode 100644 index 00000000..292d6a2e --- /dev/null +++ b/packages/services/src/mindMemory/MindArchiveStore.test.ts @@ -0,0 +1,179 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { createMindArchiveStore } from './MindArchiveStore'; +import { createMindMemoryVault } from './MindMemoryVault'; + +let mindRoot: string; + +beforeEach(() => { + mindRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-archive-')); +}); + +afterEach(() => { + fs.rmSync(mindRoot, { recursive: true, force: true }); +}); + +describe('MindArchiveStore — root', () => { + it('roots itself at /.working-memory/archive/', () => { + const archive = createMindArchiveStore(mindRoot); + expect(archive.root).toBe(path.resolve(mindRoot, '.working-memory', 'archive')); + expect(path.isAbsolute(archive.root)).toBe(true); + }); + + it('does not touch disk on construction', () => { + createMindArchiveStore(mindRoot); + expect(fs.existsSync(path.join(mindRoot, '.working-memory'))).toBe(false); + }); +}); + +describe('MindArchiveStore — writeConsolidated', () => { + it('writes to archive/consolidated/--.md and returns the relPath', async () => { + const archive = createMindArchiveStore(mindRoot); + const relPath = await archive.writeConsolidated({ + turnId: '11111111-1111-4111-8111-111111111111', + timestamp: '2026-05-12T12:34:56Z', + content: 'consolidated body', + }); + // `:` is replaced with `-` for Windows filename portability; documented in + // MindArchiveStore.ts. Input contract remains ISO-8601 UTC. + expect(relPath).toBe( + path.join('consolidated', '2026-05-12T12-34-56Z--11111111-1111-4111-8111-111111111111.md'), + ); + const abs = path.join(archive.root, relPath); + expect(fs.readFileSync(abs, 'utf-8')).toBe('consolidated body'); + }); + + it('auto-creates the consolidated/ subdirectory', async () => { + const archive = createMindArchiveStore(mindRoot); + await archive.writeConsolidated({ + turnId: 'tid-1', + timestamp: '2026-05-12T00:00:00Z', + content: 'x', + }); + expect(fs.statSync(path.join(archive.root, 'consolidated')).isDirectory()).toBe(true); + }); + + it('rejects a turnId containing path separators', async () => { + const archive = createMindArchiveStore(mindRoot); + await expect( + archive.writeConsolidated({ + turnId: '../escape', + timestamp: '2026-05-12T00:00:00Z', + content: 'x', + }), + ).rejects.toThrow(/turn|path|escape|invalid/i); + }); + + it('rejects a timestamp containing path separators', async () => { + const archive = createMindArchiveStore(mindRoot); + await expect( + archive.writeConsolidated({ + turnId: 'tid-1', + timestamp: '2026/05/12', + content: 'x', + }), + ).rejects.toThrow(/timestamp|path|invalid/i); + }); +}); + +describe('MindArchiveStore — writeWeekly / writeMonthly', () => { + it('writes weekly/.md', async () => { + const archive = createMindArchiveStore(mindRoot); + const relPath = await archive.writeWeekly('2026-W19', 'weekly body'); + expect(relPath).toBe(path.join('weekly', '2026-W19.md')); + expect(fs.readFileSync(path.join(archive.root, relPath), 'utf-8')).toBe('weekly body'); + }); + + it('writes monthly/.md', async () => { + const archive = createMindArchiveStore(mindRoot); + const relPath = await archive.writeMonthly('2026-05', 'monthly body'); + expect(relPath).toBe(path.join('monthly', '2026-05.md')); + expect(fs.readFileSync(path.join(archive.root, relPath), 'utf-8')).toBe('monthly body'); + }); + + it('weekly write replaces existing content atomically', async () => { + const archive = createMindArchiveStore(mindRoot); + await archive.writeWeekly('2026-W19', 'first'); + await archive.writeWeekly('2026-W19', 'second'); + expect(fs.readFileSync(path.join(archive.root, 'weekly', '2026-W19.md'), 'utf-8')).toBe('second'); + const dirEntries = fs.readdirSync(path.join(archive.root, 'weekly')); + expect(dirEntries.some((f) => f.includes('.tmp.'))).toBe(false); + }); + + it('rejects weekly keys containing path separators', async () => { + const archive = createMindArchiveStore(mindRoot); + await expect(archive.writeWeekly('../escape', 'x')).rejects.toThrow(/key|path|invalid/i); + await expect(archive.writeWeekly('2026/W19', 'x')).rejects.toThrow(/key|path|invalid/i); + }); + + it('rejects monthly keys containing path separators', async () => { + const archive = createMindArchiveStore(mindRoot); + await expect(archive.writeMonthly('../escape', 'x')).rejects.toThrow(/key|path|invalid/i); + await expect(archive.writeMonthly('2026\\05', 'x')).rejects.toThrow(/key|path|invalid/i); + }); +}); + +describe('MindArchiveStore — list methods', () => { + it('listConsolidated returns only files under consolidated/', async () => { + const archive = createMindArchiveStore(mindRoot); + await archive.writeConsolidated({ + turnId: 't1', + timestamp: '2026-05-12T00:00:00Z', + content: 'a', + }); + await archive.writeConsolidated({ + turnId: 't2', + timestamp: '2026-05-12T01:00:00Z', + content: 'b', + }); + await archive.writeWeekly('2026-W19', 'w'); + const list = await archive.listConsolidated(); + expect(list.sort()).toEqual([ + '2026-05-12T00-00-00Z--t1.md', + '2026-05-12T01-00-00Z--t2.md', + ]); + }); + + it('listWeekly returns only files under weekly/', async () => { + const archive = createMindArchiveStore(mindRoot); + await archive.writeWeekly('2026-W19', 'a'); + await archive.writeWeekly('2026-W20', 'b'); + await archive.writeMonthly('2026-05', 'm'); + const list = await archive.listWeekly(); + expect(list.sort()).toEqual(['2026-W19.md', '2026-W20.md']); + }); + + it('listMonthly returns only files under monthly/', async () => { + const archive = createMindArchiveStore(mindRoot); + await archive.writeMonthly('2026-04', 'a'); + await archive.writeMonthly('2026-05', 'b'); + const list = await archive.listMonthly(); + expect(list.sort()).toEqual(['2026-04.md', '2026-05.md']); + }); + + it('list methods return [] when their directory does not exist yet', async () => { + const archive = createMindArchiveStore(mindRoot); + expect(await archive.listConsolidated()).toEqual([]); + expect(await archive.listWeekly()).toEqual([]); + expect(await archive.listMonthly()).toEqual([]); + }); +}); + +describe('MindArchiveStore — interaction with MindMemoryVault', () => { + it('archive writes never appear in vault.listFiles()', async () => { + const vault = createMindMemoryVault(mindRoot); + const archive = createMindArchiveStore(mindRoot); + await vault.write('memory.md', 'm'); + await archive.writeConsolidated({ + turnId: 't1', + timestamp: '2026-05-12T00:00:00Z', + content: 'c', + }); + await archive.writeWeekly('2026-W19', 'w'); + await archive.writeMonthly('2026-05', 'mo'); + expect(await vault.listFiles()).toEqual(['memory.md']); + }); +}); diff --git a/packages/services/src/mindMemory/MindArchiveStore.ts b/packages/services/src/mindMemory/MindArchiveStore.ts new file mode 100644 index 00000000..c85bf33c --- /dev/null +++ b/packages/services/src/mindMemory/MindArchiveStore.ts @@ -0,0 +1,152 @@ +/** + * MindArchiveStore — filesystem adapter for `/.working-memory/archive/`. + * + * Phase 3 scope (locked by plan): `node:*` only — no Electron, no Chamber + * Logger, no third-party I/O libs. Errors propagate; callers own logging. + * + * Layout: + * archive/ + * consolidated/--.md + * weekly/-W.md + * monthly/-.md + * + * Same atomic-write and path-traversal guarantees as MindMemoryVault. + * Archive contents are owned exclusively by this module — they never appear + * in the vault's `listFiles()` output (the `archive/` subdirectory is filtered + * there). + */ + +import { randomUUID } from 'node:crypto'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; + +import { resolveRelPath } from './MindMemoryVault'; + +const WORKING_MEMORY_DIRNAME = '.working-memory'; +const ARCHIVE_DIRNAME = 'archive'; +const CONSOLIDATED_DIRNAME = 'consolidated'; +const WEEKLY_DIRNAME = 'weekly'; +const MONTHLY_DIRNAME = 'monthly'; + +export interface ConsolidatedRecord { + readonly turnId: string; + readonly timestamp: string; + readonly content: string; +} + +export interface MindArchiveStore { + readonly root: string; + writeConsolidated(record: ConsolidatedRecord): Promise; + writeWeekly(weekKey: string, content: string): Promise; + writeMonthly(monthKey: string, content: string): Promise; + listConsolidated(): Promise; + listWeekly(): Promise; + listMonthly(): Promise; +} + +export function createMindArchiveStore(mindPath: string): MindArchiveStore { + const root = path.resolve(mindPath, WORKING_MEMORY_DIRNAME, ARCHIVE_DIRNAME); + + async function writeConsolidated(record: ConsolidatedRecord): Promise { + assertSafeKey('turnId', record.turnId); + assertSafeKey('timestamp', record.timestamp); + // Filenames replace `:` with `-` so the path is portable across Windows + // (which forbids `:` in NTFS filenames) while keeping the input contract + // an ISO-8601 UTC timestamp. + const safeTimestamp = record.timestamp.replace(/:/g, '-'); + const filename = `${safeTimestamp}--${record.turnId}.md`; + const relPath = path.join(CONSOLIDATED_DIRNAME, filename); + await writeAtRelPath(root, relPath, record.content); + return relPath; + } + + async function writeWeekly(weekKey: string, content: string): Promise { + assertSafeKey('weekly key', weekKey); + const relPath = path.join(WEEKLY_DIRNAME, `${weekKey}.md`); + await writeAtRelPath(root, relPath, content); + return relPath; + } + + async function writeMonthly(monthKey: string, content: string): Promise { + assertSafeKey('monthly key', monthKey); + const relPath = path.join(MONTHLY_DIRNAME, `${monthKey}.md`); + await writeAtRelPath(root, relPath, content); + return relPath; + } + + function listConsolidated(): Promise { + return listSubdirFiles(path.join(root, CONSOLIDATED_DIRNAME)); + } + function listWeekly(): Promise { + return listSubdirFiles(path.join(root, WEEKLY_DIRNAME)); + } + function listMonthly(): Promise { + return listSubdirFiles(path.join(root, MONTHLY_DIRNAME)); + } + + return { + root, + writeConsolidated, + writeWeekly, + writeMonthly, + listConsolidated, + listWeekly, + listMonthly, + }; +} + +function assertSafeKey(label: string, value: string): void { + if (typeof value !== 'string' || value.length === 0) { + throw new Error(`invalid ${label}: must be a non-empty string`); + } + if (value.includes('\u0000')) { + throw new Error(`invalid ${label}: contains NUL byte`); + } + if (value.includes('/') || value.includes('\\')) { + throw new Error(`invalid ${label}: must not contain path separators (got ${value})`); + } + if (value === '.' || value === '..') { + throw new Error(`invalid ${label}: must not be . or ..`); + } +} + +async function writeAtRelPath(root: string, relPath: string, content: string): Promise { + const abs = resolveRelPath(root, relPath); + await fsp.mkdir(path.dirname(abs), { recursive: true }); + const tempPath = `${abs}.tmp.${randomUUID()}`; + const handle = await fsp.open(tempPath, 'wx'); + try { + await handle.writeFile(content); + await handle.sync(); + } finally { + await handle.close(); + } + try { + await fsp.rename(tempPath, abs); + } catch (err) { + try { + await fsp.unlink(tempPath); + } catch { + // ignore + } + throw err; + } +} + +async function listSubdirFiles(absDir: string): Promise { + let entries; + try { + entries = await fsp.readdir(absDir, { withFileTypes: true }); + } catch (err) { + if ( + typeof err === 'object' && + err !== null && + 'code' in err && + (err as NodeJS.ErrnoException).code === 'ENOENT' + ) { + return []; + } + throw err; + } + return entries.filter((entry) => entry.isFile()).map((entry) => entry.name); +} diff --git a/packages/services/src/mindMemory/MindMemoryService.test.ts b/packages/services/src/mindMemory/MindMemoryService.test.ts new file mode 100644 index 00000000..c776f3dc --- /dev/null +++ b/packages/services/src/mindMemory/MindMemoryService.test.ts @@ -0,0 +1,910 @@ +/** + * MindMemoryService — Phase 11. + * + * Lifecycle + public surface for per-mind background memory consolidation. + * Wires the InternalScheduler entry, opens dream.db, builds the vault / + * archive / daemon, and registers a TurnCompletionObserver on ChatService + * (whose `onTurnCompleted` forwards to the per-mind DailyLogWriter). + * + * Strict opt-in: when `.chamber.json → workingMemory.consolidation.enabled` + * is not exactly `true`, activate is a no-op (no db open, no factories + * called beyond configReader, no observer registered). + * + * Documented choice: a second `activateMind` for the same mindId while + * already activated is a no-op (idempotent — never replaces collaborators + * mid-flight). Callers must `releaseMind` first to swap configuration. + */ + +import path from 'node:path'; + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type Database from 'better-sqlite3'; + +import type { TurnCompletionObserver, CompletedTurn } from '@chamber/shared/turn-observer'; + +import { createMindMemoryService } from './MindMemoryService'; +import type { + ChatObserverRegistry, + MindMemoryServiceFactories, +} from './MindMemoryService'; +import type { DreamDaemon, DreamRunResult } from './DreamDaemon'; +import type { InternalScheduler, RegisterOptions } from './InternalScheduler'; +import type { MindMemoryVault } from './MindMemoryVault'; +import type { MindArchiveStore } from './MindArchiveStore'; +import type { ChamberMindConfig } from '../mind/chamberMindConfig'; +import { dreamDbPath } from './dream-schema'; + +// --------------------------------------------------------------------------- +// Fakes +// --------------------------------------------------------------------------- + +function makeFakeScheduler(): InternalScheduler & { + readonly registered: Map; + readonly closeCalls: number; +} { + const registered = new Map(); + let closeCalls = 0; + const scheduler = { + register(opts: RegisterOptions): void { + registered.set(opts.mindId, opts); + }, + unregister(mindId: string): void { + registered.delete(mindId); + }, + async runNow(mindId: string): Promise { + const entry = registered.get(mindId); + if (!entry) throw new Error(`unknown ${mindId}`); + await entry.fn(); + }, + list(): ReadonlyMap { + const m = new Map(); + for (const [k, v] of registered) m.set(k, v.cronExpr); + return m; + }, + close(): void { + closeCalls += 1; + registered.clear(); + }, + get registered() { + return registered; + }, + get closeCalls() { + return closeCalls; + }, + }; + return scheduler; +} + +function makeFakeDb(): Database.Database & { closed: boolean } { + let closed = false; + // Only the methods MindMemoryService cares about. The rest are stubs that + // throw if accidentally invoked. + const db = { + close: vi.fn(() => { + closed = true; + }), + get closed() { + return closed; + }, + }; + return db as unknown as Database.Database & { closed: boolean }; +} + +function makeFakeVault(root: string): MindMemoryVault { + return { + root, + read: vi.fn(async () => null), + write: vi.fn(async () => undefined), + append: vi.fn(async () => undefined), + exists: vi.fn(async () => false), + listFiles: vi.fn(async () => []), + ensureDir: vi.fn(async () => undefined), + }; +} + +function makeFakeArchive(root: string): MindArchiveStore { + return { + root, + writeConsolidated: vi.fn(async () => 'consolidated/x.md'), + writeWeekly: vi.fn(async () => 'weekly/x.md'), + writeMonthly: vi.fn(async () => 'monthly/x.md'), + listConsolidated: vi.fn(async () => []), + listWeekly: vi.fn(async () => []), + listMonthly: vi.fn(async () => []), + }; +} + +function makeFakeDaemon(): DreamDaemon & { runCalls: number; closeCalls: number } { + let runCalls = 0; + let closeCalls = 0; + const daemon: DreamDaemon = { + async run(): Promise { + runCalls += 1; + return { status: 'skipped', reason: 'no-turns' }; + }, + async forceRun(): Promise { + return { status: 'skipped', reason: 'no-turns' }; + }, + getStatus() { + return { phase: 'idle', locked: false, lastRunAt: null, lastResult: null }; + }, + notifyTurnCompleted() { + /* no-op */ + }, + async close(): Promise { + closeCalls += 1; + }, + }; + return Object.defineProperties(daemon, { + runCalls: { get: () => runCalls, enumerable: true }, + closeCalls: { get: () => closeCalls, enumerable: true }, + }) as DreamDaemon & { runCalls: number; closeCalls: number }; +} + +function makeFakeChat(): ChatObserverRegistry & { + readonly observers: TurnCompletionObserver[]; +} { + const observers: TurnCompletionObserver[] = []; + return { + addObserver(o: TurnCompletionObserver): void { + observers.push(o); + }, + removeObserver(o: TurnCompletionObserver): void { + const i = observers.indexOf(o); + if (i !== -1) observers.splice(i, 1); + }, + get observers() { + return observers; + }, + }; +} + +interface FactoryCallLog { + readonly events: string[]; +} + +function makeFactories(args: { + readonly chamberConfig: ChamberMindConfig; + readonly daemon?: DreamDaemon; + readonly daemonError?: Error; + readonly dbError?: Error; + readonly vaultError?: Error; + readonly archiveError?: Error; + readonly chat?: ChatObserverRegistry; + readonly scheduler?: ReturnType; + readonly schedulerRegisterError?: Error; +}): { + readonly factories: MindMemoryServiceFactories; + readonly log: FactoryCallLog; + readonly db: ReturnType; + readonly vault: MindMemoryVault; + readonly archive: MindArchiveStore; + readonly daemon: DreamDaemon; + readonly chat: ReturnType; + readonly scheduler: ReturnType; +} { + const events: string[] = []; + const db = makeFakeDb(); + const vault = makeFakeVault('/tmp/vault'); + const archive = makeFakeArchive('/tmp/archive'); + const daemon = (args.daemon ?? makeFakeDaemon()) as ReturnType; + const chat = (args.chat ?? makeFakeChat()) as ReturnType; + const scheduler = args.scheduler ?? makeFakeScheduler(); + + if (args.schedulerRegisterError) { + const baseRegister = scheduler.register.bind(scheduler); + void baseRegister; + scheduler.register = (() => { + events.push('scheduler.register'); + throw args.schedulerRegisterError; + }) as InternalScheduler['register']; + } else { + const baseRegister = scheduler.register.bind(scheduler); + scheduler.register = ((opts: RegisterOptions) => { + events.push('scheduler.register'); + baseRegister(opts); + }) as InternalScheduler['register']; + } + + const factories: MindMemoryServiceFactories = { + scheduler, + chatService: chat, + configReader: vi.fn((mindPath: string) => { + events.push(`configReader:${mindPath}`); + return args.chamberConfig; + }), + dbFactory: vi.fn((dbPath: string) => { + events.push(`dbFactory:${dbPath}`); + if (args.dbError) throw args.dbError; + return db; + }), + vaultFactory: vi.fn((mindPath: string) => { + events.push(`vaultFactory:${mindPath}`); + if (args.vaultError) throw args.vaultError; + return vault; + }), + archiveFactory: vi.fn((mindPath: string) => { + events.push(`archiveFactory:${mindPath}`); + if (args.archiveError) throw args.archiveError; + return archive; + }), + daemonFactory: vi.fn(() => { + events.push('daemonFactory'); + if (args.daemonError) throw args.daemonError; + return daemon; + }), + }; + + return { factories, log: { events }, db, vault, archive, daemon, chat, scheduler }; +} + +const ENABLED_CONFIG: ChamberMindConfig = { + workingMemory: { + consolidation: { + enabled: true, + cron: '0 3 * * *', + lastKTurns: 10, + perTurnMaxBytes: 2048, + memoryMaxBytes: 8192, + }, + }, +}; + +const DISABLED_CONFIG: ChamberMindConfig = { + workingMemory: { + consolidation: { + enabled: false, + cron: '0 3 * * *', + lastKTurns: 10, + perTurnMaxBytes: 2048, + memoryMaxBytes: 8192, + }, + }, +}; + +const MIND_PATH = path.join('/', 'tmp', 'mind-alpha'); +const MIND_ID = 'mind-alpha'; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('MindMemoryService — activateMind: opt-in honored', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns early when consolidation.enabled is false — no factories beyond configReader, no observer registered', async () => { + const { factories, log, chat, scheduler } = makeFactories({ chamberConfig: DISABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + + expect(log.events).toEqual([`configReader:${MIND_PATH}`]); + expect(factories.dbFactory).not.toHaveBeenCalled(); + expect(factories.vaultFactory).not.toHaveBeenCalled(); + expect(factories.archiveFactory).not.toHaveBeenCalled(); + expect(factories.daemonFactory).not.toHaveBeenCalled(); + expect(chat.observers).toHaveLength(0); + expect(scheduler.registered.size).toBe(0); + }); + + it('treats a truthy-but-not-true enabled value as disabled (defensive against config drift)', async () => { + const odd: ChamberMindConfig = { + workingMemory: { + consolidation: { + ...ENABLED_CONFIG.workingMemory.consolidation, + // Force a non-`true` truthy value past the type system. + enabled: 1 as unknown as boolean, + }, + }, + }; + const { factories, chat, scheduler } = makeFactories({ chamberConfig: odd }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + + expect(factories.dbFactory).not.toHaveBeenCalled(); + expect(chat.observers).toHaveLength(0); + expect(scheduler.registered.size).toBe(0); + }); +}); + +describe('MindMemoryService — activateMind: enabled path wires everything', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('opens dream.db at /.working-memory/.state/dream.db, builds vault/archive/daemon, registers scheduler, adds observer', async () => { + const { factories, log, db, vault, archive, daemon, chat, scheduler } = makeFactories({ + chamberConfig: ENABLED_CONFIG, + }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + + // Order: config → vault/archive (independent) → db → daemon → scheduler → observer. + // Strict ordering only matters where one collaborator depends on another: + // db must be opened before daemonFactory (which receives it); scheduler + // entry / observer registration come last so a daemonFactory failure + // doesn't leak a registered cron or observer. + const idxConfig = log.events.indexOf(`configReader:${MIND_PATH}`); + const idxDb = log.events.indexOf(`dbFactory:${dreamDbPath(MIND_PATH)}`); + const idxDaemon = log.events.indexOf('daemonFactory'); + const idxRegister = log.events.indexOf('scheduler.register'); + expect(idxConfig).toBeGreaterThanOrEqual(0); + expect(idxDb).toBeGreaterThan(idxConfig); + expect(idxDaemon).toBeGreaterThan(idxDb); + expect(idxRegister).toBeGreaterThan(idxDaemon); + + expect(factories.vaultFactory).toHaveBeenCalledWith(MIND_PATH); + expect(factories.archiveFactory).toHaveBeenCalledWith(MIND_PATH); + expect(factories.daemonFactory).toHaveBeenCalledWith( + expect.objectContaining({ + mindId: MIND_ID, + mindPath: MIND_PATH, + vault, + archive, + db, + config: ENABLED_CONFIG.workingMemory.consolidation, + }), + ); + + // Scheduler entry: cron from config, jitter 30s, fn drives daemon.run. + const entry = scheduler.registered.get(MIND_ID); + expect(entry).toBeDefined(); + expect(entry!.cronExpr).toBe('0 3 * * *'); + expect(entry!.jitterMs).toBe(30_000); + await entry!.fn(); + expect((daemon as ReturnType).runCalls).toBe(1); + + // Exactly one observer added. + expect(chat.observers).toHaveLength(1); + expect(typeof chat.observers[0].onTurnCompleted).toBe('function'); + }); + + it('observer forwards onTurnCompleted to the per-mind DailyLogWriter without throwing across the boundary', async () => { + const { factories, chat } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + await svc.activateMind(MIND_ID, MIND_PATH); + + const obs = chat.observers[0]; + const turn: CompletedTurn = { + turnId: 't1', + sessionId: 's1', + model: 'm', + status: 'completed', + startedAt: '2026-05-12T00:00:00.000Z', + endedAt: '2026-05-12T00:00:01.000Z', + prompt: 'hi', + finalAssistantMessage: 'hello', + }; + + // We don't actually want to touch the filesystem here; just verify the + // observer is callable and returns a thenable. The real DailyLogWriter + // is exercised by its own tests. Construction-only test for wiring. + const result = obs.onTurnCompleted(turn); + expect(result === undefined || typeof (result as Promise).then === 'function').toBe(true); + }); +}); + +describe('MindMemoryService — DailyLogWriter onTurnRecorded → dream-state activity counter', () => { + // INVARIANT (real bug fixed in Phase 14): the writer constructed inside + // `activateMind` must wire `onTurnRecorded` so each completed turn bumps + // `dream_state.turns_since_last_run`. Without this, the daemon's activity + // gate would block consolidation forever (default `minTurnsBetweenRuns: 1`). + // We exercise the wiring against a real :memory: better-sqlite3 db + real + // MindMemoryVault on disk so the assertion is end-to-end. + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('observer.onTurnCompleted increments turns_since_last_run via DailyLogWriter onTurnRecorded', async () => { + const { mkdtempSync, rmSync, mkdirSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { createRequire } = await import('node:module'); + const { createMindMemoryVault } = await import('./MindMemoryVault'); + const { createMindArchiveStore } = await import('./MindArchiveStore'); + const { migrate } = await import('./dream-schema'); + const { readState } = await import('./dream-state'); + + const runtimeRequire = createRequire(__filename); + const Database = runtimeRequire('better-sqlite3') as typeof import('better-sqlite3'); + + const root = mkdtempSync(path.join(tmpdir(), 'chamber-mindmem-onturn-')); + const mindPath = path.join(root, 'mind-real'); + mkdirSync(mindPath, { recursive: true }); + + const realDb = new Database(':memory:'); + migrate(realDb); + + const chat = makeFakeChat(); + const scheduler = makeFakeScheduler(); + const daemon = makeFakeDaemon(); + + const factories: MindMemoryServiceFactories = { + scheduler, + chatService: chat, + configReader: () => ENABLED_CONFIG, + dbFactory: () => realDb as unknown as Database.Database, + vaultFactory: createMindMemoryVault, + archiveFactory: createMindArchiveStore, + daemonFactory: () => daemon, + }; + + const svc = createMindMemoryService(factories); + try { + await svc.activateMind(MIND_ID, mindPath); + + expect(readState(realDb).turnsSinceLastRun).toBe(0); + + const obs = chat.observers[0]; + const t1: CompletedTurn = { + turnId: 't-1', + sessionId: 's-1', + model: 'm', + status: 'completed', + startedAt: '2026-05-12T00:00:00.000Z', + endedAt: '2026-05-12T00:00:01.000Z', + prompt: 'hi', + finalAssistantMessage: 'hello', + }; + const t2: CompletedTurn = { ...t1, turnId: 't-2' }; + + await obs.onTurnCompleted(t1); + await obs.onTurnCompleted(t2); + + expect(readState(realDb).turnsSinceLastRun).toBe(2); + + await svc.releaseMind(MIND_ID); + } finally { + try { + if (realDb.open) realDb.close(); + } catch { + /* noop */ + } + rmSync(root, { recursive: true, force: true }); + } + }); +}); + +describe('MindMemoryService — activateMind: idempotency', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('second activate for the same already-activated mind is a no-op (documented choice)', async () => { + const { factories, chat, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + const dbCalls = (factories.dbFactory as ReturnType).mock.calls.length; + const daemonCalls = (factories.daemonFactory as ReturnType).mock.calls.length; + const obsCount = chat.observers.length; + const schedSize = scheduler.registered.size; + + await svc.activateMind(MIND_ID, MIND_PATH); + + expect((factories.dbFactory as ReturnType).mock.calls.length).toBe(dbCalls); + expect((factories.daemonFactory as ReturnType).mock.calls.length).toBe(daemonCalls); + expect(chat.observers).toHaveLength(obsCount); + expect(scheduler.registered.size).toBe(schedSize); + }); + + it('after release, a subsequent activate rebuilds the entry', async () => { + const { factories, chat, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + await svc.releaseMind(MIND_ID); + await svc.activateMind(MIND_ID, MIND_PATH); + + expect((factories.dbFactory as ReturnType).mock.calls.length).toBe(2); + expect((factories.daemonFactory as ReturnType).mock.calls.length).toBe(2); + expect(chat.observers).toHaveLength(1); + expect(scheduler.registered.size).toBe(1); + }); +}); + +describe('MindMemoryService — releaseMind', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('tears down everything: scheduler.unregister, observer removed, daemon.close, db.close', async () => { + const { factories, db, daemon, chat, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + expect(chat.observers).toHaveLength(1); + expect(scheduler.registered.size).toBe(1); + + await svc.releaseMind(MIND_ID); + + expect(scheduler.registered.has(MIND_ID)).toBe(false); + expect(chat.observers).toHaveLength(0); + expect((daemon as ReturnType).closeCalls).toBe(1); + expect((db as ReturnType).closed).toBe(true); + }); + + it('release of an unknown mind is a no-op', async () => { + const { factories, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await expect(svc.releaseMind('never-activated')).resolves.toBeUndefined(); + expect(scheduler.registered.size).toBe(0); + }); + + it('release of a mind that opted out (activate was a no-op) is also a no-op', async () => { + const { factories, scheduler } = makeFactories({ chamberConfig: DISABLED_CONFIG }); + const svc = createMindMemoryService(factories); + await svc.activateMind(MIND_ID, MIND_PATH); + await expect(svc.releaseMind(MIND_ID)).resolves.toBeUndefined(); + expect(scheduler.registered.size).toBe(0); + }); + + it('release is idempotent — second release of the same mind does not throw or double-close', async () => { + const { factories, db, daemon } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + await svc.activateMind(MIND_ID, MIND_PATH); + await svc.releaseMind(MIND_ID); + await expect(svc.releaseMind(MIND_ID)).resolves.toBeUndefined(); + expect((daemon as ReturnType).closeCalls).toBe(1); + expect((db as ReturnType).closed).toBe(true); + }); +}); + +describe('MindMemoryService — close()', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('releases every activated mind sequentially', async () => { + const { factories, chat, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind('m1', '/tmp/m1'); + await svc.activateMind('m2', '/tmp/m2'); + expect(chat.observers).toHaveLength(2); + expect(scheduler.registered.size).toBe(2); + + await svc.close(); + + expect(chat.observers).toHaveLength(0); + expect(scheduler.registered.size).toBe(0); + }); + + it('is idempotent', async () => { + const { factories } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + await svc.activateMind(MIND_ID, MIND_PATH); + await svc.close(); + await expect(svc.close()).resolves.toBeUndefined(); + }); + + it('after close, further activate calls are rejected (fail-fast — keeps lifecycle invariant clear)', async () => { + const { factories } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + await svc.close(); + await expect(svc.activateMind(MIND_ID, MIND_PATH)).rejects.toThrow(/closed/i); + }); +}); + +describe('MindMemoryService — activation error rollback', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('daemonFactory throws → db is closed, scheduler not registered, observer not added, mind not tracked', async () => { + const { factories, db, chat, scheduler } = makeFactories({ + chamberConfig: ENABLED_CONFIG, + daemonError: new Error('daemon boom'), + }); + const svc = createMindMemoryService(factories); + + await expect(svc.activateMind(MIND_ID, MIND_PATH)).rejects.toThrow('daemon boom'); + + expect((db as ReturnType).closed).toBe(true); + expect(scheduler.registered.size).toBe(0); + expect(chat.observers).toHaveLength(0); + + // Mind is NOT tracked, so a follow-up release is a no-op AND a follow-up + // activate (after fixing the failure) re-attempts the full build. + await expect(svc.releaseMind(MIND_ID)).resolves.toBeUndefined(); + }); + + it('scheduler.register throws → daemon is closed, db is closed, observer not added, mind not tracked', async () => { + const { factories, db, daemon, chat, scheduler } = makeFactories({ + chamberConfig: ENABLED_CONFIG, + schedulerRegisterError: new Error('cron boom'), + }); + const svc = createMindMemoryService(factories); + + await expect(svc.activateMind(MIND_ID, MIND_PATH)).rejects.toThrow('cron boom'); + + expect((daemon as ReturnType).closeCalls).toBe(1); + expect((db as ReturnType).closed).toBe(true); + expect(scheduler.registered.size).toBe(0); + expect(chat.observers).toHaveLength(0); + }); + + it('dbFactory throws → no other factories called, mind not tracked', async () => { + const { factories, chat, scheduler } = makeFactories({ + chamberConfig: ENABLED_CONFIG, + dbError: new Error('db boom'), + }); + const svc = createMindMemoryService(factories); + + await expect(svc.activateMind(MIND_ID, MIND_PATH)).rejects.toThrow('db boom'); + + expect(factories.daemonFactory).not.toHaveBeenCalled(); + expect(scheduler.registered.size).toBe(0); + expect(chat.observers).toHaveLength(0); + }); +}); + +describe('MindMemoryService — multi-mind isolation', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('release of one mind does not disturb another activated mind', async () => { + const { factories, chat, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind('m1', '/tmp/m1'); + await svc.activateMind('m2', '/tmp/m2'); + await svc.releaseMind('m1'); + + expect(scheduler.registered.has('m1')).toBe(false); + expect(scheduler.registered.has('m2')).toBe(true); + expect(chat.observers).toHaveLength(1); + }); +}); + +describe('MindMemoryService — __debugGet (E2E accessor)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns null for an unknown mind id', () => { + const { factories } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + expect(svc.__debugGet('does-not-exist')).toBeNull(); + }); + + it('returns null for a disabled mind (activate was a no-op)', async () => { + const { factories } = makeFactories({ chamberConfig: DISABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + + expect(svc.__debugGet(MIND_ID)).toBeNull(); + }); + + it('returns the live daemon + dbPath for an activated mind', async () => { + const { factories, daemon } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + + const entry = svc.__debugGet(MIND_ID); + expect(entry).not.toBeNull(); + expect(entry!.daemon).toBe(daemon); + expect(entry!.dbPath).toBe(dreamDbPath(MIND_PATH)); + expect(entry!.writer).toBeDefined(); + expect(typeof entry!.writer.write).toBe('function'); + }); + + it('returns null again after releaseMind', async () => { + const { factories } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await svc.activateMind(MIND_ID, MIND_PATH); + expect(svc.__debugGet(MIND_ID)).not.toBeNull(); + + await svc.releaseMind(MIND_ID); + expect(svc.__debugGet(MIND_ID)).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// v0.60.0 — Eager migration on activate (Phase 1) +// +// When a mind that was previously opted-out flips to opted-in, the user +// experience is "I clicked the switch and now my old freeform log is +// preserved as log.legacy.md and a fresh structured log was seeded". This +// must happen WITHOUT requiring a turn to land — otherwise the user sees no +// effect until they next chat with the mind, and the "what happens to my +// log" question stays scary. +// +// Implementation contract: `activateMind` for an opted-in mind invokes +// `writer.migrateIfNeeded()` BEFORE returning. Opted-out mind: never called +// (no writer is constructed at all per the strict-opt-in contract). +// +// Tests use a real filesystem because the writer is built inline inside +// `activateMind` (no writerFactory injection). The observable contract is +// log.md state after activate resolves. +// --------------------------------------------------------------------------- + +describe('MindMemoryService — activateMind: eager migration (Phase 1)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('opted-in mind with pre-existing unstructured log.md → after activate, log.md is sentinel-only and log.legacy.md preserves the original', async () => { + const { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { STRUCTURED_LOG_SENTINEL } = await import('./StructuredLogFormat'); + + const root = mkdtempSync(path.join(tmpdir(), 'chamber-mindmem-migrate-')); + const mindPath = path.join(root, 'mind-real'); + const wmDir = path.join(mindPath, '.working-memory'); + mkdirSync(wmDir, { recursive: true }); + + const original = '# legacy freeform notes\nrandom content\n'; + writeFileSync(path.join(wmDir, 'log.md'), original); + + const { factories } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + try { + await svc.activateMind(MIND_ID, mindPath); + + // Original content rotated out of the way. + expect(existsSync(path.join(wmDir, 'log.legacy.md'))).toBe(true); + expect(readFileSync(path.join(wmDir, 'log.legacy.md'), 'utf-8')).toBe(original); + + // log.md is sentinel-only (NO turn frame — migration ran before any turns). + expect(readFileSync(path.join(wmDir, 'log.md'), 'utf-8')).toBe( + STRUCTURED_LOG_SENTINEL + '\n\n', + ); + + await svc.releaseMind(MIND_ID); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it('opted-in mind with sentinel log.md → activate is a no-op for migration (idempotent)', async () => { + const { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + const { STRUCTURED_LOG_SENTINEL } = await import('./StructuredLogFormat'); + + const root = mkdtempSync(path.join(tmpdir(), 'chamber-mindmem-migrate-noop-')); + const mindPath = path.join(root, 'mind-real'); + const wmDir = path.join(mindPath, '.working-memory'); + mkdirSync(wmDir, { recursive: true }); + + const sentinelOnly = STRUCTURED_LOG_SENTINEL + '\n\n'; + writeFileSync(path.join(wmDir, 'log.md'), sentinelOnly); + + const { factories } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + try { + await svc.activateMind(MIND_ID, mindPath); + + expect(existsSync(path.join(wmDir, 'log.legacy.md'))).toBe(false); + expect(readFileSync(path.join(wmDir, 'log.md'), 'utf-8')).toBe(sentinelOnly); + + await svc.releaseMind(MIND_ID); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it('opted-out mind with pre-existing unstructured log.md → activate does NOT touch log.md (no migration, no rotation)', async () => { + const { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + + const root = mkdtempSync(path.join(tmpdir(), 'chamber-mindmem-migrate-disabled-')); + const mindPath = path.join(root, 'mind-real'); + const wmDir = path.join(mindPath, '.working-memory'); + mkdirSync(wmDir, { recursive: true }); + + const original = '# legacy freeform notes\nrandom content\n'; + writeFileSync(path.join(wmDir, 'log.md'), original); + + const { factories } = makeFactories({ chamberConfig: DISABLED_CONFIG }); + const svc = createMindMemoryService(factories); + try { + await svc.activateMind(MIND_ID, mindPath); + + // Untouched. + expect(existsSync(path.join(wmDir, 'log.legacy.md'))).toBe(false); + expect(readFileSync(path.join(wmDir, 'log.md'), 'utf-8')).toBe(original); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it('opted-in mind with no log.md → activate creates the directory but does NOT seed log.md (migrateIfNeeded is a no-op for missing files)', async () => { + const { mkdtempSync, rmSync, mkdirSync, existsSync } = await import('node:fs'); + const { tmpdir } = await import('node:os'); + + const root = mkdtempSync(path.join(tmpdir(), 'chamber-mindmem-migrate-empty-')); + const mindPath = path.join(root, 'mind-real'); + mkdirSync(mindPath, { recursive: true }); + + const { factories } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + try { + await svc.activateMind(MIND_ID, mindPath); + + // migrateIfNeeded is a no-op when log.md does not exist. The first + // write() will seed the sentinel — until then, log.md stays absent. + expect(existsSync(path.join(mindPath, '.working-memory', 'log.md'))).toBe(false); + + await svc.releaseMind(MIND_ID); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); + +// --------------------------------------------------------------------------- +// v0.60.0 — Per-mindId activate/release serialization (Uncle Bob finding 2) +// +// `main.ts` wires MindManager events to MindMemoryService via fire-and-forget +// `.catch()` chains. A user who rapid-toggles the daemon switch (ON → OFF → +// ON within a few hundred ms) generates back-to-back activate/release calls +// that may interleave: activate#1 → release while activate#1 still running → +// activate#2 sees `active.has(mindId)` and no-ops. +// +// The fix: serialize per-mindId inside MindMemoryService so the second +// activate genuinely runs after the release completes. The composition root +// stays simple (still fire-and-forget); the service owns the invariant. +// --------------------------------------------------------------------------- + +describe('MindMemoryService — per-mindId activate/release serialization', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('rapid activate → release → activate for the same mindId all complete and end with the mind activated', async () => { + const { factories, chat, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + // Fire all three without awaiting between them — exactly the pattern + // main.ts's fire-and-forget event handlers produce on rapid toggle. + const p1 = svc.activateMind(MIND_ID, MIND_PATH); + const p2 = svc.releaseMind(MIND_ID); + const p3 = svc.activateMind(MIND_ID, MIND_PATH); + + await Promise.all([p1, p2, p3]); + + // End state: mind is activated exactly once. + expect(scheduler.registered.size).toBe(1); + expect(chat.observers).toHaveLength(1); + // dbFactory called twice (once per activate); daemonFactory called twice. + expect((factories.dbFactory as ReturnType).mock.calls.length).toBe(2); + expect((factories.daemonFactory as ReturnType).mock.calls.length).toBe(2); + }); + + it('rapid release → activate (when not active) for the same mindId end with the mind activated', async () => { + const { factories, chat, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + const p1 = svc.releaseMind(MIND_ID); // no-op (not active yet) + const p2 = svc.activateMind(MIND_ID, MIND_PATH); + + await Promise.all([p1, p2]); + + expect(scheduler.registered.size).toBe(1); + expect(chat.observers).toHaveLength(1); + }); + + it('serialization is per-mindId — independent minds run in parallel', async () => { + const { factories, chat, scheduler } = makeFactories({ chamberConfig: ENABLED_CONFIG }); + const svc = createMindMemoryService(factories); + + await Promise.all([ + svc.activateMind('mind-a', '/tmp/mind-a'), + svc.activateMind('mind-b', '/tmp/mind-b'), + svc.activateMind('mind-c', '/tmp/mind-c'), + ]); + + expect(scheduler.registered.size).toBe(3); + expect(chat.observers).toHaveLength(3); + }); +}); diff --git a/packages/services/src/mindMemory/MindMemoryService.ts b/packages/services/src/mindMemory/MindMemoryService.ts new file mode 100644 index 00000000..3611a639 --- /dev/null +++ b/packages/services/src/mindMemory/MindMemoryService.ts @@ -0,0 +1,355 @@ +/** + * MindMemoryService — Phase 11 of the Dream Daemon spike. + * + * Per-mind lifecycle layer that turns the Phase 1–10 collaborators into a + * single activate / release / close API: + * + * - `activateMind(mindId, mindPath)` reads `.chamber.json`, opts the mind + * in only if `workingMemory.consolidation.enabled === true`, opens the + * per-mind `dream.db`, builds vault/archive/daemon via injected + * factories, registers an `InternalScheduler` entry whose fn drives + * `daemon.run()`, and registers a `TurnCompletionObserver` on + * ChatService that forwards completed turns to the per-mind + * `DailyLogWriter`. + * + * - `releaseMind(mindId)` is the exact inverse: scheduler.unregister → + * remove observer → daemon.close → db.close → drop from internal map. + * Idempotent. No-op for unknown / disabled mind ids. + * + * - `close()` releases every activated mind sequentially, then refuses + * subsequent activate calls (fail-fast — keeps the lifecycle invariant + * visible rather than silently leaking minds after global teardown). + * + * Documented choices: + * + * 1. Strict opt-in: `enabled !== true` (NOT just truthy) means OFF. Even + * DailyLogWriter is NOT registered when consolidation is opted out — + * we don't write structured turn frames to disk for minds that haven't + * asked for the feature. + * + * 2. `activateMind` for an already-activated mind is an idempotent no-op. + * Callers must `releaseMind` first to swap configuration; we never + * replace collaborators mid-flight (avoids db/observer leaks if + * `releaseMind` was forgotten on the previous activation). + * + * 3. ChatService observer wiring uses a tiny `addObserver` / + * `removeObserver` pair on ChatService itself (Phase 11 addition, + * smaller than introducing a separate registry abstraction). The + * service depends on the narrow `ChatObserverRegistry` interface so + * tests can fake it. + * + * 4. Activation errors are unwound in reverse construction order: a + * throw from `daemonFactory` closes the db; a throw from + * `scheduler.register` closes the daemon AND the db. The mind is + * never recorded as activated unless every step succeeded. + */ + +import type Database from 'better-sqlite3'; + +import type { TurnCompletionObserver, CompletedTurn } from '@chamber/shared/turn-observer'; + +import { Logger } from '../logger'; +import { loadChamberMindConfig, type ChamberMindConfig, type WorkingMemoryConsolidationConfig } from '../mind/chamberMindConfig'; +import { createDailyLogWriter, type DailyLogWriter } from './DailyLogWriter'; +import type { DreamDaemon } from './DreamDaemon'; +import { dreamDbPath } from './dream-schema'; +import { incrementTurnCount } from './dream-state'; +import type { InternalScheduler } from './InternalScheduler'; +import type { MindArchiveStore } from './MindArchiveStore'; +import type { MindMemoryVault } from './MindMemoryVault'; + +// --------------------------------------------------------------------------- +// Public surface +// --------------------------------------------------------------------------- + +/** Default jitter window for daemon kick-off (defeats thundering herd at 03:00). */ +const DEFAULT_JITTER_MS = 30_000; + +/** + * Narrow ChatService surface MindMemoryService depends on — just the + * observer add/remove pair. Keeps the unit tests free of MindManager, + * TurnQueue, and the SDK harness. + */ +export interface ChatObserverRegistry { + addObserver(observer: TurnCompletionObserver): void; + removeObserver(observer: TurnCompletionObserver): void; +} + +export interface DaemonFactoryOptions { + readonly mindId: string; + readonly mindPath: string; + readonly vault: MindMemoryVault; + readonly archive: MindArchiveStore; + readonly db: Database.Database; + readonly config: WorkingMemoryConsolidationConfig; +} + +export interface MindMemoryServiceFactories { + readonly scheduler: InternalScheduler; + readonly chatService: ChatObserverRegistry; + readonly configReader: (mindPath: string) => ChamberMindConfig; + readonly dbFactory: (dbPath: string) => Database.Database; + readonly vaultFactory: (mindPath: string) => MindMemoryVault; + readonly archiveFactory: (mindPath: string) => MindArchiveStore; + readonly daemonFactory: (opts: DaemonFactoryOptions) => DreamDaemon; + readonly logger?: Logger; + /** Override jitter window (defaults to 30s). */ + readonly jitterMs?: number; +} + +export interface MindMemoryService { + activateMind(mindId: string, mindPath: string): Promise; + releaseMind(mindId: string): Promise; + close(): Promise; + /** + * Test/E2E-only accessor. Returns the live `DreamDaemon` plus the + * `dream.db` path for an active mind, or `null` if the mind is not + * currently activated. Production code must NOT depend on this surface; + * callers are expected to gate on `process.env.CHAMBER_E2E === '1'`. + * + * Lifecycle: + * - returns `null` for unknown / disabled mind ids + * - returns the same `daemon` reference handed back by `daemonFactory` + * during `activateMind` + * - returns `null` again after `releaseMind` (entry is removed from + * the internal active map) + */ + __debugGet(mindId: string): { + readonly daemon: DreamDaemon; + readonly dbPath: string; + readonly writer: DailyLogWriter; + } | null; +} + +// --------------------------------------------------------------------------- +// Implementation +// --------------------------------------------------------------------------- + +interface ActiveEntry { + readonly mindPath: string; + readonly dbPath: string; + readonly db: Database.Database; + readonly daemon: DreamDaemon; + readonly writer: DailyLogWriter; + readonly observer: TurnCompletionObserver; +} + +export function createMindMemoryService( + factories: MindMemoryServiceFactories, +): MindMemoryService { + const log = factories.logger ?? Logger.create('MindMemoryService'); + const jitterMs = factories.jitterMs ?? DEFAULT_JITTER_MS; + const active = new Map(); + let closed = false; + + // Per-mindId serialization (Uncle Bob plan-review finding 2). The + // composition root wires `mindManager.on('mind:loaded', ctx => + // mindMemoryService.activateMind(...).catch(...))` — fire-and-forget. A + // user who rapid-toggles the dream-daemon switch (ON → OFF → ON) generates + // back-to-back activate/release events. With the eager-migration await + // added below, activate yields BEFORE calling `active.set`, opening a + // race window where release no-ops (mind not yet active) and the next + // activate's idempotency check no-ops too — leaving the mind in a stale + // state. Serializing here keeps the contract intact at the service + // layer, so the composition root can stay simple. + const lifecycleQueues = new Map>(); + + function enqueueLifecycle(mindId: string, fn: () => Promise): Promise { + const prior = lifecycleQueues.get(mindId) ?? Promise.resolve(); + const next = prior.then(fn, fn); + // Tail tracking — we keep the chain as a Promise that swallows + // rejections so a failed activate/release does not poison the queue. + const tail: Promise = next.then( + () => undefined, + () => undefined, + ); + lifecycleQueues.set(mindId, tail); + // Best-effort cleanup once the queue is fully drained for this mind. + void tail.then(() => { + if (lifecycleQueues.get(mindId) === tail) { + lifecycleQueues.delete(mindId); + } + }); + return next; + } + + function activateMind(mindId: string, mindPath: string): Promise { + return enqueueLifecycle(mindId, () => activateMindInner(mindId, mindPath)); + } + + function releaseMind(mindId: string): Promise { + return enqueueLifecycle(mindId, () => releaseMindInner(mindId)); + } + + async function activateMindInner(mindId: string, mindPath: string): Promise { + if (closed) { + throw new Error('MindMemoryService is closed'); + } + if (active.has(mindId)) { + log.debug(`Mind ${mindId} already activated; activateMind is a no-op`); + return; + } + + const config = factories.configReader(mindPath); + const consolidation = config.workingMemory?.consolidation; + // Strict opt-in. `enabled !== true` (not just truthy) means OFF — also + // means we do NOT register DailyLogWriter; the writer would otherwise + // start materializing structured log frames for minds that never asked + // for the feature, defeating the opt-in. + if (!consolidation || consolidation.enabled !== true) { + return; + } + + // Build collaborators in dependency order; unwind on failure. + let db: Database.Database | null = null; + let daemon: DreamDaemon | null = null; + let observer: TurnCompletionObserver | null = null; + let registered = false; + + try { + const vault = factories.vaultFactory(mindPath); + const archive = factories.archiveFactory(mindPath); + const dbPath = dreamDbPath(mindPath); + db = factories.dbFactory(dbPath); + + daemon = factories.daemonFactory({ + mindId, + mindPath, + vault, + archive, + db, + config: consolidation, + }); + + // DailyLogWriter is built inline — its construction is pure (no I/O + // until a turn arrives), so injecting a writer factory would be + // strictly more wiring without test value. Tests replace this surface + // by faking ChatObserverRegistry and asserting one observer was added. + // + // INVARIANT: `onTurnRecorded` MUST bump `dream_state.turns_since_last_run` + // — otherwise the daemon's activity gate (`minTurnsBetweenRuns >= 1` by + // default) would block consolidation forever. Phase 11 spec wires this + // hook; verified end-to-end by tests/integration/mindMemory.integration + // and packages/services/src/mindMemory/MindMemoryService.test.ts. + const dbForHook = db; + const writer = createDailyLogWriter({ + mindId, + mindPath, + deps: { + onTurnRecorded: () => { + incrementTurnCount(dbForHook, 1); + }, + }, + }); + + // Eager migration (v0.60.0 Phase 1). When a mind that previously + // opted out flips ON, the user expects their freeform log.md to be + // preserved as log.legacy.md and a fresh sentinel-only log to be + // seeded — without waiting for the next chat turn. Idempotent for + // already-structured logs; no-op for missing log.md. + await writer.migrateIfNeeded(); + + observer = { + onTurnCompleted: (turn: CompletedTurn) => writer.write(turn), + }; + + factories.scheduler.register({ + mindId, + cronExpr: consolidation.cron, + fn: () => daemon!.run().then(() => undefined), + jitterMs, + }); + registered = true; + + factories.chatService.addObserver(observer); + + active.set(mindId, { mindPath, dbPath, db, daemon, writer, observer }); + } catch (err) { + // Unwind in reverse order — only what we successfully built. Each + // step is wrapped to keep the original error as the surfaced one. + if (registered) { + try { + factories.scheduler.unregister(mindId); + } catch (releaseErr) { + log.warn(`activate rollback: scheduler.unregister(${mindId}) failed`, releaseErr); + } + } + // Observer is only added after register succeeded; no rollback needed + // unless we move that step earlier in the future. + if (daemon) { + try { + await daemon.close(); + } catch (closeErr) { + log.warn(`activate rollback: daemon.close(${mindId}) failed`, closeErr); + } + } + if (db) { + try { + db.close(); + } catch (closeErr) { + log.warn(`activate rollback: db.close(${mindId}) failed`, closeErr); + } + } + throw err; + } + } + + async function releaseMindInner(mindId: string): Promise { + const entry = active.get(mindId); + if (!entry) return; + // Drop the map entry FIRST so a teardown failure doesn't leave a + // half-released mind that a subsequent `release` would try to tear + // down again. The daemon and db have their own idempotent close + // contracts; we surface failures via warn but never block. + active.delete(mindId); + + try { + factories.scheduler.unregister(mindId); + } catch (err) { + log.warn(`release: scheduler.unregister(${mindId}) failed`, err); + } + try { + factories.chatService.removeObserver(entry.observer); + } catch (err) { + log.warn(`release: chatService.removeObserver(${mindId}) failed`, err); + } + try { + await entry.daemon.close(); + } catch (err) { + log.warn(`release: daemon.close(${mindId}) failed`, err); + } + try { + entry.db.close(); + } catch (err) { + log.warn(`release: db.close(${mindId}) failed`, err); + } + } + + async function close(): Promise { + if (closed) return; + closed = true; + // Snapshot keys so we don't mutate during iteration (releaseMind + // deletes from `active`). + const ids = Array.from(active.keys()); + for (const id of ids) { + await releaseMind(id); + } + } + + return { activateMind, releaseMind, close, __debugGet }; + + function __debugGet(mindId: string): { + readonly daemon: DreamDaemon; + readonly dbPath: string; + readonly writer: DailyLogWriter; + } | null { + const entry = active.get(mindId); + if (!entry) return null; + return { daemon: entry.daemon, dbPath: entry.dbPath, writer: entry.writer }; + } +} + +// Re-export the default loader so the composition root can pass it as +// `configReader` without an extra import. +export const defaultConfigReader = loadChamberMindConfig; + diff --git a/packages/services/src/mindMemory/MindMemoryVault.test.ts b/packages/services/src/mindMemory/MindMemoryVault.test.ts new file mode 100644 index 00000000..d740145a --- /dev/null +++ b/packages/services/src/mindMemory/MindMemoryVault.test.ts @@ -0,0 +1,227 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { createMindMemoryVault } from './MindMemoryVault'; + +let mindRoot: string; + +beforeEach(() => { + mindRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-vault-')); +}); + +afterEach(() => { + fs.rmSync(mindRoot, { recursive: true, force: true }); +}); + +describe('MindMemoryVault — root and ensureDir', () => { + it('exposes an absolute, normalized root under /.working-memory/', () => { + const vault = createMindMemoryVault(mindRoot); + expect(path.isAbsolute(vault.root)).toBe(true); + expect(vault.root).toBe(path.resolve(mindRoot, '.working-memory')); + }); + + it('does not touch the disk on construction', () => { + createMindMemoryVault(mindRoot); + expect(fs.existsSync(path.join(mindRoot, '.working-memory'))).toBe(false); + }); + + it('ensureDir is idempotent and creates the working-memory directory', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.ensureDir(); + await vault.ensureDir(); + expect(fs.statSync(vault.root).isDirectory()).toBe(true); + }); +}); + +describe('MindMemoryVault — read / write / exists', () => { + it('round-trips read/write for a top-level file', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write('memory.md', '# memories\n'); + expect(await vault.read('memory.md')).toBe('# memories\n'); + expect(await vault.exists('memory.md')).toBe(true); + }); + + it('returns null when reading a missing file', async () => { + const vault = createMindMemoryVault(mindRoot); + expect(await vault.read('memory.md')).toBeNull(); + expect(await vault.exists('memory.md')).toBe(false); + }); + + it('write is atomic — no .tmp.* files remain after success', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write('log.md', 'hello'); + const files = fs.readdirSync(vault.root); + expect(files.some((f) => f.includes('.tmp.'))).toBe(false); + expect(files).toContain('log.md'); + }); + + it('write replaces existing content atomically', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write('rules.md', 'first'); + await vault.write('rules.md', 'second'); + expect(await vault.read('rules.md')).toBe('second'); + }); + + it('write creates parent directories under root for nested rel paths', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write(path.join('subdir', 'note.md'), 'nested'); + expect(await vault.read(path.join('subdir', 'note.md'))).toBe('nested'); + }); +}); + +describe('MindMemoryVault — append', () => { + it('appends to a non-existent file (creating it)', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.append('log.md', 'line1\n'); + expect(await vault.read('log.md')).toBe('line1\n'); + }); + + it('appends to existing content', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write('log.md', 'a\n'); + await vault.append('log.md', 'b\n'); + expect(await vault.read('log.md')).toBe('a\nb\n'); + }); + + it('serializes concurrent appends to the same file (no interleaving / loss)', async () => { + const vault = createMindMemoryVault(mindRoot); + const lines = Array.from({ length: 50 }, (_, i) => `line-${i.toString().padStart(3, '0')}\n`); + await Promise.all(lines.map((line) => vault.append('log.md', line))); + const content = await vault.read('log.md'); + expect(content).not.toBeNull(); + const sortedLines = (content as string).split('\n').filter(Boolean).sort(); + expect(sortedLines).toEqual(lines.map((l) => l.trimEnd()).sort()); + expect((content as string).length).toBe(lines.reduce((sum, l) => sum + l.length, 0)); + }); +}); + +describe('MindMemoryVault — listFiles', () => { + it('lists top-level files only', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write('memory.md', 'm'); + await vault.write('rules.md', 'r'); + await vault.write('log.md', 'l'); + const list = await vault.listFiles(); + expect(list.sort()).toEqual(['log.md', 'memory.md', 'rules.md']); + }); + + it('returns an empty list before ensureDir / when root does not exist', async () => { + const vault = createMindMemoryVault(mindRoot); + expect(await vault.listFiles()).toEqual([]); + }); + + it('excludes the .state/ subdirectory and its contents', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write('memory.md', 'm'); + fs.mkdirSync(path.join(vault.root, '.state'), { recursive: true }); + fs.writeFileSync(path.join(vault.root, '.state', 'dream.db'), 'x'); + const list = await vault.listFiles(); + expect(list).toContain('memory.md'); + expect(list).not.toContain('.state'); + expect(list).not.toContain('dream.db'); + }); + + it('excludes the archive/ subdirectory', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write('memory.md', 'm'); + fs.mkdirSync(path.join(vault.root, 'archive'), { recursive: true }); + fs.writeFileSync(path.join(vault.root, 'archive', 'something.md'), 'a'); + const list = await vault.listFiles(); + expect(list).toContain('memory.md'); + expect(list).not.toContain('archive'); + expect(list).not.toContain('something.md'); + }); + + it('excludes nested subdirectory contents (only top-level files)', async () => { + const vault = createMindMemoryVault(mindRoot); + await vault.write('memory.md', 'm'); + fs.mkdirSync(path.join(vault.root, 'nested'), { recursive: true }); + fs.writeFileSync(path.join(vault.root, 'nested', 'inner.md'), 'i'); + const list = await vault.listFiles(); + expect(list).toEqual(['memory.md']); + }); +}); + +describe('MindMemoryVault — path traversal guard', () => { + const traversalCases: Array<{ name: string; relPath: string }> = [ + { name: 'parent reference (posix)', relPath: '../escape.md' }, + { name: 'parent reference (windows)', relPath: '..\\escape.md' }, + { name: 'nested parent reference', relPath: 'a/../../escape.md' }, + { name: 'absolute posix path', relPath: '/etc/passwd' }, + { name: 'absolute windows path', relPath: 'C:\\Windows\\System32\\config' }, + { name: 'UNC path', relPath: '\\\\server\\share\\file' }, + { name: 'embedded null byte', relPath: 'good\u0000bad.md' }, + { name: 'just ..', relPath: '..' }, + { name: 'empty string', relPath: '' }, + ]; + + for (const { name, relPath } of traversalCases) { + it(`read rejects ${name}`, async () => { + const vault = createMindMemoryVault(mindRoot); + await expect(vault.read(relPath)).rejects.toThrow(/path|escape|invalid/i); + }); + + it(`write rejects ${name}`, async () => { + const vault = createMindMemoryVault(mindRoot); + await expect(vault.write(relPath, 'x')).rejects.toThrow(/path|escape|invalid/i); + }); + + it(`append rejects ${name}`, async () => { + const vault = createMindMemoryVault(mindRoot); + await expect(vault.append(relPath, 'x')).rejects.toThrow(/path|escape|invalid/i); + }); + + it(`exists rejects ${name}`, async () => { + const vault = createMindMemoryVault(mindRoot); + await expect(vault.exists(relPath)).rejects.toThrow(/path|escape|invalid/i); + }); + } +}); + +describe('MindMemoryVault — mind boundary isolation', () => { + it('two vaults rooted at sibling paths cannot read each other via ..', async () => { + const otherMind = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-vault-other-')); + try { + const vaultA = createMindMemoryVault(mindRoot); + const vaultB = createMindMemoryVault(otherMind); + + await vaultA.write('memory.md', 'A-only'); + await vaultB.write('memory.md', 'B-only'); + + expect(await vaultA.read('memory.md')).toBe('A-only'); + expect(await vaultB.read('memory.md')).toBe('B-only'); + + const otherName = path.basename(otherMind); + const escape = path.join('..', '..', otherName, '.working-memory', 'memory.md'); + await expect(vaultA.read(escape)).rejects.toThrow(/path|escape/i); + + expect(await vaultA.read('memory.md')).toBe('A-only'); + expect(await vaultB.read('memory.md')).toBe('B-only'); + } finally { + fs.rmSync(otherMind, { recursive: true, force: true }); + } + }); + + it('write under mindA does not produce any file under mindB', async () => { + const otherMind = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-vault-other-')); + try { + const vaultA = createMindMemoryVault(mindRoot); + const vaultB = createMindMemoryVault(otherMind); + await vaultA.write('memory.md', 'A'); + await vaultA.write('rules.md', 'A-rules'); + + const bRootExists = fs.existsSync(vaultB.root); + if (bRootExists) { + expect(fs.readdirSync(vaultB.root)).toEqual([]); + } + + const aFiles = await fsp.readdir(vaultA.root); + expect(aFiles.sort()).toEqual(['memory.md', 'rules.md']); + } finally { + fs.rmSync(otherMind, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/services/src/mindMemory/MindMemoryVault.ts b/packages/services/src/mindMemory/MindMemoryVault.ts new file mode 100644 index 00000000..b628ce44 --- /dev/null +++ b/packages/services/src/mindMemory/MindMemoryVault.ts @@ -0,0 +1,190 @@ +/** + * MindMemoryVault — filesystem adapter for `/.working-memory/`. + * + * Phase 3 scope (locked by plan): `node:*` only — no Electron, no Chamber + * Logger, no third-party I/O libs. Errors propagate; callers own logging. + * + * Responsibilities: + * - Atomic writes via temp + rename (no partial writes ever observable). + * - Path-traversal guard rejects every relPath that resolves outside root, + * including absolute paths, `..` segments, drive letters, UNC prefixes, + * and embedded NUL bytes. + * - Per-file in-process append serialization. Cross-process serialization + * is the DailyLogWriter's job (Phase 5). + * - `listFiles()` excludes managed subdirectories (`.state/`, `archive/`, + * and any dotted subdirectory) — only top-level regular files surface + * to the WorkingMemoryComposer (Phase 12). + */ + +import { randomUUID } from 'node:crypto'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import path from 'node:path'; + +const WORKING_MEMORY_DIRNAME = '.working-memory'; +const ARCHIVE_DIRNAME = 'archive'; +const STATE_DIRNAME = '.state'; + +export interface MindMemoryVault { + readonly root: string; + read(relPath: string): Promise; + write(relPath: string, content: string): Promise; + append(relPath: string, content: string): Promise; + exists(relPath: string): Promise; + listFiles(): Promise; + ensureDir(): Promise; +} + +export function createMindMemoryVault(mindPath: string): MindMemoryVault { + const root = path.resolve(mindPath, WORKING_MEMORY_DIRNAME); + // Per-file in-process mutex chains. Map key = absolute file path. + // Every append on a given file awaits the prior chain link before issuing + // its own read-modify-write cycle, eliminating intra-process interleaving. + const appendChains = new Map>(); + + async function ensureDir(): Promise { + await fsp.mkdir(root, { recursive: true }); + } + + async function read(relPath: string): Promise { + const abs = resolveRelPath(root, relPath); + try { + return await fsp.readFile(abs, 'utf-8'); + } catch (err) { + if (isErrnoCode(err, 'ENOENT')) return null; + throw err; + } + } + + async function write(relPath: string, content: string): Promise { + const abs = resolveRelPath(root, relPath); + await fsp.mkdir(path.dirname(abs), { recursive: true }); + await atomicWriteFile(abs, content); + } + + async function append(relPath: string, content: string): Promise { + const abs = resolveRelPath(root, relPath); + const prior = appendChains.get(abs) ?? Promise.resolve(); + const next = prior.then(async () => { + await fsp.mkdir(path.dirname(abs), { recursive: true }); + const handle = await fsp.open(abs, 'a'); + try { + await handle.write(content); + await handle.sync(); + } finally { + await handle.close(); + } + }); + // Swallow errors on the chain itself (caller still gets the rejection + // through `next`); this prevents one failed append from poisoning the + // queue for later callers. + appendChains.set( + abs, + next.catch(() => undefined), + ); + try { + await next; + } finally { + // GC the chain entry once it's the tail and has settled. + if (appendChains.get(abs) === next || appendChains.get(abs)?.then === next.then) { + // best-effort cleanup; safe to leave entry if a new append slotted in. + } + } + } + + async function exists(relPath: string): Promise { + const abs = resolveRelPath(root, relPath); + try { + await fsp.access(abs, fs.constants.F_OK); + return true; + } catch (err) { + if (isErrnoCode(err, 'ENOENT')) return false; + throw err; + } + } + + async function listFiles(): Promise { + let entries: fs.Dirent[]; + try { + entries = await fsp.readdir(root, { withFileTypes: true }); + } catch (err) { + if (isErrnoCode(err, 'ENOENT')) return []; + throw err; + } + return entries + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .filter((name) => name !== ARCHIVE_DIRNAME && name !== STATE_DIRNAME); + } + + return { root, read, write, append, exists, listFiles, ensureDir }; +} + +export function resolveRelPath(root: string, relPath: string): string { + assertSafeRelPath(relPath); + const resolved = path.resolve(root, relPath); + const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep; + if (resolved !== root && !resolved.startsWith(rootWithSep)) { + throw new Error(`path escapes vault root: ${relPath} (root=${root})`); + } + if (resolved === root) { + throw new Error(`path resolves to vault root, not a file: ${relPath} (root=${root})`); + } + return resolved; +} + +function assertSafeRelPath(relPath: string): void { + if (typeof relPath !== 'string' || relPath.length === 0) { + throw new Error('invalid path: must be a non-empty string'); + } + if (relPath.includes('\u0000')) { + throw new Error('invalid path: contains NUL byte'); + } + // Normalize separators so a Windows-style backslash sequence is evaluated + // the same way on POSIX hosts (where `\` would otherwise be a literal char + // and slip past `path.posix.isAbsolute`). + const slashed = relPath.replace(/\\/g, '/'); + if (path.posix.isAbsolute(slashed) || path.win32.isAbsolute(relPath)) { + throw new Error(`invalid path: must be relative, got ${relPath}`); + } + // Reject Windows drive letters (e.g. `C:something`) even without a slash. + if (/^[A-Za-z]:/.test(relPath)) { + throw new Error(`invalid path: drive-relative paths not allowed: ${relPath}`); + } + const normalized = path.posix.normalize(slashed); + const segments = normalized.split('/'); + if (segments.some((seg) => seg === '..')) { + throw new Error(`invalid path: parent traversal not allowed: ${relPath}`); + } +} + +async function atomicWriteFile(absPath: string, content: string): Promise { + const tempPath = `${absPath}.tmp.${randomUUID()}`; + const handle = await fsp.open(tempPath, 'wx'); + try { + await handle.writeFile(content); + await handle.sync(); + } finally { + await handle.close(); + } + try { + await fsp.rename(tempPath, absPath); + } catch (err) { + // Best-effort cleanup if rename fails; surface the original error. + try { + await fsp.unlink(tempPath); + } catch { + // ignore + } + throw err; + } +} + +function isErrnoCode(err: unknown, code: string): boolean { + return ( + typeof err === 'object' && + err !== null && + 'code' in err && + (err as NodeJS.ErrnoException).code === code + ); +} diff --git a/packages/services/src/mindMemory/StructuredLogFormat.test.ts b/packages/services/src/mindMemory/StructuredLogFormat.test.ts new file mode 100644 index 00000000..0585d4f6 --- /dev/null +++ b/packages/services/src/mindMemory/StructuredLogFormat.test.ts @@ -0,0 +1,404 @@ +import { describe, expect, it } from 'vitest'; + +import { + STRUCTURED_LOG_SENTINEL, + detectSentinel, + parseLog, + serializeTurn, + type CompletedTurn, +} from './StructuredLogFormat'; + +const SENTINEL = STRUCTURED_LOG_SENTINEL; + +const baseTurn = (overrides: Partial = {}): CompletedTurn => ({ + turnId: '11111111-1111-4111-8111-111111111111', + sessionId: 'sess-abc', + model: 'gpt-5.5', + status: 'completed', + startedAt: '2026-05-12T15:00:00Z', + endedAt: '2026-05-12T15:00:05Z', + prompt: 'hello', + finalAssistantMessage: 'hi there', + ...overrides, +}); + +describe('STRUCTURED_LOG_SENTINEL', () => { + it('is the exact magic marker locked in the plan', () => { + expect(STRUCTURED_LOG_SENTINEL).toBe(''); + }); +}); + +describe('serializeTurn', () => { + it('emits the canonical frame with double-space separators in the heading', () => { + const out = serializeTurn(baseTurn()); + expect(out).toBe( + '## 2026-05-12T15:00:05Z turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: sess-abc\n' + + 'model: gpt-5.5\n' + + '\n' + + '### user\n' + + 'hello\n' + + '\n' + + '### assistant\n' + + 'hi there\n', + ); + }); + + it('ends with a trailing newline so multiple turns concatenate cleanly', () => { + const out = serializeTurn(baseTurn()); + expect(out.endsWith('\n')).toBe(true); + }); + + it('preserves multi-line prompts verbatim', () => { + const out = serializeTurn(baseTurn({ prompt: 'line 1\nline 2\nline 3' })); + expect(out).toContain('### user\nline 1\nline 2\nline 3\n\n### assistant'); + }); + + it('preserves multi-line assistant messages verbatim', () => { + const out = serializeTurn(baseTurn({ finalAssistantMessage: 'a\nb\nc' })); + expect(out).toContain('### assistant\na\nb\nc\n'); + }); + + it('renders status:completed', () => { + const out = serializeTurn(baseTurn({ status: 'completed' })); + expect(out).toMatch(/^## .+ {2}status:completed$/m); + }); + + it('renders status:aborted', () => { + const out = serializeTurn(baseTurn({ status: 'aborted' })); + expect(out).toMatch(/^## .+ {2}status:aborted$/m); + }); + + it('renders status:error', () => { + const out = serializeTurn(baseTurn({ status: 'error' })); + expect(out).toMatch(/^## .+ {2}status:error$/m); + }); +}); + +describe('parseLog — sentinel detector', () => { + it('returns sentinel:false for an empty file', () => { + expect(parseLog('')).toEqual({ sentinel: false, turns: [], malformed: 0 }); + }); + + it('returns sentinel:false for whitespace-only content', () => { + const out = parseLog(' \n\n\t\n'); + expect(out.sentinel).toBe(false); + expect(out.turns).toEqual([]); + expect(out.malformed).toBe(0); + }); + + it('detects sentinel after a UTF-8 BOM', () => { + const out = parseLog('\uFEFF' + SENTINEL + '\n'); + expect(out.sentinel).toBe(true); + expect(out.turns).toEqual([]); + expect(out.malformed).toBe(0); + }); + + it('detects sentinel and parses turns when the file uses CRLF line endings', () => { + const turn = baseTurn(); + const content = SENTINEL + '\r\n' + serializeTurn(turn).replace(/\n/g, '\r\n'); + const out = parseLog(content); + expect(out.sentinel).toBe(true); + expect(out.malformed).toBe(0); + expect(out.turns).toHaveLength(1); + expect(out.turns[0].prompt).toBe('hello'); + expect(out.turns[0].assistant).toBe('hi there'); + }); + + it('returns sentinel:false when the marker is missing', () => { + const out = parseLog('# Just notes\nnothing structured here\n'); + expect(out.sentinel).toBe(false); + expect(out.turns).toEqual([]); + expect(out.malformed).toBe(0); + }); + + it('returns sentinel:false when the marker is not the first non-blank line', () => { + const out = parseLog('some prose\n' + SENTINEL + '\n'); + expect(out.sentinel).toBe(false); + expect(out.turns).toEqual([]); + }); + + it('tolerates a duplicated sentinel later in the file without inflating malformed count', () => { + const t = baseTurn({ + finalAssistantMessage: 'before\n' + SENTINEL + '\nafter', + }); + const content = SENTINEL + '\n' + serializeTurn(t); + const out = parseLog(content); + expect(out.sentinel).toBe(true); + expect(out.malformed).toBe(0); + expect(out.turns).toHaveLength(1); + expect(out.turns[0].assistant).toBe('before\n' + SENTINEL + '\nafter'); + }); + + it('detectSentinel agrees with parseLog for canonical input', () => { + const content = SENTINEL + '\n' + serializeTurn(baseTurn()); + expect(detectSentinel(content)).toBe(true); + }); + + it('detectSentinel returns false for empty content', () => { + expect(detectSentinel('')).toBe(false); + }); + + it('detectSentinel returns false when sentinel is not first non-blank line', () => { + expect(detectSentinel('garbage\n' + SENTINEL + '\n')).toBe(false); + }); + + it('detectSentinel handles BOM + CRLF combined', () => { + expect(detectSentinel('\uFEFF' + SENTINEL + '\r\n')).toBe(true); + }); +}); + +describe('parseLog — turn parsing', () => { + it('round-trips a single turn (sentinel + serialize)', () => { + const t = baseTurn(); + const out = parseLog(SENTINEL + '\n' + serializeTurn(t)); + expect(out.sentinel).toBe(true); + expect(out.malformed).toBe(0); + expect(out.turns).toHaveLength(1); + expect(out.turns[0]).toEqual({ + turnId: t.turnId, + sessionId: t.sessionId, + model: t.model, + status: t.status, + timestamp: t.endedAt, + prompt: t.prompt, + assistant: t.finalAssistantMessage, + }); + }); + + it('round-trips multiple turns and preserves order', () => { + const t1 = baseTurn({ + turnId: '11111111-1111-4111-8111-111111111111', + endedAt: '2026-05-12T15:00:05Z', + prompt: 'q1', + finalAssistantMessage: 'a1', + }); + const t2 = baseTurn({ + turnId: '22222222-2222-4222-8222-222222222222', + endedAt: '2026-05-12T15:01:05Z', + prompt: 'q2', + finalAssistantMessage: 'a2', + status: 'aborted', + }); + const t3 = baseTurn({ + turnId: '33333333-3333-4333-8333-333333333333', + endedAt: '2026-05-12T15:02:05Z', + prompt: 'q3', + finalAssistantMessage: 'a3', + status: 'error', + }); + const content = + SENTINEL + '\n' + serializeTurn(t1) + serializeTurn(t2) + serializeTurn(t3); + const out = parseLog(content); + expect(out.malformed).toBe(0); + expect(out.turns.map((t) => t.turnId)).toEqual([t1.turnId, t2.turnId, t3.turnId]); + expect(out.turns.map((t) => t.status)).toEqual(['completed', 'aborted', 'error']); + expect(out.turns.map((t) => t.prompt)).toEqual(['q1', 'q2', 'q3']); + expect(out.turns.map((t) => t.assistant)).toEqual(['a1', 'a2', 'a3']); + }); + + it('drops a block with missing session: line and increments malformed', () => { + const goodTurn = baseTurn({ + turnId: '22222222-2222-4222-8222-222222222222', + endedAt: '2026-05-12T15:01:00Z', + }); + const bad = + '## 2026-05-12T15:00:00Z turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'model: gpt-5.5\n' + + '\n### user\nq\n\n### assistant\na\n'; + const content = SENTINEL + '\n' + bad + serializeTurn(goodTurn); + const out = parseLog(content); + expect(out.malformed).toBe(1); + expect(out.turns).toHaveLength(1); + expect(out.turns[0].turnId).toBe(goodTurn.turnId); + }); + + it('drops a block with missing model: line and increments malformed', () => { + const bad = + '## 2026-05-12T15:00:00Z turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: sess-abc\n' + + '\n### user\nq\n\n### assistant\na\n'; + const out = parseLog(SENTINEL + '\n' + bad); + expect(out.malformed).toBe(1); + expect(out.turns).toHaveLength(0); + }); + + it('accepts a frame with an empty model: line (model selection may not be set at turn time)', () => { + const frame = + '## 2026-05-12T15:00:00Z turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: sess-abc\n' + + 'model: \n' + + '\n### user\nq\n\n### assistant\na\n'; + const out = parseLog(SENTINEL + '\n' + frame); + expect(out.malformed).toBe(0); + expect(out.turns).toHaveLength(1); + expect(out.turns[0].model).toBe(''); + }); + + it('drops a block with malformed timestamp', () => { + const bad = + '## not-a-date turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: s\nmodel: m\n\n### user\nq\n\n### assistant\na\n'; + const out = parseLog(SENTINEL + '\n' + bad); + expect(out.malformed).toBe(1); + expect(out.turns).toHaveLength(0); + }); + + it('drops a block with timestamp not in UTC (no Z suffix)', () => { + const bad = + '## 2026-05-12T15:00:00+02:00 turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: s\nmodel: m\n\n### user\nq\n\n### assistant\na\n'; + const out = parseLog(SENTINEL + '\n' + bad); + expect(out.malformed).toBe(1); + expect(out.turns).toHaveLength(0); + }); + + it('drops a block with unknown status', () => { + const bad = + '## 2026-05-12T15:00:00Z turn:11111111-1111-4111-8111-111111111111 status:weird\n' + + 'session: s\nmodel: m\n\n### user\nq\n\n### assistant\na\n'; + const out = parseLog(SENTINEL + '\n' + bad); + expect(out.malformed).toBe(1); + expect(out.turns).toHaveLength(0); + }); + + it('drops a block missing the ### user header', () => { + const bad = + '## 2026-05-12T15:00:00Z turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: s\nmodel: m\n\n### assistant\na\n'; + const out = parseLog(SENTINEL + '\n' + bad); + expect(out.malformed).toBe(1); + expect(out.turns).toHaveLength(0); + }); + + it('drops a block missing the ### assistant header', () => { + const bad = + '## 2026-05-12T15:00:00Z turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: s\nmodel: m\n\n### user\nq\n'; + const out = parseLog(SENTINEL + '\n' + bad); + expect(out.malformed).toBe(1); + expect(out.turns).toHaveLength(0); + }); + + it('continues parsing after a malformed block', () => { + const good = baseTurn({ + turnId: '22222222-2222-4222-8222-222222222222', + endedAt: '2026-05-12T15:02:00Z', + prompt: 'good', + finalAssistantMessage: 'OK', + }); + const bad = + '## not-a-date turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: s\nmodel: m\n\n### user\nq\n\n### assistant\na\n'; + const out = parseLog(SENTINEL + '\n' + bad + serializeTurn(good)); + expect(out.malformed).toBe(1); + expect(out.turns).toHaveLength(1); + expect(out.turns[0].prompt).toBe('good'); + }); + + it('discards orphan content before the first heading without flagging it as malformed', () => { + const t = baseTurn(); + const content = + SENTINEL + '\nstray prose between sentinel and first heading\n\n' + serializeTurn(t); + const out = parseLog(content); + expect(out.malformed).toBe(0); + expect(out.turns).toHaveLength(1); + }); + + it('returns no turns when the file contains only the sentinel', () => { + const out = parseLog(SENTINEL + '\n'); + expect(out.sentinel).toBe(true); + expect(out.turns).toEqual([]); + expect(out.malformed).toBe(0); + }); +}); + +describe('parseLog — round-trip property pack', () => { + const fixtures: ReadonlyArray<{ name: string; turn: CompletedTurn }> = [ + { name: 'simple ASCII', turn: baseTurn() }, + { name: 'empty prompt body', turn: baseTurn({ prompt: '' }) }, + { name: 'empty assistant body', turn: baseTurn({ finalAssistantMessage: '' }) }, + { + name: 'UTF-8 multibyte (emoji + Japanese)', + turn: baseTurn({ prompt: '🚀 こんにちは', finalAssistantMessage: '世界 🌍' }), + }, + { name: 'multi-line prompt', turn: baseTurn({ prompt: 'a\nb\nc\nd' }) }, + { + name: 'multi-line assistant', + turn: baseTurn({ finalAssistantMessage: 'one\ntwo\nthree' }), + }, + { + name: 'embedded "## " in prompt that does not match heading regex', + turn: baseTurn({ prompt: '## look at this header\n## another one' }), + }, + { + name: 'embedded "## " in assistant that does not match heading regex', + turn: baseTurn({ finalAssistantMessage: '## final answer\n## see above' }), + }, + { + name: 'inline ### tokens not at column 0', + turn: baseTurn({ + prompt: 'see ###deepheading inline', + finalAssistantMessage: 'foo ### bar baz', + }), + }, + { + name: 'tabs and mixed whitespace', + turn: baseTurn({ prompt: '\thello\tworld', finalAssistantMessage: ' spaced ' }), + }, + { name: 'aborted status', turn: baseTurn({ status: 'aborted' }) }, + { name: 'error status', turn: baseTurn({ status: 'error' }) }, + { name: 'long single-line prompt', turn: baseTurn({ prompt: 'x'.repeat(5000) }) }, + { + name: 'long single-line assistant', + turn: baseTurn({ finalAssistantMessage: 'y'.repeat(5000) }), + }, + { + name: 'truncate marker preserved verbatim in prompt', + turn: baseTurn({ prompt: 'first 2KB of content\n[…truncated, originally 5 KB]' }), + }, + { + name: 'truncate marker preserved verbatim in assistant', + turn: baseTurn({ + finalAssistantMessage: 'reply body\n[…truncated, originally 8 KB]', + }), + }, + { + name: 'fractional second timestamp', + turn: baseTurn({ endedAt: '2026-05-12T15:00:05.123Z' }), + }, + { + name: 'unicode in session/model', + turn: baseTurn({ sessionId: 'sess-✨-1', model: 'gpt-5.5' }), + }, + { + name: 'numeric-looking content', + turn: baseTurn({ prompt: '1234567890', finalAssistantMessage: '0.0001' }), + }, + { + name: 'colon-heavy content (does not confuse frontmatter parser)', + turn: baseTurn({ + prompt: 'a:b:c:d', + finalAssistantMessage: 'session: not-a-real-frontmatter\nmodel: also-not-real', + }), + }, + ]; + + for (const { name, turn } of fixtures) { + it(`round-trips: ${name}`, () => { + const content = STRUCTURED_LOG_SENTINEL + '\n' + serializeTurn(turn); + const out = parseLog(content); + expect(out.sentinel).toBe(true); + expect(out.malformed).toBe(0); + expect(out.turns).toHaveLength(1); + const parsed = out.turns[0]; + expect(parsed.turnId).toBe(turn.turnId); + expect(parsed.sessionId).toBe(turn.sessionId); + expect(parsed.model).toBe(turn.model); + expect(parsed.status).toBe(turn.status); + expect(parsed.timestamp).toBe(turn.endedAt); + expect(parsed.prompt).toBe(turn.prompt); + expect(parsed.assistant).toBe(turn.finalAssistantMessage); + }); + } +}); diff --git a/packages/services/src/mindMemory/StructuredLogFormat.ts b/packages/services/src/mindMemory/StructuredLogFormat.ts new file mode 100644 index 00000000..e1265f60 --- /dev/null +++ b/packages/services/src/mindMemory/StructuredLogFormat.ts @@ -0,0 +1,217 @@ +/** + * Structured log format (chamber-structured-log/v1) — pure serializer + parser. + * + * A mind's `/.working-memory/log.md` is migrated to a structured form + * whose first non-blank line is the magic sentinel below. Each completed turn + * is appended as a self-delimited frame so the Dream Daemon can consume the + * log deterministically. + * + * Phase 2 scope (locked by plan): pure module only. No fs, no Electron, no + * Logger. Operates on strings in / strings out. Byte-budget truncation lives + * on `DailyLogWriter` (Phase 5). + * + * Frame format: + * + * + * ## turn: status: + * session: + * model: + * + * ### user + * + * + * ### assistant + * + * + * Heading separator: TWO spaces between `## ` and `turn:` and + * between `turn:` and `status:` are required. + * + * Embedded heading escape strategy: + * - The turn-heading regex anchors to line start AND requires the trailing + * `turn: status:` pair. Plain `## something` lines inside a + * body therefore parse as content, not as new turns. + * - The body-section markers `### user` and `### assistant` are recognised + * only when they (a) appear on their own line at column 0 AND (b) are + * immediately preceded by a blank line. Inline text such as + * `see ### user mode docs` or `## a markdown heading` round-trips fine. + * Pathological round-trips of bodies whose own content contains a + * blank-line-then-`### user|assistant` sequence are out of scope; callers + * producing such content must escape it themselves. + * + * The parser is deliberately tolerant: malformed blocks are dropped and + * counted, never thrown over, so a partially-corrupt log never blocks the + * daemon. + */ + +export const STRUCTURED_LOG_SENTINEL = ''; + +// `CompletedTurn` and `TurnStatus` were relocated to `@chamber/shared` in +// Phase 6 so ChatService (the producer) and DailyLogWriter (the first +// consumer) depend on a single canonical shape. Re-exported here for +// backward compatibility with Phase 5 callers that import from this module. +export type { CompletedTurn, TurnStatus } from '@chamber/shared/turn-observer'; +import type { CompletedTurn, TurnStatus } from '@chamber/shared/turn-observer'; + +export interface ParsedTurn { + readonly turnId: string; + readonly sessionId: string; + readonly model: string; + readonly status: TurnStatus; + readonly timestamp: string; + readonly prompt: string; + readonly assistant: string; +} + +export interface ParsedLog { + readonly sentinel: boolean; + readonly turns: ParsedTurn[]; + readonly malformed: number; +} + +const HEADING_RE = /^## (\S+) {2}turn:(\S+) {2}status:(\S+)$/; +const ISO_UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/; +const STATUS_VALUES: ReadonlySet = new Set([ + 'completed', + 'aborted', + 'error', +]); + +function normalize(content: string): string { + let s = content; + if (s.length > 0 && s.charCodeAt(0) === 0xfeff) { + s = s.slice(1); + } + return s.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); +} + +function firstNonBlankLine(lines: readonly string[]): { idx: number; value: string } | null { + for (let i = 0; i < lines.length; i++) { + if (lines[i].trim() !== '') { + return { idx: i, value: lines[i] }; + } + } + return null; +} + +export function detectSentinel(content: string): boolean { + const lines = normalize(content).split('\n'); + const first = firstNonBlankLine(lines); + return first !== null && first.value === STRUCTURED_LOG_SENTINEL; +} + +export function serializeTurn(turn: CompletedTurn): string { + const heading = `## ${turn.endedAt} turn:${turn.turnId} status:${turn.status}`; + return ( + heading + + '\n' + + `session: ${turn.sessionId}\n` + + `model: ${turn.model}\n` + + '\n' + + '### user\n' + + turn.prompt + + '\n' + + '\n' + + '### assistant\n' + + turn.finalAssistantMessage + + '\n' + ); +} + +function parseBlock(blockLines: readonly string[]): ParsedTurn | null { + if (blockLines.length === 0) return null; + + const headingMatch = blockLines[0].match(HEADING_RE); + if (!headingMatch) return null; + const [, ts, turnId, statusRaw] = headingMatch; + + if (!ISO_UTC_RE.test(ts) || Number.isNaN(Date.parse(ts))) return null; + if (!STATUS_VALUES.has(statusRaw)) return null; + const status = statusRaw as TurnStatus; + + if (blockLines.length < 3) return null; + const sessionMatch = blockLines[1].match(/^session: (.+)$/); + // `model:` accepts an empty value because the writer may not have a model + // selected at turn time (ChatService falls back to '' when both the + // turn-time model and the mind's selectedModel are unset). A frame with + // an empty model is still valid; we just record an empty string. + const modelMatch = blockLines[2].match(/^model: (.*)$/); + if (!sessionMatch || !modelMatch) return null; + const sessionId = sessionMatch[1]; + const model = modelMatch[1]; + + // `### user` must appear at column 0, preceded by a blank line. Earliest + // possible position is index 4: heading(0), session(1), model(2), blank(3). + let userIdx = -1; + for (let i = 4; i < blockLines.length; i++) { + if (blockLines[i] === '### user' && blockLines[i - 1] === '') { + userIdx = i; + break; + } + } + if (userIdx === -1) return null; + + let assistantIdx = -1; + for (let i = userIdx + 2; i < blockLines.length; i++) { + if (blockLines[i] === '### assistant' && blockLines[i - 1] === '') { + assistantIdx = i; + break; + } + } + if (assistantIdx === -1) return null; + + const userBodyLines = blockLines.slice(userIdx + 1, assistantIdx); + const assistantBodyLines = blockLines.slice(assistantIdx + 1); + + // Trim a single trailing blank line introduced by the serializer's + // section terminator on the user body, and any trailing blanks on the + // assistant body produced by concatenated turns or the final newline. + while (userBodyLines.length > 0 && userBodyLines[userBodyLines.length - 1] === '') { + userBodyLines.pop(); + } + while ( + assistantBodyLines.length > 0 && + assistantBodyLines[assistantBodyLines.length - 1] === '' + ) { + assistantBodyLines.pop(); + } + + return { + turnId, + sessionId, + model, + status, + timestamp: ts, + prompt: userBodyLines.join('\n'), + assistant: assistantBodyLines.join('\n'), + }; +} + +export function parseLog(content: string): ParsedLog { + const lines = normalize(content).split('\n'); + const first = firstNonBlankLine(lines); + if (first === null || first.value !== STRUCTURED_LOG_SENTINEL) { + return { sentinel: false, turns: [], malformed: 0 }; + } + + const headingIdxs: number[] = []; + for (let i = first.idx + 1; i < lines.length; i++) { + if (HEADING_RE.test(lines[i])) { + headingIdxs.push(i); + } + } + + const turns: ParsedTurn[] = []; + let malformed = 0; + for (let h = 0; h < headingIdxs.length; h++) { + const blockStart = headingIdxs[h]; + const blockEnd = h + 1 < headingIdxs.length ? headingIdxs[h + 1] : lines.length; + const parsed = parseBlock(lines.slice(blockStart, blockEnd)); + if (parsed === null) { + malformed += 1; + } else { + turns.push(parsed); + } + } + + return { sentinel: true, turns, malformed }; +} diff --git a/packages/services/src/mindMemory/__fakes__/FakeLLMClient.test.ts b/packages/services/src/mindMemory/__fakes__/FakeLLMClient.test.ts new file mode 100644 index 00000000..b2d0ef36 --- /dev/null +++ b/packages/services/src/mindMemory/__fakes__/FakeLLMClient.test.ts @@ -0,0 +1,53 @@ +/** + * Phase 8 — FakeLLMClient sanity tests. + * + * Phase 9 (DreamDaemon) wires its orchestrator tests against this fake; + * keeping a small smoke around the helper itself prevents drift in the + * canned-response semantics from silently skewing daemon tests later. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { createFakeLLMClient } from './FakeLLMClient'; + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe('createFakeLLMClient', () => { + it('records each synthesize call', async () => { + const client = createFakeLLMClient(); + await client.synthesize({ prompt: 'a', timeoutMs: 1_000 }); + await client.synthesize({ prompt: 'b', timeoutMs: 2_000, maxTokens: 32 }); + expect(client.calls.map((c) => c.prompt)).toEqual(['a', 'b']); + expect(client.calls[1].maxTokens).toBe(32); + }); + + it('returns the longest matching prefix response', async () => { + const client = createFakeLLMClient({ + responses: { 'memory:': 'short', 'memory:weekly:': 'long' }, + defaultResponse: 'fallback', + }); + expect(await client.synthesize({ prompt: 'memory:weekly:foo', timeoutMs: 1_000 })).toBe('long'); + expect(await client.synthesize({ prompt: 'memory:daily:foo', timeoutMs: 1_000 })).toBe('short'); + expect(await client.synthesize({ prompt: 'other', timeoutMs: 1_000 })).toBe('fallback'); + }); + + it('throws the configured error verbatim', async () => { + const client = createFakeLLMClient({ error: new Error('nope') }); + await expect(client.synthesize({ prompt: 'p', timeoutMs: 1_000 })).rejects.toThrow('nope'); + }); + + it('rejects with the canonical timeout message when latency exceeds timeoutMs', async () => { + const client = createFakeLLMClient({ latencyMs: 5_000 }); + const settled = client.synthesize({ prompt: 'p', timeoutMs: 250 }) + .catch((e: unknown) => e as Error); + await vi.advanceTimersByTimeAsync(250); + const err = await settled; + expect((err as Error).message).toBe('LLM synthesis timed out after 250ms'); + }); +}); diff --git a/packages/services/src/mindMemory/__fakes__/FakeLLMClient.ts b/packages/services/src/mindMemory/__fakes__/FakeLLMClient.ts new file mode 100644 index 00000000..808c946b --- /dev/null +++ b/packages/services/src/mindMemory/__fakes__/FakeLLMClient.ts @@ -0,0 +1,68 @@ +/** + * FakeLLMClient — in-memory `LLMClient` for unit tests. + * + * Phase 8 ships this so Phase 9 (DreamDaemon) can drive the orchestrator + * with canned responses keyed by prompt prefix. Co-located under + * `__fakes__/` because it is intentionally NOT part of the public package + * surface (`mindMemory/index.ts` re-exports the interface, not this helper). + */ + +import type { LLMClient, SynthesizeRequest } from '../LLMClient'; + +export interface FakeLLMClientOptions { + /** + * Map of prompt-prefix → canned response. The longest matching prefix + * wins so callers can layer specific overrides on top of generic ones. + */ + readonly responses?: Record; + /** Returned when no prefix matches. Defaults to an empty string. */ + readonly defaultResponse?: string; + /** Forced rejection; useful for error-path tests. */ + readonly error?: Error; + /** + * Artificial latency in ms. The fake honors `timeoutMs` by rejecting + * with the same shape `CopilotLLMClient` uses, so daemon tests can + * exercise the timeout branch without a real adapter. + */ + readonly latencyMs?: number; +} + +export interface FakeLLMClient extends LLMClient { + readonly calls: ReadonlyArray; +} + +export function createFakeLLMClient(options: FakeLLMClientOptions = {}): FakeLLMClient { + const calls: SynthesizeRequest[] = []; + const responses = options.responses ?? {}; + const prefixes = Object.keys(responses).sort((a, b) => b.length - a.length); + + return { + get calls() { + return calls; + }, + async synthesize(req: SynthesizeRequest): Promise { + calls.push(req); + if (options.error) throw options.error; + + const latency = options.latencyMs ?? 0; + if (latency > 0) { + const timedOut = await new Promise((resolve) => { + const t = setTimeout(() => resolve(false), latency); + const onTimeout = setTimeout(() => { + clearTimeout(t); + resolve(true); + }, req.timeoutMs); + t.unref?.(); + onTimeout.unref?.(); + }); + if (timedOut) { + throw new Error(`LLM synthesis timed out after ${req.timeoutMs}ms`); + } + } + + const match = prefixes.find((p) => req.prompt.startsWith(p)); + if (match) return responses[match]; + return options.defaultResponse ?? ''; + }, + }; +} diff --git a/packages/services/src/mindMemory/consolidation-priorities.test.ts b/packages/services/src/mindMemory/consolidation-priorities.test.ts new file mode 100644 index 00000000..9c9317b8 --- /dev/null +++ b/packages/services/src/mindMemory/consolidation-priorities.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, it } from 'vitest'; + +import { + type EntryPriority, + getEntryPriority, + sortByPriority, + trimToFit, +} from './consolidation-priorities'; +import type { MemoryEntry } from './memory-entries'; + +function makeEntry( + type: MemoryEntry['type'], + name: string, + createdAt?: string, + content = 'placeholder body for the entry', +): MemoryEntry { + return { + type, + name, + description: `${name} description`, + content, + createdAt, + }; +} + +describe('getEntryPriority', () => { + it('maps user → critical', () => { + expect(getEntryPriority(makeEntry('user', 'X'))).toBe('critical'); + }); + + it('maps prohibition → critical', () => { + expect(getEntryPriority(makeEntry('prohibition', 'X'))).toBe('critical'); + }); + + it('maps feedback → high', () => { + expect(getEntryPriority(makeEntry('feedback', 'X'))).toBe('high'); + }); + + it('maps project → medium', () => { + expect(getEntryPriority(makeEntry('project', 'X'))).toBe('medium'); + }); + + it('maps reference → low', () => { + expect(getEntryPriority(makeEntry('reference', 'X'))).toBe('low'); + }); +}); + +describe('sortByPriority', () => { + it('returns critical entries before lower priorities', () => { + const entries: MemoryEntry[] = [ + makeEntry('reference', 'R'), + makeEntry('user', 'U'), + makeEntry('project', 'P'), + makeEntry('prohibition', 'PRO'), + makeEntry('feedback', 'F'), + ]; + const sorted = sortByPriority(entries); + const types = sorted.map((e) => e.type); + // critical (user, prohibition) → high (feedback) → medium (project) → low (reference) + expect(types.slice(0, 2)).toEqual(expect.arrayContaining(['user', 'prohibition'])); + expect(types[2]).toBe('feedback'); + expect(types[3]).toBe('project'); + expect(types[4]).toBe('reference'); + }); + + it('sorts within the same priority by createdAt descending (newer first)', () => { + const entries: MemoryEntry[] = [ + makeEntry('user', 'old', '2026-01-01T00:00:00Z'), + makeEntry('user', 'new', '2026-05-01T00:00:00Z'), + makeEntry('user', 'middle', '2026-03-01T00:00:00Z'), + ]; + const sorted = sortByPriority(entries).map((e) => e.name); + expect(sorted).toEqual(['new', 'middle', 'old']); + }); + + it('does not mutate the input array', () => { + const entries: MemoryEntry[] = [ + makeEntry('reference', 'R'), + makeEntry('user', 'U'), + ]; + const snapshot = [...entries]; + sortByPriority(entries); + expect(entries).toEqual(snapshot); + }); + + it('handles empty input', () => { + expect(sortByPriority([])).toEqual([]); + }); + + it('places entries with createdAt before those without (within same priority)', () => { + const entries: MemoryEntry[] = [ + makeEntry('user', 'no-date'), + makeEntry('user', 'has-date', '2026-05-01T00:00:00Z'), + ]; + const sorted = sortByPriority(entries).map((e) => e.name); + expect(sorted).toEqual(['has-date', 'no-date']); + }); +}); + +describe('trimToFit', () => { + it('returns [] for empty input', () => { + expect(trimToFit([], 10, 1000)).toEqual([]); + }); + + it('returns the original entries when they already fit', () => { + const entries: MemoryEntry[] = [makeEntry('user', 'A'), makeEntry('project', 'B')]; + const out = trimToFit(entries, 200, 25_000); + expect(out).toHaveLength(2); + }); + + it('drops lowest-priority entries first when over the line limit', () => { + const entries: MemoryEntry[] = [ + makeEntry('reference', 'low-pri'), + makeEntry('project', 'mid-pri'), + makeEntry('user', 'top-pri'), + ]; + // Each serialized entry takes ~6 lines (frontmatter + content). Limit at 12 lines forces + // trimming the lowest-priority (reference) entry. + const out = trimToFit(entries, 12, 25_000); + const names = out.map((e) => e.name); + expect(names).toContain('top-pri'); + expect(names).not.toContain('low-pri'); + }); + + it('returns [] when the limits cannot fit even the highest-priority entry', () => { + const entries: MemoryEntry[] = [makeEntry('user', 'unfittable')]; + expect(trimToFit(entries, 1, 10)).toEqual([]); + }); + + it('drops by priority then by age within a priority', () => { + const entries: MemoryEntry[] = [ + makeEntry('reference', 'newest-low', '2026-05-01T00:00:00Z'), + makeEntry('reference', 'oldest-low', '2026-01-01T00:00:00Z'), + makeEntry('user', 'critical', '2026-05-01T00:00:00Z'), + ]; + // Force enough pressure to drop one reference entry — older one should go. + const out = trimToFit(entries, 15, 25_000); + const names = out.map((e) => e.name); + expect(names).toContain('critical'); + expect(names).toContain('newest-low'); + expect(names).not.toContain('oldest-low'); + }); +}); diff --git a/packages/services/src/mindMemory/consolidation-priorities.ts b/packages/services/src/mindMemory/consolidation-priorities.ts new file mode 100644 index 00000000..07a966c1 --- /dev/null +++ b/packages/services/src/mindMemory/consolidation-priorities.ts @@ -0,0 +1,64 @@ +/** + * Entry prioritization for MEMORY.md consolidation. + * Determines which entries to keep when pruning is needed. + * + * Pure module: no I/O, no logging. + */ +import { type MemoryEntry, serializeMemoryMd } from './memory-entries'; +import { countBytes, countLines } from './memory-limits'; + +export type EntryPriority = 'critical' | 'high' | 'medium' | 'low'; + +const PRIORITY_MAP: Record = { + user: 'critical', + feedback: 'high', + prohibition: 'critical', + project: 'medium', + reference: 'low', +}; + +const PRIORITY_RANK: Record = { + critical: 0, + high: 1, + medium: 2, + low: 3, +}; + +export function getEntryPriority(entry: MemoryEntry): EntryPriority { + return PRIORITY_MAP[entry.type]; +} + +export function sortByPriority(entries: ReadonlyArray): ReadonlyArray { + return [...entries].sort((a, b) => { + const rankDiff = PRIORITY_RANK[getEntryPriority(a)] - PRIORITY_RANK[getEntryPriority(b)]; + if (rankDiff !== 0) return rankDiff; + + const dateA = a.createdAt ?? ''; + const dateB = b.createdAt ?? ''; + if (dateA && dateB) return dateB.localeCompare(dateA); + if (dateA) return -1; + if (dateB) return 1; + return 0; + }); +} + +export function trimToFit( + entries: ReadonlyArray, + maxLines: number, + maxBytes: number, +): ReadonlyArray { + if (entries.length === 0) return []; + + const sorted = sortByPriority(entries); + let current = [...sorted]; + + while (current.length > 0) { + const serialized = serializeMemoryMd(current); + if (countLines(serialized) <= maxLines && countBytes(serialized) <= maxBytes) { + return current; + } + current = current.slice(0, -1); + } + + return []; +} diff --git a/packages/services/src/mindMemory/consolidation-scheduler.test.ts b/packages/services/src/mindMemory/consolidation-scheduler.test.ts new file mode 100644 index 00000000..63595ebf --- /dev/null +++ b/packages/services/src/mindMemory/consolidation-scheduler.test.ts @@ -0,0 +1,289 @@ +/** + * Tests for consolidation-scheduler — cron evaluation, per-mind in-memory + * mutex (try-lock), and tick orchestration that combines cron, gates, DB + * lock, and run/persist. + * + * Phase 7 acceptance: + * - evaluateCron answers due/not-due against a controlled clock. + * - withMindMutex serializes per mind; concurrent acquires fail-fast. + * - tick() respects gates, DB lock, and the mutex. + * - tick() records run history on success and skips with a reason on + * each gate failure path. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import Database from 'better-sqlite3'; + +import { migrate } from './dream-schema'; +import { + acquireLock, + buildLockHolder, + getLock, + incrementTurnCount, + listRuns, + readState, +} from './dream-state'; +import { + __resetMindMutexForTesting, + createConsolidationScheduler, + evaluateCron, + withMindMutex, +} from './consolidation-scheduler'; + +let db: Database.Database; + +beforeEach(() => { + db = new Database(':memory:'); + migrate(db); + __resetMindMutexForTesting(); +}); + +afterEach(() => { + db.close(); +}); + +describe('consolidation-scheduler — evaluateCron', () => { + it('reports due when nextRun(lastFireAt) <= now', () => { + // Daily at 03:00 UTC. + const last = new Date('2026-05-12T00:00:00Z'); + const now = new Date('2026-05-12T03:00:01Z'); + const r = evaluateCron('0 3 * * *', last, now, { timezone: 'UTC' }); + expect(r.due).toBe(true); + expect(r.nextDueAt!.toISOString()).toBe('2026-05-12T03:00:00.000Z'); + }); + + it('reports not-due when nextRun is in the future', () => { + const last = new Date('2026-05-12T03:00:00Z'); + const now = new Date('2026-05-12T04:00:00Z'); + const r = evaluateCron('0 3 * * *', last, now, { timezone: 'UTC' }); + expect(r.due).toBe(false); + expect(r.nextDueAt!.toISOString()).toBe('2026-05-13T03:00:00.000Z'); + }); + + it('treats lastFireAt=null as the unix epoch (always-due on first tick)', () => { + const r = evaluateCron('0 3 * * *', null, new Date('2026-05-12T03:00:01Z'), { timezone: 'UTC' }); + expect(r.due).toBe(true); + }); +}); + +describe('consolidation-scheduler — withMindMutex (try-lock)', () => { + it('serializes runs for the same mindId; second concurrent caller fails fast', async () => { + let release!: () => void; + const blocker = new Promise((r) => { + release = r; + }); + + const a = withMindMutex('mind-x', async () => { + await blocker; + return 'a-done'; + }); + const b = withMindMutex('mind-x', async () => 'b-done'); + + await expect(b).resolves.toEqual({ acquired: false, reason: 'locked' }); + release(); + await expect(a).resolves.toEqual({ acquired: true, value: 'a-done' }); + }); + + it('different mindIds do not block each other', async () => { + let release!: () => void; + const blocker = new Promise((r) => { + release = r; + }); + + const a = withMindMutex('mind-a', async () => { + await blocker; + return 'a'; + }); + const b = withMindMutex('mind-b', async () => 'b'); + + await expect(b).resolves.toEqual({ acquired: true, value: 'b' }); + release(); + await expect(a).resolves.toEqual({ acquired: true, value: 'a' }); + }); + + it('releases the slot after rejection so the next call may proceed', async () => { + const first = withMindMutex('mind-x', async () => { + throw new Error('boom'); + }); + await expect(first).rejects.toThrow(/boom/); + + const second = withMindMutex('mind-x', async () => 'ok'); + await expect(second).resolves.toEqual({ acquired: true, value: 'ok' }); + }); +}); + +describe('consolidation-scheduler — tick', () => { + function buildScheduler(opts: { + now: number; + runDaily?: () => Promise; + cron?: string; + minTurnsBetweenRuns?: number; + minIntervalMs?: number; + lockTtlMs?: number; + }) { + let current = opts.now; + const clock = () => new Date(current); + const setNow = (n: number) => { + current = n; + }; + + const sched = createConsolidationScheduler({ + mindId: 'mind-x', + db, + cronExpr: opts.cron ?? '* * * * *', // every minute by default + gateConfig: { + minTurnsBetweenRuns: opts.minTurnsBetweenRuns ?? 1, + minIntervalMs: opts.minIntervalMs ?? 0, + }, + lockTtlMs: opts.lockTtlMs ?? 60_000, + clock, + runDaily: + opts.runDaily ?? + (async () => { + /* no-op */ + }), + timezone: 'UTC', + }); + + return { sched, setNow }; + } + + it('runs daily, records the run, marks phase complete, and resets activity', async () => { + incrementTurnCount(db, 3); + const { sched } = buildScheduler({ now: 1_700_000_000_000 }); + + const r = await sched.tick(); + expect(r.run).toBe(true); + expect(r.reason).toBe('ready'); + expect(r.phase).toBe('daily'); + + const state = readState(db); + expect(state.turnsSinceLastRun).toBe(0); + expect(state.lastDailyAt).toBe(1_700_000_000_000); + + const runs = listRuns(db); + expect(runs).toHaveLength(1); + expect(runs[0].status).toBe('success'); + expect(runs[0].phase).toBe('daily'); + + expect(getLock(db, 'daily')).toBeNull(); + }); + + it('skips with reason=no-activity when activity gate fails (no run recorded as success)', async () => { + const { sched } = buildScheduler({ now: 1_700_000_000_000, minTurnsBetweenRuns: 5 }); + const r = await sched.tick(); + expect(r.run).toBe(false); + expect(r.reason).toBe('no-activity'); + + const runs = listRuns(db); + expect(runs).toHaveLength(1); + expect(runs[0].status).toBe('skipped'); + expect(runs[0].reason).toBe('no-activity'); + }); + + it('skips with reason=cron-not-due when cron has not yet matched', async () => { + incrementTurnCount(db, 1); + // Cron at 03:00 UTC every day; current time is 02:00 UTC and lastDailyAt + // is "today at 03:00", so nextRun(03:00) is tomorrow at 03:00 → not due. + const today03 = new Date('2026-05-12T03:00:00Z').getTime(); + const today02 = new Date('2026-05-12T02:00:00Z').getTime(); + db.prepare('UPDATE dream_state SET last_daily_at = ? WHERE id = 1').run(today03); + + const { sched } = buildScheduler({ + now: today02 + 24 * 60 * 60 * 1000, // jump a day forward but before next 03:00 + cron: '0 3 * * *', + }); + // Adjust: now = 2026-05-13T02:00:00Z, last = 2026-05-12T03:00:00Z, + // nextRun(last) = 2026-05-13T03:00:00Z → still > now, not due. + const r = await sched.tick(); + expect(r.run).toBe(false); + expect(r.reason).toBe('cron-not-due'); + }); + + it('returns reason=locked when the in-memory mutex is held by a concurrent tick', async () => { + incrementTurnCount(db, 3); + + let release!: () => void; + const blocker = new Promise((r) => { + release = r; + }); + + const { sched } = buildScheduler({ + now: 1_700_000_000_000, + runDaily: async () => { + await blocker; + }, + }); + + const first = sched.tick(); + const second = await sched.tick(); + + expect(second.run).toBe(false); + expect(second.reason).toBe('locked'); + + release(); + const r1 = await first; + expect(r1.run).toBe(true); + }); + + it('returns reason=db-locked when another process holds a fresh DB lock', async () => { + incrementTurnCount(db, 3); + // Simulate another process holding the lock with plenty of TTL left. + acquireLock(db, { + phase: 'daily', + mindId: 'someone-else', + now: 1_700_000_000_000, + ttlMs: 60_000, + }); + + const { sched } = buildScheduler({ now: 1_700_000_000_000 }); + const r = await sched.tick(); + expect(r.run).toBe(false); + expect(r.reason).toBe('db-locked'); + + // Skipped run must be recorded. + const runs = listRuns(db); + expect(runs.some((x) => x.status === 'skipped' && x.reason === 'db-locked')).toBe(true); + }); + + it('breaks a stale DB lock and proceeds', async () => { + incrementTurnCount(db, 3); + db.prepare( + 'INSERT INTO dream_locks (phase, holder, acquired_at, expires_at) VALUES (?, ?, ?, ?)', + ).run('daily', buildLockHolder('ghost', 1, 'old'), 0, 1); + + const { sched } = buildScheduler({ now: 1_700_000_000_000 }); + const r = await sched.tick(); + expect(r.run).toBe(true); + expect(r.reason).toBe('ready'); + }); + + it('records a failed run when runDaily throws and releases the lock', async () => { + incrementTurnCount(db, 3); + const failing = vi.fn(async () => { + throw new Error('synthetic consolidator failure'); + }); + + const { sched } = buildScheduler({ now: 1_700_000_000_000, runDaily: failing }); + + await expect(sched.tick()).rejects.toThrow(/synthetic consolidator failure/); + + expect(failing).toHaveBeenCalledTimes(1); + const runs = listRuns(db); + expect(runs[0].status).toBe('failed'); + expect(getLock(db, 'daily')).toBeNull(); + // Activity counter is NOT reset on failure (the consolidator did not run + // to completion). + expect(readState(db).turnsSinceLastRun).toBe(3); + }); + + it('getNextDueAt projects from lastDailyAt', () => { + db.prepare('UPDATE dream_state SET last_daily_at = ? WHERE id = 1').run( + new Date('2026-05-12T03:00:00Z').getTime(), + ); + const { sched } = buildScheduler({ now: Date.UTC(2026, 4, 12, 3, 0, 0), cron: '0 3 * * *' }); + const next = sched.getNextDueAt(); + expect(next!.toISOString()).toBe('2026-05-13T03:00:00.000Z'); + }); +}); diff --git a/packages/services/src/mindMemory/consolidation-scheduler.ts b/packages/services/src/mindMemory/consolidation-scheduler.ts new file mode 100644 index 00000000..21e29aaa --- /dev/null +++ b/packages/services/src/mindMemory/consolidation-scheduler.ts @@ -0,0 +1,267 @@ +/** + * consolidation-scheduler — composes croner-based cron evaluation, the + * per-mind in-memory mutex, the DB lock, and the activity/time gates into + * a single `tick()` orchestration. + * + * Three layers of mutual exclusion: + * + * 1. Per-mind in-memory mutex (try-lock). Defeats same-process races + * where two `tick()` calls land in the JS event loop before the DB + * lock row is written. Implemented as `Map` with + * fail-fast semantics — the second concurrent caller returns + * `{ acquired: false, reason: 'locked' }` instead of queuing. + * + * 2. DB lock row in `dream_locks`. Defeats cross-process races. Honors + * a configurable TTL so a crashed daemon cannot wedge the mind + * forever — see `dream-state.acquireLock`. + * + * 3. Combined activity + time gate (`dream-gates.evaluateGates`). The + * cron expression is treated as a coarse "is it the configured wake + * time yet?" filter on top of the time gate. + * + * Phase 7 ships only the daily phase; weekly/monthly land in Phase 9. + * + * `evaluateCron` is exported separately so the scheduler can be stubbed + * in higher-level tests without dragging in croner. + */ + +import { Cron } from 'croner'; +import type Database from 'better-sqlite3'; + +import { Logger } from '../logger'; +import { evaluateGates, type GateConfig } from './dream-gates'; +import { + acquireLock, + buildLockHolder, + getLock, + markPhaseComplete, + readState, + recordRun, + releaseLock, +} from './dream-state'; + +export interface CronEvaluation { + readonly due: boolean; + readonly nextDueAt: Date | null; +} + +export function evaluateCron( + cronExpr: string, + lastFireAt: Date | null, + now: Date, + opts: { timezone?: string } = {}, +): CronEvaluation { + // `paused: true` disables the croner background timer — we only use the + // expression evaluator. + const job = new Cron(cronExpr, { paused: true, timezone: opts.timezone }); + try { + const seed = lastFireAt ?? new Date(0); + const next = job.nextRun(seed); + if (!next) return { due: false, nextDueAt: null }; + return { due: next.getTime() <= now.getTime(), nextDueAt: next }; + } finally { + job.stop(); + } +} + +// --------------------------------------------------------------------------- +// Per-mind in-memory mutex (try-lock) +// --------------------------------------------------------------------------- + +const mindMutex = new Map>(); + +export type WithMindMutexResult = + | { readonly acquired: true; readonly value: T } + | { readonly acquired: false; readonly reason: 'locked' }; + +export async function withMindMutex( + mindId: string, + fn: () => Promise, +): Promise> { + if (mindMutex.has(mindId)) { + return { acquired: false, reason: 'locked' }; + } + const promise = (async () => fn())(); + mindMutex.set( + mindId, + promise.then( + () => undefined, + () => undefined, + ), + ); + try { + const value = await promise; + return { acquired: true, value }; + } finally { + mindMutex.delete(mindId); + } +} + +/** Test-only helper to drop in-memory state between tests. */ +export function __resetMindMutexForTesting(): void { + mindMutex.clear(); +} + +// --------------------------------------------------------------------------- +// Scheduler +// --------------------------------------------------------------------------- + +export interface RunContext { + readonly mindId: string; + readonly startedAt: number; +} + +export interface SchedulerOptions { + readonly mindId: string; + readonly db: Database.Database; + readonly cronExpr: string; + readonly gateConfig: GateConfig; + readonly lockTtlMs: number; + readonly clock: () => Date; + readonly runDaily: (ctx: RunContext) => Promise; + readonly timezone?: string; +} + +export type TickReason = + | 'ready' + | 'locked' + | 'db-locked' + | 'no-activity' + | 'too-soon' + | 'cron-not-due'; + +export interface TickResult { + readonly run: boolean; + readonly reason: TickReason; + readonly phase?: 'daily'; +} + +export interface ConsolidationScheduler { + tick(): Promise; + getNextDueAt(): Date | null; +} + +export function createConsolidationScheduler(opts: SchedulerOptions): ConsolidationScheduler { + const log = Logger.create('ConsolidationScheduler'); + + function projectNextDue(): Date | null { + const state = readState(opts.db); + const last = state.lastDailyAt !== null ? new Date(state.lastDailyAt) : null; + return evaluateCron(opts.cronExpr, last, opts.clock(), { timezone: opts.timezone }).nextDueAt; + } + + async function runOnceLocked(): Promise { + const startedAtDate = opts.clock(); + const now = startedAtDate.getTime(); + + const state = readState(opts.db); + const lockRow = getLock(opts.db, 'daily'); + const lockHeldByOther = lockRow !== null && lockRow.expiresAt > now; + + // Cron evaluation — the configured wake time must have passed since + // the last successful daily run. + const cron = evaluateCron( + opts.cronExpr, + state.lastDailyAt !== null ? new Date(state.lastDailyAt) : null, + startedAtDate, + { timezone: opts.timezone }, + ); + if (!cron.due) { + recordRun(opts.db, { + phase: 'daily', + startedAt: now, + endedAt: now, + status: 'skipped', + reason: 'cron-not-due', + }); + return { run: false, reason: 'cron-not-due', phase: 'daily' }; + } + + // Activity + time gate (lockHeld surfaced for completeness, but + // db-locked is reported separately below for clearer triage). + const gate = evaluateGates( + { phase: 'daily', state, now, lockHeld: lockHeldByOther }, + opts.gateConfig, + ); + if (!gate.run) { + const reason: TickReason = gate.reason === 'locked' ? 'db-locked' : gate.reason; + recordRun(opts.db, { + phase: 'daily', + startedAt: now, + endedAt: now, + status: 'skipped', + reason, + }); + return { run: false, reason, phase: 'daily' }; + } + + // Try to acquire the DB lock (with stale-break). + const lock = acquireLock(opts.db, { + phase: 'daily', + mindId: opts.mindId, + now, + ttlMs: opts.lockTtlMs, + }); + if (!lock.acquired) { + recordRun(opts.db, { + phase: 'daily', + startedAt: now, + endedAt: now, + status: 'skipped', + reason: 'db-locked', + }); + return { run: false, reason: 'db-locked', phase: 'daily' }; + } + + const holder = lock.holder ?? buildLockHolder(opts.mindId); + try { + await opts.runDaily({ mindId: opts.mindId, startedAt: now }); + const endedAt = opts.clock().getTime(); + + // Persist success: timestamp + reset activity + run history. + // Activity reset is intentional only on success — a failed run + // leaves the counter intact so the next tick will retry. + opts.db.transaction(() => { + opts.db.prepare('UPDATE dream_state SET turns_since_last_run = 0, last_daily_at = ? WHERE id = 1').run(endedAt); + })(); + // markPhaseComplete is functionally equivalent for daily but kept + // for symmetry with future weekly/monthly callers. + markPhaseComplete(opts.db, 'daily', endedAt); + recordRun(opts.db, { + phase: 'daily', + startedAt: now, + endedAt, + status: 'success', + reason: 'ready', + }); + return { run: true, reason: 'ready', phase: 'daily' }; + } catch (err) { + const endedAt = opts.clock().getTime(); + const message = err instanceof Error ? err.message : String(err); + recordRun(opts.db, { + phase: 'daily', + startedAt: now, + endedAt, + status: 'failed', + reason: message, + }); + log.warn(`daily consolidation failed for mind ${opts.mindId}: ${message}`); + throw err; + } finally { + releaseLock(opts.db, 'daily', holder); + } + } + + return { + async tick(): Promise { + const outcome = await withMindMutex(opts.mindId, () => runOnceLocked()); + if (!outcome.acquired) { + return { run: false, reason: 'locked', phase: 'daily' }; + } + return outcome.value; + }, + getNextDueAt(): Date | null { + return projectNextDue(); + }, + }; +} diff --git a/packages/services/src/mindMemory/consolidation.test.ts b/packages/services/src/mindMemory/consolidation.test.ts new file mode 100644 index 00000000..116641c6 --- /dev/null +++ b/packages/services/src/mindMemory/consolidation.test.ts @@ -0,0 +1,290 @@ +import { describe, it, expect } from 'vitest'; +import { + orient, + gather, + consolidate, + prune, + runConsolidation, +} from './consolidation'; +import { serializeMemoryMd } from './memory-entries'; +import type { MemoryEntry } from './memory-entries'; + +function entry(overrides: Partial = {}): MemoryEntry { + return { + type: 'project', + name: 'test-entry', + description: 'A test entry', + content: 'Some test content.', + ...overrides, + }; +} + +function buildMd(entries: MemoryEntry[]): string { + return serializeMemoryMd(entries); +} + +const REF_DATE = new Date('2025-06-15'); + +describe('orient', () => { + it('parses valid MEMORY.md into correct entries', () => { + const entries = [ + entry({ name: 'Pref A', type: 'user', description: 'desc A', content: 'content A' }), + entry({ name: 'Proj B', type: 'project', description: 'desc B', content: 'content B' }), + ]; + const md = buildMd(entries); + const result = orient(md); + expect(result).toHaveLength(2); + expect(result[0]!.name).toBe('Pref A'); + expect(result[1]!.name).toBe('Proj B'); + }); + + it('empty MEMORY.md → empty array', () => { + expect(orient('')).toEqual([]); + expect(orient(' ')).toEqual([]); + }); + + it('malformed MEMORY.md → empty array (graceful)', () => { + expect(orient('not valid markdown at all')).toEqual([]); + expect(orient('---\nbroken\n')).toEqual([]); + }); + + it('converts relative dates to absolute', () => { + const entries = [entry({ name: 'E1', content: 'Discovered yesterday in code review.' })]; + const md = buildMd(entries); + const result = orient(md, REF_DATE); + expect(result[0]!.content).toContain('2025-06-14'); + expect(result[0]!.content).not.toContain('yesterday'); + }); +}); + +describe('gather', () => { + it('valid entries pass through', () => { + const input: MemoryEntry[] = [entry({ name: 'G1' }), entry({ name: 'G2' })]; + const result = gather(input); + expect(result).toHaveLength(2); + }); + + it('invalid entries filtered out', () => { + const input = [ + entry({ name: 'Good' }), + { type: 'user', name: '', description: '', content: '' } as unknown as MemoryEntry, + ]; + const result = gather(input); + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('Good'); + }); + + it('converts relative dates', () => { + const input = [entry({ name: 'E1', content: 'Added yesterday.' })]; + const result = gather(input, REF_DATE); + expect(result[0]!.content).toContain('2025-06-14'); + }); + + it('empty input → empty output', () => { + expect(gather([])).toEqual([]); + }); +}); + +describe('consolidate', () => { + it('existing + new merged correctly (new entries are newer)', () => { + const existing = [entry({ name: 'Ex1', content: 'existing' })]; + const gathered = [entry({ name: 'New1', content: 'new thing' })]; + const result = consolidate(existing, gathered); + expect(result.entries.length).toBeGreaterThanOrEqual(2); + }); + + it('duplicate entries deduplicated (keep newer)', () => { + const existing = [ + entry({ name: 'DB Info', content: 'Uses PostgreSQL with Supabase for data.' }), + ]; + const gathered = [ + entry({ name: 'Database Info', content: 'Uses PostgreSQL with Supabase for data!' }), + ]; + const result = consolidate(existing, gathered); + expect(result.entries).toHaveLength(1); + expect(result.entries[0]!.name).toBe('Database Info'); + expect(result.deduped).toBeGreaterThanOrEqual(1); + }); + + it('contradicting entries resolved (keep newer)', () => { + const existing = [entry({ name: 'DB', content: 'Use MySQL' })]; + const gathered = [entry({ name: 'DB', content: 'Use PostgreSQL' })]; + const result = consolidate(existing, gathered); + expect(result.entries).toHaveLength(1); + expect(result.entries[0]!.content).toBe('Use PostgreSQL'); + expect(result.contradictions).toBeGreaterThanOrEqual(1); + }); + + it('no duplicates → all kept, deduped=0', () => { + const existing = [entry({ name: 'X', content: 'xxx-distinct' })]; + const gathered = [entry({ name: 'Y', content: 'yyy-different' })]; + const result = consolidate(existing, gathered); + expect(result.entries).toHaveLength(2); + expect(result.deduped).toBe(0); + expect(result.contradictions).toBe(0); + }); + + it('groups entries by type (user before reference)', () => { + const existing: MemoryEntry[] = []; + const gathered: MemoryEntry[] = [ + entry({ + type: 'reference', + name: 'Ref1', + content: 'A useful reference link.', + createdAt: '2025-01-01', + }), + entry({ + type: 'user', + name: 'Usr1', + content: 'User prefers dark mode always.', + createdAt: '2025-01-01', + }), + entry({ + type: 'project', + name: 'Prj1', + content: 'Project uses Node.js runtime.', + createdAt: '2025-01-01', + }), + ]; + const result = consolidate(existing, gathered); + const types = result.entries.map((e) => e.type); + const userIdx = types.indexOf('user'); + const refIdx = types.indexOf('reference'); + expect(userIdx).toBeLessThan(refIdx); + }); +}); + +describe('prune', () => { + it('under limits → no pruning, truncated=false', () => { + const entries = [entry({ name: 'Small', content: 'tiny.' })]; + const result = prune(entries); + expect(result.truncated).toBe(false); + expect(result.entries).toHaveLength(1); + }); + + it('over line limit → entries removed to fit', () => { + const entries: MemoryEntry[] = []; + for (let i = 0; i < 50; i++) { + entries.push( + entry({ + name: `Entry-${i}`, + type: i < 5 ? 'user' : 'reference', + content: `Line 1\nLine 2\nLine 3\nLine 4\nLine 5`, + createdAt: `2025-01-${String(i + 1).padStart(2, '0')}`, + }), + ); + } + const result = prune(entries); + expect(result.entries.length).toBeLessThan(50); + }); + + it('reference entries dropped before user entries', () => { + const entries: MemoryEntry[] = []; + for (let i = 0; i < 10; i++) { + entries.push( + entry({ + name: `User-${i}`, + type: 'user', + content: 'User pref line.', + createdAt: `2025-06-${String(i + 1).padStart(2, '0')}`, + }), + ); + } + for (let i = 0; i < 40; i++) { + entries.push( + entry({ + name: `Ref-${i}`, + type: 'reference', + content: `Ref line 1\nRef line 2\nRef line 3\nRef line 4`, + createdAt: `2025-01-${String(i + 1).padStart(2, '0')}`, + }), + ); + } + const result = prune(entries); + const userCount = result.entries.filter((e) => e.type === 'user').length; + expect(userCount).toBe(10); + }); + + it('truncation as final safety net', () => { + const entries: MemoryEntry[] = [ + entry({ name: 'Big-user', type: 'user', content: 'X\n'.repeat(250) }), + ]; + const result = prune(entries); + expect(result.truncated).toBe(true); + }); +}); + +describe('runConsolidation', () => { + it('full pipeline: existing MEMORY.md + new entries → improved MEMORY.md', () => { + const existingMd = buildMd([ + entry({ name: 'Existing', type: 'project', content: 'Existing project info.' }), + ]); + const newEntries: MemoryEntry[] = [ + entry({ name: 'New Pref', type: 'user', content: 'New user preference.' }), + ]; + const result = runConsolidation({ currentMemoryMd: existingMd, newEntries }); + expect(result.memoryMd).toContain('Existing'); + expect(result.memoryMd).toContain('New Pref'); + expect(result.entriesProcessed).toBe(2); + expect(result.entriesKept).toBe(2); + }); + + it('existing with same-name new entry → contradiction resolved (keep newer)', () => { + const existingMd = buildMd([entry({ name: 'SharedName', content: 'old content' })]); + const newEntries: MemoryEntry[] = [entry({ name: 'SharedName', content: 'updated content' })]; + const result = runConsolidation({ currentMemoryMd: existingMd, newEntries }); + expect(result.contradictionsResolved).toBeGreaterThanOrEqual(1); + expect(result.memoryMd).toContain('updated content'); + expect(result.entriesKept).toBe(1); + }); + + it('phase log has correct stats', () => { + const existingMd = buildMd([entry({ name: 'A', type: 'user', content: 'A content' })]); + const newEntries: MemoryEntry[] = [entry({ name: 'B', type: 'project', content: 'B content' })]; + const result = runConsolidation({ currentMemoryMd: existingMd, newEntries }); + expect(result.phases.orient.existingEntries).toBe(1); + expect(result.phases.gather.newEntries).toBe(1); + expect(result.phases.consolidate.merged).toBe(2); + }); + + it('result fits within limits when overflowing', () => { + const newEntries: MemoryEntry[] = Array.from({ length: 60 }, (_, i) => + entry({ + name: `Entry-${i}`, + type: 'reference', + content: `Line 1\nLine 2\nLine 3\nLine 4`, + createdAt: `2025-01-${String((i % 28) + 1).padStart(2, '0')}`, + }), + ); + const result = runConsolidation({ currentMemoryMd: '', newEntries }); + const lines = result.memoryMd.split('\n').length; + expect(lines).toBeLessThanOrEqual(210); + }); + + it('idempotency: run twice with same input → same output', () => { + const existingMd = buildMd([ + entry({ name: 'Stable', type: 'user', content: 'Stable content.' }), + ]); + const newEntries: MemoryEntry[] = [ + entry({ name: 'New', type: 'project', content: 'New content.' }), + ]; + const input = { currentMemoryMd: existingMd, newEntries, referenceDate: REF_DATE }; + const result1 = runConsolidation(input); + const result2 = runConsolidation(input); + expect(result1.memoryMd).toBe(result2.memoryMd); + expect(result1.entriesKept).toBe(result2.entriesKept); + }); + + it('referenceDate is used for date conversion', () => { + const newEntries: MemoryEntry[] = [ + entry({ name: 'DatedEntry', content: 'Found yesterday in the logs.' }), + ]; + const result = runConsolidation({ + currentMemoryMd: '', + newEntries, + referenceDate: REF_DATE, + }); + expect(result.memoryMd).toContain('2025-06-14'); + expect(result.memoryMd).not.toContain('yesterday'); + }); +}); diff --git a/packages/services/src/mindMemory/consolidation.ts b/packages/services/src/mindMemory/consolidation.ts new file mode 100644 index 00000000..661d7518 --- /dev/null +++ b/packages/services/src/mindMemory/consolidation.ts @@ -0,0 +1,198 @@ +/** + * Four-phase consolidation pipeline for MEMORY.md. + * orient → gather → consolidate → prune + * + * Pure functions — no file I/O, no database access. + * + * Ported from SCNS (`scns/src/dream/consolidation.ts`). + */ +import type { MemoryEntry } from './memory-entries'; +import { + parseMemoryMd, + serializeMemoryMd, + deduplicateEntries, + resolveContradictions, + validateMemoryEntry, +} from './memory-entries'; +import { + truncateEntrypoint, + countLines, + countBytes, + MAX_ENTRYPOINT_LINES, + MAX_ENTRYPOINT_BYTES, +} from './memory-limits'; +import { convertRelativeDates } from './date-utils'; +import { sortByPriority, trimToFit } from './consolidation-priorities'; + +export interface ConsolidationInput { + readonly currentMemoryMd: string; + readonly newEntries: ReadonlyArray; + readonly referenceDate?: Date; +} + +export interface ConsolidationPhaseLog { + readonly orient: { existingEntries: number }; + readonly gather: { newEntries: number }; + readonly consolidate: { merged: number; deduped: number; contradictions: number }; + readonly prune: { + beforeLines: number; + afterLines: number; + beforeBytes: number; + afterBytes: number; + }; +} + +export interface ConsolidationResult { + readonly memoryMd: string; + readonly entriesProcessed: number; + readonly entriesKept: number; + readonly duplicatesRemoved: number; + readonly contradictionsResolved: number; + readonly truncated: boolean; + readonly phases: ConsolidationPhaseLog; +} + +export function orient( + currentMemoryMd: string, + referenceDate: Date = new Date(), +): ReadonlyArray { + const entries = parseMemoryMd(currentMemoryMd); + return entries.map((e) => ({ + ...e, + content: convertRelativeDates(e.content, referenceDate), + description: convertRelativeDates(e.description, referenceDate), + })); +} + +export function gather( + newEntries: ReadonlyArray, + referenceDate: Date = new Date(), +): ReadonlyArray { + return newEntries + .filter((e) => validateMemoryEntry(e)) + .map((e) => ({ + ...e, + content: convertRelativeDates(e.content, referenceDate), + description: convertRelativeDates(e.description, referenceDate), + })); +} + +export interface ConsolidateResult { + readonly entries: ReadonlyArray; + readonly deduped: number; + readonly contradictions: number; +} + +export function consolidate( + existing: ReadonlyArray, + gathered: ReadonlyArray, +): ConsolidateResult { + const merged = [...existing, ...gathered]; + const mergedCount = merged.length; + + const afterContradictions = resolveContradictions(merged); + const contradictions = mergedCount - afterContradictions.length; + + const afterDedup = deduplicateEntries(afterContradictions); + const deduped = afterContradictions.length - afterDedup.length; + + const sorted = sortByPriority(afterDedup); + + return { entries: sorted, deduped, contradictions }; +} + +export interface PruneResult { + readonly entries: ReadonlyArray; + readonly truncated: boolean; + readonly linesRemoved: number; +} + +export function prune(entries: ReadonlyArray): PruneResult { + if (entries.length === 0) { + return { entries: [], truncated: false, linesRemoved: 0 }; + } + + const beforeSerialized = serializeMemoryMd(entries); + const beforeLines = countLines(beforeSerialized); + + const trimmed = trimToFit(entries, MAX_ENTRYPOINT_LINES, MAX_ENTRYPOINT_BYTES); + + if (trimmed.length === 0 && entries.length > 0) { + const truncResult = truncateEntrypoint(beforeSerialized); + const reparsed = parseMemoryMd(truncResult.content); + const afterLines = countLines(serializeMemoryMd(reparsed)); + return { + entries: reparsed, + truncated: true, + linesRemoved: beforeLines - afterLines, + }; + } + + const serialized = serializeMemoryMd(trimmed); + const truncResult = truncateEntrypoint(serialized); + + if (truncResult.truncated) { + const reparsed = parseMemoryMd(truncResult.content); + const afterLines = countLines(serializeMemoryMd(reparsed)); + return { + entries: reparsed, + truncated: true, + linesRemoved: beforeLines - afterLines, + }; + } + + const afterLines = countLines(serialized); + return { + entries: trimmed, + truncated: false, + linesRemoved: beforeLines - afterLines, + }; +} + +export function runConsolidation(input: ConsolidationInput): ConsolidationResult { + const refDate = input.referenceDate ?? new Date(); + + const oriented = orient(input.currentMemoryMd, refDate); + const gathered = gather(input.newEntries, refDate); + const consolidated = consolidate(oriented, gathered); + + const beforePrune = serializeMemoryMd(consolidated.entries); + const beforePruneLines = countLines(beforePrune); + const beforePruneBytes = countBytes(beforePrune); + const pruned = prune(consolidated.entries); + const finalMd = serializeMemoryMd(pruned.entries); + + let resultMd = finalMd; + if (pruned.truncated) { + const truncResult = truncateEntrypoint(finalMd); + resultMd = truncResult.truncated ? truncResult.content : finalMd; + } + + const afterMd = resultMd; + const afterLines = countLines(afterMd); + const afterBytes = countBytes(afterMd); + + return { + memoryMd: afterMd, + entriesProcessed: oriented.length + gathered.length, + entriesKept: pruned.entries.length, + duplicatesRemoved: consolidated.deduped, + contradictionsResolved: consolidated.contradictions, + truncated: pruned.truncated, + phases: { + orient: { existingEntries: oriented.length }, + gather: { newEntries: gathered.length }, + consolidate: { + merged: oriented.length + gathered.length, + deduped: consolidated.deduped, + contradictions: consolidated.contradictions, + }, + prune: { + beforeLines: beforePruneLines, + afterLines, + beforeBytes: beforePruneBytes, + afterBytes, + }, + }, + }; +} diff --git a/packages/services/src/mindMemory/date-utils.test.ts b/packages/services/src/mindMemory/date-utils.test.ts new file mode 100644 index 00000000..8fe532c8 --- /dev/null +++ b/packages/services/src/mindMemory/date-utils.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest'; + +import { convertRelativeDates, parseRelativeDate } from './date-utils'; + +const REF = new Date('2026-05-12T00:00:00Z'); + +describe('convertRelativeDates', () => { + it('returns the content untouched when no relative phrases are present', () => { + expect(convertRelativeDates('nothing relative here', REF)).toBe('nothing relative here'); + }); + + it('converts "yesterday" to ref - 1 day', () => { + expect(convertRelativeDates('met yesterday with team', REF)).toBe( + 'met 2026-05-11 with team', + ); + }); + + it('converts "today" and "this morning" to ref date', () => { + expect(convertRelativeDates('shipped today', REF)).toBe('shipped 2026-05-12'); + expect(convertRelativeDates('this morning the build broke', REF)).toBe( + '2026-05-12 the build broke', + ); + }); + + it('converts "last week" to ref - 7 days', () => { + expect(convertRelativeDates('decided last week', REF)).toBe('decided 2026-05-05'); + }); + + it('converts "last month" to ref - 30 days', () => { + expect(convertRelativeDates('happened last month', REF)).toBe('happened 2026-04-12'); + }); + + it('converts "a few days ago" to ref - 3 days', () => { + expect(convertRelativeDates('a few days ago we shipped', REF)).toBe( + '2026-05-09 we shipped', + ); + }); + + it('converts " days ago" with the parsed integer offset', () => { + expect(convertRelativeDates('5 days ago', REF)).toBe('2026-05-07'); + expect(convertRelativeDates('1 day ago', REF)).toBe('2026-05-11'); + }); + + it('is case-insensitive for keyword forms', () => { + expect(convertRelativeDates('YESTERDAY', REF)).toBe('2026-05-11'); + expect(convertRelativeDates('Last Week', REF)).toBe('2026-05-05'); + }); + + it('replaces multiple occurrences in one pass', () => { + expect(convertRelativeDates('today and yesterday', REF)).toBe('2026-05-12 and 2026-05-11'); + }); + + it('only matches whole-word boundaries (does not corrupt "yesterdays")', () => { + expect(convertRelativeDates('yesterdays plans', REF)).toBe('yesterdays plans'); + }); +}); + +describe('convertRelativeDates fix for "a few days ago"', () => { + // Documented: 3 days before 2026-05-12 == 2026-05-09. The earlier example used + // 2026-04-12 by mistake. Re-verify with explicit small math here. + it('"a few days ago" resolves to ref - 3 days exactly', () => { + const ref = new Date('2026-05-12T00:00:00Z'); + expect(convertRelativeDates('a few days ago', ref)).toBe('2026-05-09'); + }); +}); + +describe('parseRelativeDate', () => { + it('returns the resolved Date for a single matching phrase', () => { + const out = parseRelativeDate('yesterday', REF); + expect(out).not.toBeNull(); + expect(out?.toISOString().slice(0, 10)).toBe('2026-05-11'); + }); + + it('returns the resolved Date for " days ago"', () => { + const out = parseRelativeDate('14 days ago', REF); + expect(out?.toISOString().slice(0, 10)).toBe('2026-04-28'); + }); + + it('returns null when the input is not a recognised relative phrase', () => { + expect(parseRelativeDate('next thursday', REF)).toBeNull(); + expect(parseRelativeDate('', REF)).toBeNull(); + expect(parseRelativeDate('something else', REF)).toBeNull(); + }); + + it('returns null when the input contains the phrase but extra text', () => { + expect(parseRelativeDate('yesterday at noon', REF)).toBeNull(); + }); + + it('trims whitespace before matching', () => { + expect(parseRelativeDate(' today ', REF)?.toISOString().slice(0, 10)).toBe('2026-05-12'); + }); +}); diff --git a/packages/services/src/mindMemory/date-utils.ts b/packages/services/src/mindMemory/date-utils.ts new file mode 100644 index 00000000..477908f2 --- /dev/null +++ b/packages/services/src/mindMemory/date-utils.ts @@ -0,0 +1,61 @@ +/** + * Date conversion utilities — convert relative date references to absolute ISO dates. + * + * Pure module: no I/O, no logging. + */ + +interface DatePattern { + readonly regex: RegExp; + readonly resolve: (match: RegExpMatchArray, ref: Date) => Date; +} + +function subtractDays(date: Date, days: number): Date { + const d = new Date(date); + d.setUTCDate(d.getUTCDate() - days); + return d; +} + +function formatISO(date: Date): string { + return date.toISOString().slice(0, 10); +} + +const PATTERNS: readonly DatePattern[] = [ + { regex: /\byesterday\b/gi, resolve: (_m, ref) => subtractDays(ref, 1) }, + { regex: /\btoday\b/gi, resolve: (_m, ref) => subtractDays(ref, 0) }, + { regex: /\bthis morning\b/gi, resolve: (_m, ref) => subtractDays(ref, 0) }, + { regex: /\blast week\b/gi, resolve: (_m, ref) => subtractDays(ref, 7) }, + { regex: /\blast month\b/gi, resolve: (_m, ref) => subtractDays(ref, 30) }, + { regex: /\ba few days ago\b/gi, resolve: (_m, ref) => subtractDays(ref, 3) }, + { + regex: /\b(\d+)\s+days?\s+ago\b/gi, + resolve: (m, ref) => subtractDays(ref, parseInt(m[1]!, 10)), + }, +]; + +export function convertRelativeDates(content: string, referenceDate: Date = new Date()): string { + let result = content; + + for (const pattern of PATTERNS) { + result = result.replace(pattern.regex, (...args) => { + const match = args as unknown as RegExpMatchArray; + const date = pattern.resolve(match, referenceDate); + return formatISO(date); + }); + } + + return result; +} + +export function parseRelativeDate(text: string, referenceDate: Date): Date | null { + const trimmed = text.trim(); + + for (const pattern of PATTERNS) { + const rx = new RegExp(pattern.regex.source, pattern.regex.flags); + const match = rx.exec(trimmed); + if (match && match[0].length === trimmed.length) { + return pattern.resolve(match, referenceDate); + } + } + + return null; +} diff --git a/packages/services/src/mindMemory/dream-gates.test.ts b/packages/services/src/mindMemory/dream-gates.test.ts new file mode 100644 index 00000000..12674b0b --- /dev/null +++ b/packages/services/src/mindMemory/dream-gates.test.ts @@ -0,0 +1,170 @@ +/** + * Tests for dream-gates — combined activity/time/lock gate evaluation. + * + * Phase 7 acceptance: + * - All three gates must pass for run=true. + * - Each failure produces a distinct, machine-readable reason. + * - Boundary conditions: exactly threshold, exact equality on time gate. + */ + +import { describe, expect, it } from 'vitest'; + +import { + DEFAULT_DAILY_GATE, + evaluateGates, + type GateConfig, + type GateInput, +} from './dream-gates'; +import type { DreamState } from './dream-state'; + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +function baseState(overrides: Partial = {}): DreamState { + return { + turnsSinceLastRun: 5, + lastDailyAt: null, + lastWeeklyAt: null, + lastMonthlyAt: null, + lastConsolidatedTurnId: null, + ...overrides, + }; +} + +function input(overrides: Partial = {}): GateInput { + return { + phase: 'daily', + state: baseState(), + now: 10 * MS_PER_DAY, + lockHeld: false, + ...overrides, + }; +} + +describe('dream-gates — lock gate', () => { + it('returns run=false reason=locked when the lock is held, regardless of activity/time', () => { + const r = evaluateGates( + input({ + lockHeld: true, + state: baseState({ turnsSinceLastRun: 1000, lastDailyAt: 0 }), + }), + DEFAULT_DAILY_GATE, + ); + expect(r.run).toBe(false); + expect(r.reason).toBe('locked'); + }); +}); + +describe('dream-gates — activity gate', () => { + it('passes when turnsSinceLastRun >= minTurnsBetweenRuns', () => { + const cfg: GateConfig = { minTurnsBetweenRuns: 5, minIntervalMs: 0 }; + const r = evaluateGates(input({ state: baseState({ turnsSinceLastRun: 5 }) }), cfg); + expect(r.run).toBe(true); + expect(r.reason).toBe('ready'); + }); + + it('fails when turnsSinceLastRun < minTurnsBetweenRuns', () => { + const cfg: GateConfig = { minTurnsBetweenRuns: 5, minIntervalMs: 0 }; + const r = evaluateGates(input({ state: baseState({ turnsSinceLastRun: 4 }) }), cfg); + expect(r.run).toBe(false); + expect(r.reason).toBe('no-activity'); + }); + + it('fails on zero turns even when threshold is 1', () => { + const cfg: GateConfig = { minTurnsBetweenRuns: 1, minIntervalMs: 0 }; + const r = evaluateGates(input({ state: baseState({ turnsSinceLastRun: 0 }) }), cfg); + expect(r.run).toBe(false); + expect(r.reason).toBe('no-activity'); + }); +}); + +describe('dream-gates — time gate', () => { + it('passes when no prior daily run has been recorded', () => { + const r = evaluateGates( + input({ state: baseState({ turnsSinceLastRun: 1, lastDailyAt: null }), now: 1 }), + DEFAULT_DAILY_GATE, + ); + expect(r.run).toBe(true); + }); + + it('fails when (now - lastDailyAt) < minIntervalMs', () => { + const r = evaluateGates( + input({ + state: baseState({ turnsSinceLastRun: 10, lastDailyAt: 5 * MS_PER_DAY }), + now: 5 * MS_PER_DAY + (MS_PER_DAY - 1), + }), + DEFAULT_DAILY_GATE, + ); + expect(r.run).toBe(false); + expect(r.reason).toBe('too-soon'); + }); + + it('passes at exactly the interval boundary', () => { + const r = evaluateGates( + input({ + state: baseState({ turnsSinceLastRun: 10, lastDailyAt: 5 * MS_PER_DAY }), + now: 5 * MS_PER_DAY + MS_PER_DAY, + }), + DEFAULT_DAILY_GATE, + ); + expect(r.run).toBe(true); + }); +}); + +describe('dream-gates — phase selection', () => { + it('weekly phase reads lastWeeklyAt, ignores lastDailyAt', () => { + const cfg: GateConfig = { minTurnsBetweenRuns: 1, minIntervalMs: MS_PER_DAY }; + const r = evaluateGates( + input({ + phase: 'weekly', + state: baseState({ + turnsSinceLastRun: 1, + lastDailyAt: 999 * MS_PER_DAY, // would block daily + lastWeeklyAt: null, + }), + now: 10 * MS_PER_DAY, + }), + cfg, + ); + expect(r.run).toBe(true); + }); + + it('monthly phase reads lastMonthlyAt', () => { + const cfg: GateConfig = { minTurnsBetweenRuns: 1, minIntervalMs: MS_PER_DAY }; + const r = evaluateGates( + input({ + phase: 'monthly', + state: baseState({ + turnsSinceLastRun: 1, + lastMonthlyAt: 10 * MS_PER_DAY - 1, + }), + now: 10 * MS_PER_DAY, + }), + cfg, + ); + expect(r.run).toBe(false); + expect(r.reason).toBe('too-soon'); + }); +}); + +describe('dream-gates — combination matrix', () => { + const cfg: GateConfig = { minTurnsBetweenRuns: 5, minIntervalMs: MS_PER_DAY }; + + it.each([ + ['activity-fail, time-fail, unlocked', { turns: 1, lastAt: 9.5 * MS_PER_DAY, lockHeld: false }, false, 'no-activity'], + ['activity-pass, time-fail, unlocked', { turns: 5, lastAt: 9.5 * MS_PER_DAY, lockHeld: false }, false, 'too-soon'], + ['activity-fail, time-pass, unlocked', { turns: 1, lastAt: 0, lockHeld: false }, false, 'no-activity'], + ['activity-pass, time-pass, locked', { turns: 5, lastAt: 0, lockHeld: true }, false, 'locked'], + ['activity-pass, time-pass, unlocked', { turns: 5, lastAt: 0, lockHeld: false }, true, 'ready'], + ])('%s → run=%s reason=%s', (_label, args, expectedRun, expectedReason) => { + const r = evaluateGates( + input({ + state: baseState({ turnsSinceLastRun: args.turns, lastDailyAt: args.lastAt }), + now: 10 * MS_PER_DAY, + lockHeld: args.lockHeld, + }), + cfg, + ); + expect(r.run).toBe(expectedRun); + expect(r.reason).toBe(expectedReason); + }); +}); diff --git a/packages/services/src/mindMemory/dream-gates.ts b/packages/services/src/mindMemory/dream-gates.ts new file mode 100644 index 00000000..38b7c9a6 --- /dev/null +++ b/packages/services/src/mindMemory/dream-gates.ts @@ -0,0 +1,87 @@ +/** + * dream-gates — combined lock + activity + time gate evaluation for the + * Dream Daemon. Replaces SCNS's "session count" gate with an *activity* + * gate driven by `turns_since_last_run`. + * + * The activity counter is bumped by DailyLogWriter when it appends a turn + * frame, NOT by SDK session start. This keeps the daemon from running + * after idle reconnects. + * + * `evaluateGates` is pure: it takes a snapshot of state + clock + lock + * status and returns `{ run, reason }`. The caller is responsible for + * checking the consolidation-enabled flag from `.chamber.json` before + * invoking this function. + * + * Gate order (first failure short-circuits): + * 1. Lock gate reason='locked' + * 2. Activity gate reason='no-activity' + * 3. Time gate reason='too-soon' + * else reason='ready' + */ + +import type { DreamPhase } from './dream-schema'; +import type { DreamState } from './dream-state'; + +export interface GateConfig { + readonly minTurnsBetweenRuns: number; + readonly minIntervalMs: number; +} + +export interface GateInput { + readonly phase: DreamPhase; + readonly state: DreamState; + readonly now: number; + readonly lockHeld: boolean; +} + +export type GateReason = 'locked' | 'no-activity' | 'too-soon' | 'ready'; + +export interface GateResult { + readonly run: boolean; + readonly reason: GateReason; +} + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +export const DEFAULT_DAILY_GATE: GateConfig = { + minTurnsBetweenRuns: 1, + minIntervalMs: MS_PER_DAY, +}; + +export const DEFAULT_WEEKLY_GATE: GateConfig = { + minTurnsBetweenRuns: 1, + minIntervalMs: 7 * MS_PER_DAY, +}; + +export const DEFAULT_MONTHLY_GATE: GateConfig = { + minTurnsBetweenRuns: 1, + minIntervalMs: 30 * MS_PER_DAY, +}; + +function lastPhaseAt(state: DreamState, phase: DreamPhase): number | null { + switch (phase) { + case 'daily': + return state.lastDailyAt; + case 'weekly': + return state.lastWeeklyAt; + case 'monthly': + return state.lastMonthlyAt; + } +} + +export function evaluateGates(input: GateInput, config: GateConfig): GateResult { + if (input.lockHeld) { + return { run: false, reason: 'locked' }; + } + + if (input.state.turnsSinceLastRun < config.minTurnsBetweenRuns) { + return { run: false, reason: 'no-activity' }; + } + + const last = lastPhaseAt(input.state, input.phase); + if (last !== null && input.now - last < config.minIntervalMs) { + return { run: false, reason: 'too-soon' }; + } + + return { run: true, reason: 'ready' }; +} diff --git a/packages/services/src/mindMemory/dream-schema.test.ts b/packages/services/src/mindMemory/dream-schema.test.ts new file mode 100644 index 00000000..cda9566a --- /dev/null +++ b/packages/services/src/mindMemory/dream-schema.test.ts @@ -0,0 +1,166 @@ +/** + * Tests for dream-schema — per-mind better-sqlite3 schema bootstrap. + * + * Phase 7 acceptance: + * - migrate() is idempotent. + * - openDreamDb() materializes the parent .state directory. + * - WAL pragma is applied on file-backed DBs. + * - dream_state singleton row exists after migrate. + * - dream_state has the new last_consolidated_turn_id TEXT NULL column. + * - dream_locks and dream_runs tables exist with the expected columns. + */ + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import Database from 'better-sqlite3'; + +import { dreamDbPath, migrate, openDreamDb } from './dream-schema'; + +let mindRoot: string; + +beforeEach(() => { + mindRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-dream-schema-')); +}); + +afterEach(() => { + fs.rmSync(mindRoot, { recursive: true, force: true }); +}); + +function tableInfo(db: Database.Database, table: string): Array<{ name: string; type: string; notnull: number; dflt_value: unknown; pk: number }> { + return db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ + name: string; + type: string; + notnull: number; + dflt_value: unknown; + pk: number; + }>; +} + +describe('dream-schema — dreamDbPath', () => { + it('puts the database under /.working-memory/.state/dream.db', () => { + const p = dreamDbPath('/tmp/some/mind'); + expect(p.replace(/\\/g, '/')).toBe('/tmp/some/mind/.working-memory/.state/dream.db'); + }); +}); + +describe('dream-schema — openDreamDb', () => { + it('creates the parent .state directory and opens a WAL-mode database', () => { + const dbPath = dreamDbPath(mindRoot); + const db = openDreamDb(dbPath); + try { + expect(fs.existsSync(path.dirname(dbPath))).toBe(true); + expect(fs.existsSync(dbPath)).toBe(true); + const mode = db.pragma('journal_mode', { simple: true }); + expect(mode).toBe('wal'); + } finally { + db.close(); + } + }); + + it('seeds the dream_state singleton row on first open', () => { + const dbPath = dreamDbPath(mindRoot); + const db = openDreamDb(dbPath); + try { + const row = db + .prepare('SELECT id, turns_since_last_run, last_consolidated_turn_id FROM dream_state') + .all() as Array<{ id: number; turns_since_last_run: number; last_consolidated_turn_id: string | null }>; + expect(row).toHaveLength(1); + expect(row[0].id).toBe(1); + expect(row[0].turns_since_last_run).toBe(0); + expect(row[0].last_consolidated_turn_id).toBeNull(); + } finally { + db.close(); + } + }); +}); + +describe('dream-schema — migrate idempotency', () => { + it('running migrate twice is a no-op (no error, no duplicate seed row)', () => { + const db = new Database(':memory:'); + try { + migrate(db); + migrate(db); + const rows = db.prepare('SELECT id FROM dream_state').all() as Array<{ id: number }>; + expect(rows).toHaveLength(1); + expect(rows[0].id).toBe(1); + } finally { + db.close(); + } + }); + + it('preserves user-written state across re-migration', () => { + const db = new Database(':memory:'); + try { + migrate(db); + db.prepare('UPDATE dream_state SET turns_since_last_run = 42, last_consolidated_turn_id = ? WHERE id = 1').run( + 'turn-xyz', + ); + migrate(db); + const row = db + .prepare('SELECT turns_since_last_run, last_consolidated_turn_id FROM dream_state WHERE id = 1') + .get() as { turns_since_last_run: number; last_consolidated_turn_id: string }; + expect(row.turns_since_last_run).toBe(42); + expect(row.last_consolidated_turn_id).toBe('turn-xyz'); + } finally { + db.close(); + } + }); +}); + +describe('dream-schema — table shapes', () => { + it('dream_state has the required columns including last_consolidated_turn_id TEXT NULL', () => { + const db = new Database(':memory:'); + try { + migrate(db); + const cols = tableInfo(db, 'dream_state'); + const byName = new Map(cols.map((c) => [c.name, c])); + expect(byName.has('id')).toBe(true); + expect(byName.has('turns_since_last_run')).toBe(true); + expect(byName.has('last_daily_at')).toBe(true); + expect(byName.has('last_weekly_at')).toBe(true); + expect(byName.has('last_monthly_at')).toBe(true); + expect(byName.has('last_consolidated_turn_id')).toBe(true); + const lcti = byName.get('last_consolidated_turn_id')!; + expect(lcti.type).toBe('TEXT'); + expect(lcti.notnull).toBe(0); + } finally { + db.close(); + } + }); + + it('dream_locks has phase/holder/acquired_at/expires_at', () => { + const db = new Database(':memory:'); + try { + migrate(db); + const cols = tableInfo(db, 'dream_locks').map((c) => c.name); + expect(cols).toEqual(expect.arrayContaining(['phase', 'holder', 'acquired_at', 'expires_at'])); + } finally { + db.close(); + } + }); + + it('dream_runs has phase/started_at/ended_at/status/reason/from_turn_id/to_turn_id', () => { + const db = new Database(':memory:'); + try { + migrate(db); + const cols = tableInfo(db, 'dream_runs').map((c) => c.name); + expect(cols).toEqual( + expect.arrayContaining([ + 'id', + 'phase', + 'started_at', + 'ended_at', + 'status', + 'reason', + 'from_turn_id', + 'to_turn_id', + ]), + ); + } finally { + db.close(); + } + }); +}); diff --git a/packages/services/src/mindMemory/dream-schema.ts b/packages/services/src/mindMemory/dream-schema.ts new file mode 100644 index 00000000..5a6b363a --- /dev/null +++ b/packages/services/src/mindMemory/dream-schema.ts @@ -0,0 +1,87 @@ +/** + * dream-schema — per-mind better-sqlite3 schema bootstrap for the Dream + * Daemon's local state, locks, and run history. + * + * Database location: `/.working-memory/.state/dream.db`. + * Journal mode: WAL (durable, multi-reader, single-writer). + * + * Tables (all created idempotently by `migrate`): + * + * dream_state Singleton (id=1) row tracking per-phase last-run + * timestamps, the activity counter + * (`turns_since_last_run`), and the cutoff turn id of + * the last successful daily consolidation + * (`last_consolidated_turn_id`). + * dream_locks One row per phase. Holder string, acquired_at, and + * expires_at form a TTL-bounded mutex broken via + * transactional steal in dream-state.acquireLock. + * dream_runs Append-only run history (success | failed | skipped) + * with optional reason and turn-id range processed. + * + * This module is I/O — it owns the dream.db file. dream-state.ts wraps it + * with typed CRUD; dream-gates.ts and consolidation-scheduler.ts compose + * on top. + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +import Database from 'better-sqlite3'; + +export type DreamPhase = 'daily' | 'weekly' | 'monthly'; +export type DreamRunStatus = 'success' | 'failed' | 'skipped'; + +const WORKING_MEMORY_DIRNAME = '.working-memory'; +const STATE_DIRNAME = '.state'; +const DREAM_DB_FILENAME = 'dream.db'; + +export function dreamDbPath(mindPath: string): string { + return path.join(mindPath, WORKING_MEMORY_DIRNAME, STATE_DIRNAME, DREAM_DB_FILENAME); +} + +export function openDreamDb(dbPath: string): Database.Database { + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + const db = new Database(dbPath); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + migrate(db); + return db; +} + +export function migrate(db: Database.Database): void { + db.exec(` + CREATE TABLE IF NOT EXISTS dream_state ( + id INTEGER PRIMARY KEY CHECK (id = 1), + turns_since_last_run INTEGER NOT NULL DEFAULT 0, + last_daily_at INTEGER, + last_weekly_at INTEGER, + last_monthly_at INTEGER, + last_consolidated_turn_id TEXT + ); + + CREATE TABLE IF NOT EXISTS dream_locks ( + phase TEXT PRIMARY KEY, + holder TEXT NOT NULL, + acquired_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS dream_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phase TEXT NOT NULL, + started_at INTEGER NOT NULL, + ended_at INTEGER, + status TEXT NOT NULL, + reason TEXT, + from_turn_id TEXT, + to_turn_id TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_dream_runs_phase_started + ON dream_runs (phase, started_at DESC); + `); + + // Seed the singleton row. INSERT OR IGNORE keeps migrate idempotent and + // preserves user-written state across re-opens. + db.prepare('INSERT OR IGNORE INTO dream_state (id) VALUES (1)').run(); +} diff --git a/packages/services/src/mindMemory/dream-state.test.ts b/packages/services/src/mindMemory/dream-state.test.ts new file mode 100644 index 00000000..be1e24f9 --- /dev/null +++ b/packages/services/src/mindMemory/dream-state.test.ts @@ -0,0 +1,249 @@ +/** + * Tests for dream-state — typed CRUD over the dream-schema tables, plus + * lock acquire/release with stale-lock break. + * + * Phase 7 acceptance: + * - readState round-trips through writes; defaults when row missing. + * - incrementTurnCount accumulates atomically. + * - markPhaseComplete updates only the targeted phase. + * - setLastConsolidatedTurnId round-trips through null. + * - recordRun appends to dream_runs and listRuns reads back. + * - acquireLock/releaseLock honor the lock holder format and stale-break. + */ + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import Database from 'better-sqlite3'; + +import { migrate } from './dream-schema'; +import { + acquireLock, + buildLockHolder, + getLock, + incrementTurnCount, + listRuns, + markPhaseComplete, + readState, + recordRun, + releaseLock, + resetActivityCounter, + setLastConsolidatedTurnId, +} from './dream-state'; + +let db: Database.Database; + +beforeEach(() => { + db = new Database(':memory:'); + migrate(db); +}); + +afterEach(() => { + db.close(); +}); + +describe('dream-state — readState defaults', () => { + it('returns the singleton defaults after migrate', () => { + const s = readState(db); + expect(s.turnsSinceLastRun).toBe(0); + expect(s.lastDailyAt).toBeNull(); + expect(s.lastWeeklyAt).toBeNull(); + expect(s.lastMonthlyAt).toBeNull(); + expect(s.lastConsolidatedTurnId).toBeNull(); + }); + + it('returns sensible defaults if the singleton row was deleted', () => { + db.prepare('DELETE FROM dream_state WHERE id = 1').run(); + const s = readState(db); + expect(s.turnsSinceLastRun).toBe(0); + expect(s.lastDailyAt).toBeNull(); + expect(s.lastConsolidatedTurnId).toBeNull(); + }); +}); + +describe('dream-state — incrementTurnCount', () => { + it('increments by 1 by default', () => { + incrementTurnCount(db); + incrementTurnCount(db); + expect(readState(db).turnsSinceLastRun).toBe(2); + }); + + it('increments by n', () => { + incrementTurnCount(db, 5); + expect(readState(db).turnsSinceLastRun).toBe(5); + }); + + it('rejects non-positive n', () => { + expect(() => incrementTurnCount(db, 0)).toThrow(); + expect(() => incrementTurnCount(db, -1)).toThrow(); + }); +}); + +describe('dream-state — resetActivityCounter', () => { + it('zeroes turns_since_last_run', () => { + incrementTurnCount(db, 7); + resetActivityCounter(db); + expect(readState(db).turnsSinceLastRun).toBe(0); + }); +}); + +describe('dream-state — markPhaseComplete', () => { + it('updates only the targeted phase timestamp', () => { + markPhaseComplete(db, 'daily', 1000); + let s = readState(db); + expect(s.lastDailyAt).toBe(1000); + expect(s.lastWeeklyAt).toBeNull(); + expect(s.lastMonthlyAt).toBeNull(); + + markPhaseComplete(db, 'weekly', 2000); + s = readState(db); + expect(s.lastDailyAt).toBe(1000); + expect(s.lastWeeklyAt).toBe(2000); + expect(s.lastMonthlyAt).toBeNull(); + + markPhaseComplete(db, 'monthly', 3000); + s = readState(db); + expect(s.lastMonthlyAt).toBe(3000); + }); +}); + +describe('dream-state — setLastConsolidatedTurnId', () => { + it('round-trips through a non-null id', () => { + setLastConsolidatedTurnId(db, 'turn-alpha'); + expect(readState(db).lastConsolidatedTurnId).toBe('turn-alpha'); + }); + + it('round-trips through null', () => { + setLastConsolidatedTurnId(db, 'turn-alpha'); + setLastConsolidatedTurnId(db, null); + expect(readState(db).lastConsolidatedTurnId).toBeNull(); + }); +}); + +describe('dream-state — recordRun + listRuns', () => { + it('appends in chronological order; listRuns returns most-recent-first', () => { + recordRun(db, { + phase: 'daily', + startedAt: 100, + endedAt: 200, + status: 'success', + fromTurnId: 'turn-1', + toTurnId: 'turn-3', + }); + recordRun(db, { + phase: 'daily', + startedAt: 300, + endedAt: null, + status: 'skipped', + reason: 'no-activity', + }); + + const runs = listRuns(db); + expect(runs).toHaveLength(2); + expect(runs[0].startedAt).toBe(300); + expect(runs[0].status).toBe('skipped'); + expect(runs[0].reason).toBe('no-activity'); + expect(runs[1].fromTurnId).toBe('turn-1'); + expect(runs[1].toTurnId).toBe('turn-3'); + }); + + it('listRuns honors phase filter and limit', () => { + recordRun(db, { phase: 'daily', startedAt: 1, endedAt: 2, status: 'success' }); + recordRun(db, { phase: 'weekly', startedAt: 3, endedAt: 4, status: 'success' }); + recordRun(db, { phase: 'daily', startedAt: 5, endedAt: 6, status: 'success' }); + + const daily = listRuns(db, { phase: 'daily' }); + expect(daily.map((r) => r.startedAt)).toEqual([5, 1]); + + const limited = listRuns(db, { limit: 1 }); + expect(limited).toHaveLength(1); + expect(limited[0].startedAt).toBe(5); + }); +}); + +describe('dream-state — buildLockHolder', () => { + it('produces the dream-daemon::: shape', () => { + const holder = buildLockHolder('mind-x', 1234, 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'); + expect(holder).toBe('dream-daemon:mind-x:1234:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'); + }); + + it('defaults pid to process.pid and uuid to a random uuid v4-ish string', () => { + const holder = buildLockHolder('mind-y'); + expect(holder.startsWith(`dream-daemon:mind-y:${process.pid}:`)).toBe(true); + const uuid = holder.split(':').pop()!; + expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); + }); +}); + +describe('dream-state — acquireLock / releaseLock', () => { + it('first acquire succeeds with reason=acquired', () => { + const r = acquireLock(db, { phase: 'daily', mindId: 'm', now: 1000, ttlMs: 5000 }); + expect(r.acquired).toBe(true); + expect(r.reason).toBe('acquired'); + expect(r.holder).toMatch(/^dream-daemon:m:/); + + const row = getLock(db, 'daily')!; + expect(row.phase).toBe('daily'); + expect(row.acquiredAt).toBe(1000); + expect(row.expiresAt).toBe(6000); + }); + + it('second acquire while still held returns reason=held with the existing holder', () => { + const a = acquireLock(db, { phase: 'daily', mindId: 'm', now: 1000, ttlMs: 5000 }); + const b = acquireLock(db, { phase: 'daily', mindId: 'm', now: 1500, ttlMs: 5000 }); + expect(a.acquired).toBe(true); + expect(b.acquired).toBe(false); + expect(b.reason).toBe('held'); + expect(b.holder).toBe(a.holder); + }); + + it('stale lock (expires_at < now) is stolen with reason=stolen-stale', () => { + const first = acquireLock(db, { phase: 'daily', mindId: 'm', now: 1000, ttlMs: 1000 }); + expect(first.acquired).toBe(true); + // jump past expiry + const second = acquireLock(db, { phase: 'daily', mindId: 'm', now: 5000, ttlMs: 1000 }); + expect(second.acquired).toBe(true); + expect(second.reason).toBe('stolen-stale'); + expect(second.holder).not.toBe(first.holder); + + const row = getLock(db, 'daily')!; + expect(row.holder).toBe(second.holder); + expect(row.acquiredAt).toBe(5000); + }); + + it('only one of two concurrent steal attempts succeeds (transactional re-check)', () => { + // Plant a stale lock + db.prepare( + 'INSERT INTO dream_locks (phase, holder, acquired_at, expires_at) VALUES (?, ?, ?, ?)', + ).run('daily', 'dream-daemon:m:1:old-uuid', 0, 1); + + // Two distinct callers try to steal at the same `now` + const a = acquireLock(db, { phase: 'daily', mindId: 'm', uuid: 'a', now: 1000, ttlMs: 1000 }); + const b = acquireLock(db, { phase: 'daily', mindId: 'm', uuid: 'b', now: 1000, ttlMs: 1000 }); + + // The first call performs the steal; the second sees a's lock and reports held. + expect(a.acquired).toBe(true); + expect(a.reason).toBe('stolen-stale'); + expect(b.acquired).toBe(false); + expect(b.reason).toBe('held'); + expect(b.holder).toBe(a.holder); + }); + + it('releaseLock only removes the row when the holder matches', () => { + const a = acquireLock(db, { phase: 'daily', mindId: 'm', now: 1000, ttlMs: 5000 }); + const released = releaseLock(db, 'daily', 'dream-daemon:m:0:wrong'); + expect(released).toBe(false); + expect(getLock(db, 'daily')).not.toBeNull(); + + const ok = releaseLock(db, 'daily', a.holder!); + expect(ok).toBe(true); + expect(getLock(db, 'daily')).toBeNull(); + }); + + it('after release, a fresh acquire succeeds with reason=acquired', () => { + const a = acquireLock(db, { phase: 'daily', mindId: 'm', now: 1000, ttlMs: 5000 }); + releaseLock(db, 'daily', a.holder!); + const c = acquireLock(db, { phase: 'daily', mindId: 'm', now: 2000, ttlMs: 5000 }); + expect(c.acquired).toBe(true); + expect(c.reason).toBe('acquired'); + }); +}); diff --git a/packages/services/src/mindMemory/dream-state.ts b/packages/services/src/mindMemory/dream-state.ts new file mode 100644 index 00000000..5bfa9d6b --- /dev/null +++ b/packages/services/src/mindMemory/dream-state.ts @@ -0,0 +1,249 @@ +/** + * dream-state — typed CRUD over the dream-schema tables, plus DB-backed + * lock acquire/release with stale-lock break. + * + * All writes are wrapped in `db.transaction(...)` so a partial failure + * cannot leave the singleton row half-updated. Lock steal is implemented + * inside a single transaction that re-checks `expires_at` so two + * simultaneous stealers cannot both succeed. + * + * Lock holder format: `dream-daemon:::` — the uuid + * component defeats same-process re-acquisition by a stale handle. The + * in-memory mutex layered on top lives in consolidation-scheduler.ts. + */ + +import { randomUUID } from 'node:crypto'; + +import type Database from 'better-sqlite3'; + +import type { DreamPhase, DreamRunStatus } from './dream-schema'; + +export interface DreamState { + readonly turnsSinceLastRun: number; + readonly lastDailyAt: number | null; + readonly lastWeeklyAt: number | null; + readonly lastMonthlyAt: number | null; + readonly lastConsolidatedTurnId: string | null; +} + +export interface RunRecord { + readonly phase: DreamPhase; + readonly startedAt: number; + readonly endedAt: number | null; + readonly status: DreamRunStatus; + readonly reason?: string | null; + readonly fromTurnId?: string | null; + readonly toTurnId?: string | null; +} + +export interface DreamLockRow { + readonly phase: DreamPhase; + readonly holder: string; + readonly acquiredAt: number; + readonly expiresAt: number; +} + +export interface AcquireLockArgs { + readonly phase: DreamPhase; + readonly mindId: string; + readonly pid?: number; + readonly uuid?: string; + readonly now: number; + readonly ttlMs: number; +} + +export type AcquireLockReason = 'acquired' | 'stolen-stale' | 'held'; + +export interface AcquireLockResult { + readonly acquired: boolean; + readonly holder: string | null; + readonly reason: AcquireLockReason; +} + +export interface ListRunsOptions { + readonly phase?: DreamPhase; + readonly limit?: number; +} + +const DEFAULT_STATE: DreamState = { + turnsSinceLastRun: 0, + lastDailyAt: null, + lastWeeklyAt: null, + lastMonthlyAt: null, + lastConsolidatedTurnId: null, +}; + +const PHASE_COLUMN: Record = { + daily: 'last_daily_at', + weekly: 'last_weekly_at', + monthly: 'last_monthly_at', +}; + +interface DreamStateRow { + turns_since_last_run: number; + last_daily_at: number | null; + last_weekly_at: number | null; + last_monthly_at: number | null; + last_consolidated_turn_id: string | null; +} + +export function readState(db: Database.Database): DreamState { + const row = db + .prepare( + `SELECT turns_since_last_run, last_daily_at, last_weekly_at, last_monthly_at, last_consolidated_turn_id + FROM dream_state WHERE id = 1`, + ) + .get() as DreamStateRow | undefined; + + if (!row) return DEFAULT_STATE; + + return { + turnsSinceLastRun: row.turns_since_last_run, + lastDailyAt: row.last_daily_at, + lastWeeklyAt: row.last_weekly_at, + lastMonthlyAt: row.last_monthly_at, + lastConsolidatedTurnId: row.last_consolidated_turn_id, + }; +} + +export function incrementTurnCount(db: Database.Database, n = 1): void { + if (!Number.isInteger(n) || n <= 0) { + throw new Error(`incrementTurnCount: n must be a positive integer (got ${n})`); + } + db.transaction(() => { + db.prepare('INSERT OR IGNORE INTO dream_state (id) VALUES (1)').run(); + db.prepare('UPDATE dream_state SET turns_since_last_run = turns_since_last_run + ? WHERE id = 1').run(n); + })(); +} + +export function resetActivityCounter(db: Database.Database): void { + db.transaction(() => { + db.prepare('INSERT OR IGNORE INTO dream_state (id) VALUES (1)').run(); + db.prepare('UPDATE dream_state SET turns_since_last_run = 0 WHERE id = 1').run(); + })(); +} + +export function markPhaseComplete(db: Database.Database, phase: DreamPhase, ts: number): void { + const col = PHASE_COLUMN[phase]; + db.transaction(() => { + db.prepare('INSERT OR IGNORE INTO dream_state (id) VALUES (1)').run(); + db.prepare(`UPDATE dream_state SET ${col} = ? WHERE id = 1`).run(ts); + })(); +} + +export function setLastConsolidatedTurnId(db: Database.Database, turnId: string | null): void { + db.transaction(() => { + db.prepare('INSERT OR IGNORE INTO dream_state (id) VALUES (1)').run(); + db.prepare('UPDATE dream_state SET last_consolidated_turn_id = ? WHERE id = 1').run(turnId); + })(); +} + +export function recordRun(db: Database.Database, record: RunRecord): number { + const info = db + .prepare( + `INSERT INTO dream_runs (phase, started_at, ended_at, status, reason, from_turn_id, to_turn_id) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + ) + .run( + record.phase, + record.startedAt, + record.endedAt, + record.status, + record.reason ?? null, + record.fromTurnId ?? null, + record.toTurnId ?? null, + ); + return Number(info.lastInsertRowid); +} + +interface DreamRunRow { + phase: DreamPhase; + started_at: number; + ended_at: number | null; + status: DreamRunStatus; + reason: string | null; + from_turn_id: string | null; + to_turn_id: string | null; +} + +export function listRuns(db: Database.Database, opts: ListRunsOptions = {}): RunRecord[] { + const where = opts.phase ? 'WHERE phase = ?' : ''; + const limitClause = opts.limit ? `LIMIT ${Math.max(1, Math.floor(opts.limit))}` : ''; + const sql = `SELECT phase, started_at, ended_at, status, reason, from_turn_id, to_turn_id + FROM dream_runs ${where} + ORDER BY started_at DESC, id DESC ${limitClause}`; + const stmt = db.prepare(sql); + const rows = (opts.phase ? stmt.all(opts.phase) : stmt.all()) as DreamRunRow[]; + return rows.map((r) => ({ + phase: r.phase, + startedAt: r.started_at, + endedAt: r.ended_at, + status: r.status, + reason: r.reason, + fromTurnId: r.from_turn_id, + toTurnId: r.to_turn_id, + })); +} + +export function buildLockHolder(mindId: string, pid: number = process.pid, uuid: string = randomUUID()): string { + return `dream-daemon:${mindId}:${pid}:${uuid}`; +} + +interface LockRowRaw { + phase: DreamPhase; + holder: string; + acquired_at: number; + expires_at: number; +} + +export function getLock(db: Database.Database, phase: DreamPhase): DreamLockRow | null { + const row = db + .prepare('SELECT phase, holder, acquired_at, expires_at FROM dream_locks WHERE phase = ?') + .get(phase) as LockRowRaw | undefined; + if (!row) return null; + return { + phase: row.phase, + holder: row.holder, + acquiredAt: row.acquired_at, + expiresAt: row.expires_at, + }; +} + +export function acquireLock(db: Database.Database, args: AcquireLockArgs): AcquireLockResult { + const holder = buildLockHolder(args.mindId, args.pid, args.uuid); + const expiresAt = args.now + args.ttlMs; + + // The whole acquire is a single transaction: SELECT-then-write under + // BEGIN IMMEDIATE serializes against any other writer attempting the + // same operation, so two would-be stealers cannot both win. + const txn = db.transaction((): AcquireLockResult => { + const existing = db + .prepare('SELECT phase, holder, acquired_at, expires_at FROM dream_locks WHERE phase = ?') + .get(args.phase) as LockRowRaw | undefined; + + if (!existing) { + db.prepare( + 'INSERT INTO dream_locks (phase, holder, acquired_at, expires_at) VALUES (?, ?, ?, ?)', + ).run(args.phase, holder, args.now, expiresAt); + return { acquired: true, holder, reason: 'acquired' }; + } + + if (existing.expires_at <= args.now) { + db.prepare( + 'UPDATE dream_locks SET holder = ?, acquired_at = ?, expires_at = ? WHERE phase = ?', + ).run(holder, args.now, expiresAt, args.phase); + return { acquired: true, holder, reason: 'stolen-stale' }; + } + + return { acquired: false, holder: existing.holder, reason: 'held' }; + }); + + return txn.immediate(); +} + +export function releaseLock(db: Database.Database, phase: DreamPhase, holder: string): boolean { + const info = db + .prepare('DELETE FROM dream_locks WHERE phase = ? AND holder = ?') + .run(phase, holder); + return info.changes > 0; +} diff --git a/packages/services/src/mindMemory/extraction.test.ts b/packages/services/src/mindMemory/extraction.test.ts new file mode 100644 index 00000000..f3018907 --- /dev/null +++ b/packages/services/src/mindMemory/extraction.test.ts @@ -0,0 +1,287 @@ +import { describe, it, expect } from 'vitest'; +import { + parseDailyLog, + classifyEntry, + extractFromLog, + extractFromMultipleLogs, + generateEntryName, + containsSensitive, +} from './extraction'; +import type { DailyLogEntry } from './extraction'; + +const singleSessionLog = `## 2026-04-05 + +### 14:30 — Session abc123 +- Working on SCNS project +- Decided to use Express 5 instead of Fastify +- User prefers TypeScript over JavaScript always +`; + +const multiSessionLog = `## 2026-04-05 + +### 14:30 — Session abc123 +- Working on SCNS project +- Decided to use Express 5 instead of Fastify +- User prefers TypeScript over JavaScript always + +### 16:45 — Session def456 +- Reviewed PR for real estate analysis +- PostgreSQL queries optimized with EXPLAIN ANALYZE +- User wants conventional commits enforced +`; + +const noSessionIdLog = `## 2026-04-05 + +### 09:00 +- Morning standup notes +- Remember to always run lint before commit +`; + +const realisticLog = `## 2026-04-05 + +### 14:30 — Session abc123 +- Working on SCNS project +- Decided to use Express 5 instead of Fastify +- User prefers TypeScript over JavaScript always +- Dashboard at https://grafana.example.com + +### 16:45 — Session def456 +- Reviewed PR for real estate analysis +- User dislikes tabs, prefers spaces +- Remember to always run lint before commit +- Docs at https://docs.example.com/api +`; + +describe('parseDailyLog', () => { + it('parses a single session entry with time and session ID', () => { + const entries = parseDailyLog(singleSessionLog, '2026-04-05'); + expect(entries).toHaveLength(1); + expect(entries[0]).toEqual({ + date: '2026-04-05', + time: '14:30', + sessionId: 'abc123', + lines: [ + 'Working on SCNS project', + 'Decided to use Express 5 instead of Fastify', + 'User prefers TypeScript over JavaScript always', + ], + }); + }); + + it('parses multiple session entries in one log', () => { + const entries = parseDailyLog(multiSessionLog, '2026-04-05'); + expect(entries).toHaveLength(2); + expect(entries[0]!.sessionId).toBe('abc123'); + expect(entries[1]!.sessionId).toBe('def456'); + }); + + it('parses entry without session ID (just time header)', () => { + const entries = parseDailyLog(noSessionIdLog, '2026-04-05'); + expect(entries).toHaveLength(1); + expect(entries[0]!.sessionId).toBeNull(); + expect(entries[0]!.time).toBe('09:00'); + }); + + it('returns empty array for empty content', () => { + expect(parseDailyLog('', '2026-04-05')).toEqual([]); + expect(parseDailyLog(' \n\n ', '2026-04-05')).toEqual([]); + }); + + it('strips bullet "- " prefix from lines', () => { + const entries = parseDailyLog(multiSessionLog, '2026-04-05'); + for (const line of entries[0]!.lines) { + expect(line).not.toMatch(/^- /); + } + }); +}); + +describe('classifyEntry', () => { + it('classifies "User prefers TypeScript" as user type', () => { + const result = classifyEntry('User prefers TypeScript'); + expect(result?.type).toBe('user'); + expect(result!.confidence).toBeGreaterThanOrEqual(0.6); + }); + + it('classifies "Decided to use Express 5" as project type', () => { + expect(classifyEntry('Decided to use Express 5')?.type).toBe('project'); + }); + + it('classifies "Remember to always run lint before commit" as feedback type', () => { + expect(classifyEntry('Remember to always run lint before commit')?.type).toBe('feedback'); + }); + + it('classifies "Dashboard at https://grafana.example.com" as reference type', () => { + expect(classifyEntry('Dashboard at https://grafana.example.com')?.type).toBe('reference'); + }); + + it('returns null for routine action "Fixed a bug in the parser"', () => { + expect(classifyEntry('Fixed a bug in the parser')).toBeNull(); + }); + + it('returns null for status update "Working on SCNS project"', () => { + expect(classifyEntry('Working on SCNS project')).toBeNull(); + }); +}); + +describe('extractFromLog', () => { + it('extracts entries from a realistic daily log', () => { + const entries = extractFromLog(realisticLog, '2026-04-05'); + expect(entries.length).toBeGreaterThan(0); + for (const entry of entries) { + expect(entry.type).toMatch(/^(user|feedback|project|reference|prohibition)$/); + expect(entry.name.length).toBeGreaterThan(0); + } + }); + + it('returns empty array for empty log', () => { + expect(extractFromLog('', '2026-04-05')).toEqual([]); + }); + + it('sets correct source on entries', () => { + const entries = extractFromLog(realisticLog, '2026-04-05'); + for (const entry of entries) { + expect(entry.source).toBe('daily-log:2026-04-05'); + } + }); + + it('sets correct createdAt on entries', () => { + const entries = extractFromLog(realisticLog, '2026-04-05'); + for (const entry of entries) { + expect(entry.createdAt).toMatch(/^2026-04-05T\d{2}:\d{2}:00Z$/); + } + }); +}); + +describe('extractFromMultipleLogs', () => { + it('combines entries from two logs sorted by date', () => { + const log1 = `## 2026-04-04\n\n### 10:00 — Session aaa\n- User prefers dark mode\n`; + const log2 = `## 2026-04-05\n\n### 14:00 — Session bbb\n- Decided to use PostgreSQL\n`; + const entries = extractFromMultipleLogs([ + { content: log1, date: '2026-04-04' }, + { content: log2, date: '2026-04-05' }, + ]); + expect(entries.length).toBe(2); + expect(entries[0]!.createdAt!.startsWith('2026-04-04')).toBe(true); + expect(entries[1]!.createdAt!.startsWith('2026-04-05')).toBe(true); + }); + + it('deduplicates across logs (same fact → keep latest)', () => { + const log1 = `## 2026-04-04\n\n### 10:00 — Session aaa\n- User prefers dark mode\n`; + const log2 = `## 2026-04-05\n\n### 14:00 — Session bbb\n- User prefers dark mode\n`; + const entries = extractFromMultipleLogs([ + { content: log1, date: '2026-04-04' }, + { content: log2, date: '2026-04-05' }, + ]); + expect(entries.length).toBe(1); + expect(entries[0]!.createdAt!.startsWith('2026-04-05')).toBe(true); + }); +}); + +describe('generateEntryName', () => { + it('truncates long content to ~60 chars', () => { + const name = generateEntryName( + 'This is a really long description that goes on and on and should be truncated to fit within sixty characters or so', + ); + expect(name.length).toBeLessThanOrEqual(60); + }); + + it('removes leading bullet markers', () => { + expect(generateEntryName('- User prefers TypeScript')).not.toMatch(/^-/); + }); + + it('title-cases the result', () => { + expect(generateEntryName('user prefers dark mode')).toBe('User Prefers Dark Mode'); + }); + + it('handles empty string', () => { + expect(generateEntryName('')).toBe(''); + }); +}); + +describe('containsSensitive — redaction guard', () => { + it('detects OpenAI-style sk- API keys', () => { + expect(containsSensitive('My API key is sk-abc123def456ghi789jkl012mno345pq')).toBe(true); + }); + + it('detects AWS access keys (AKIA...)', () => { + expect(containsSensitive('Use access key AKIAIOSFODNN7EXAMPLE for S3')).toBe(true); + }); + + it('detects GitHub-style ghp_ tokens', () => { + expect(containsSensitive('token: ghp_abcdefghijklmnopqrstuvwxyz0123456789')).toBe(true); + }); + + it('detects "password is X" patterns', () => { + expect(containsSensitive('the database password is hunter2')).toBe(true); + }); + + it('detects "secret = X" patterns', () => { + expect(containsSensitive('SECRET_TOKEN = abc123xyz789def456')).toBe(true); + }); + + it('does not flag innocent mentions of "key" or "token"', () => { + expect(containsSensitive('We use JWT tokens for auth')).toBe(false); + expect(containsSensitive('The primary key is the user ID')).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// Chamber-flavored fixture pack (5 cases — required by Phase 1 plan) +// 3 positives (memorized) + 2 negatives (transient + sensitive redaction) +// --------------------------------------------------------------------------- +describe('Chamber fixture pack — extractFromLog', () => { + const date = '2026-04-15'; + const log = `## ${date} + +### 10:00 — Session chamber-fixture +- I prefer concise commit messages +- We're using Postgres for the auth service +- Stop saying 'sure thing' +- Run npm test +- My API key is sk-abc123def456ghi789jkl012mno345pq +`; + + it('positive: "I prefer concise commit messages" → user entry', () => { + const entries = extractFromLog(log, date); + const match = entries.find((e) => e.content.toLowerCase().includes('concise commit')); + expect(match).toBeDefined(); + expect(match!.type).toBe('user'); + }); + + it('positive: "We\'re using Postgres for the auth service" → project entry', () => { + const entries = extractFromLog(log, date); + const match = entries.find((e) => e.content.toLowerCase().includes('postgres')); + expect(match).toBeDefined(); + expect(match!.type).toBe('project'); + }); + + it('positive: "Stop saying \'sure thing\'" → prohibition entry', () => { + const entries = extractFromLog(log, date); + const match = entries.find((e) => e.type === 'prohibition'); + expect(match).toBeDefined(); + }); + + it('negative: "Run npm test" → transient, NOT memorized', () => { + const entries = extractFromLog(log, date); + const match = entries.find((e) => e.content.toLowerCase().includes('run npm test')); + expect(match).toBeUndefined(); + }); + + it('negative: "My API key is sk-..." → sensitive, NOT memorized (redaction guard)', () => { + const entries = extractFromLog(log, date); + const leaked = entries.find( + (e) => + e.content.includes('sk-abc123def456ghi789jkl012mno345pq') || + e.description.includes('sk-abc123def456ghi789jkl012mno345pq'), + ); + expect(leaked).toBeUndefined(); + }); + + it('redaction guard also catches sensitive content even when classifier matches', () => { + // "I prefer using sk-..." would normally match the user 'prefer' pattern. + // The redaction guard must override and drop the entry. + const sneaky = `## ${date}\n\n### 11:00 — Session sneaky\n- I prefer using sk-abc123def456ghi789jkl012mno345pq for auth\n`; + const entries = extractFromLog(sneaky, date); + expect(entries.find((e) => e.content.includes('sk-'))).toBeUndefined(); + }); +}); diff --git a/packages/services/src/mindMemory/extraction.ts b/packages/services/src/mindMemory/extraction.ts new file mode 100644 index 00000000..454e9cf0 --- /dev/null +++ b/packages/services/src/mindMemory/extraction.ts @@ -0,0 +1,465 @@ +/** + * Daily log parsing and memory entry extraction. + * + * Pure functions — no file I/O. Content is passed as strings. + * + * Ported from SCNS (`scns/src/dream/extraction.ts`) with a Chamber-specific + * **sensitive-content redaction guard** added (see {@link containsSensitive}): + * before any extracted line is synthesized into a MemoryEntry, the raw text + * is screened for credential-shaped substrings (sk-… , AKIA… , ghp_… , + * "password is …", "secret = …"). Matches are dropped on the floor — they + * are NOT memorized, even if the classifier would otherwise accept them. + */ + +import { randomUUID } from 'node:crypto'; +import type { MemoryEntry } from './memory-entries'; +import { deduplicateEntries } from './memory-entries'; + +export interface DailyLogEntry { + readonly date: string; + readonly time: string; + readonly sessionId: string | null; + readonly lines: ReadonlyArray; +} + +interface Classification { + readonly type: MemoryEntry['type']; + readonly confidence: number; +} + +const STRONG_CONFIDENCE = 0.9; +const WEAK_CONFIDENCE = 0.6; + +type PatternDef = readonly [RegExp, number]; + +const USER_PATTERNS: readonly PatternDef[] = [ + [/\bprefers?\b/i, STRONG_CONFIDENCE], + [/\balways wants?\b/i, STRONG_CONFIDENCE], + [/\blikes?\b/i, WEAK_CONFIDENCE], + [/\bdislikes?\b/i, STRONG_CONFIDENCE], + [/\bstyle\b/i, WEAK_CONFIDENCE], + [/\bconvention\b/i, WEAK_CONFIDENCE], +]; + +const FEEDBACK_PATTERNS: readonly PatternDef[] = [ + [/\bshould\b/i, WEAK_CONFIDENCE], + [/\bdon['']t\b/i, STRONG_CONFIDENCE], + [/\bremember to\b/i, STRONG_CONFIDENCE], + [/\bbetter to\b/i, STRONG_CONFIDENCE], + [/\blesson learned\b/i, STRONG_CONFIDENCE], +]; + +const PROJECT_PATTERNS: readonly PatternDef[] = [ + [/\bdecided\b/i, STRONG_CONFIDENCE], + [/\busing\b/i, WEAK_CONFIDENCE], + [/\barchitecture\b/i, STRONG_CONFIDENCE], + [/\bdatabase\b/i, WEAK_CONFIDENCE], + [/\bdeployed\b/i, STRONG_CONFIDENCE], + [/\bconfigured\b/i, WEAK_CONFIDENCE], +]; + +const REFERENCE_PATTERNS: readonly PatternDef[] = [ + [/\bURL\b/i, STRONG_CONFIDENCE], + [/\blink\b/i, WEAK_CONFIDENCE], + [/\bdashboard at\b/i, STRONG_CONFIDENCE], + [/\bdocs at\b/i, STRONG_CONFIDENCE], + [/\bAPI at\b/i, STRONG_CONFIDENCE], + [/\blocated at\b/i, STRONG_CONFIDENCE], + [/https?:\/\//i, WEAK_CONFIDENCE], +]; + +const PROHIBITION_PATTERNS: readonly PatternDef[] = [ + [/\bnever (do|claim|say|assume|make|lie|skip|write|mark)\b/i, STRONG_CONFIDENCE], + [/\bstop (doing|making|saying|lying|claiming|ignoring|skipping)\b/i, STRONG_CONFIDENCE], + [/\bdo not (ever|again|assume|claim|skip)\b/i, STRONG_CONFIDENCE], + [/\bmust not\b/i, STRONG_CONFIDENCE], + [/\bavoid\b.*\b(always|never|must)\b/i, WEAK_CONFIDENCE], + [/\bnegative feedback\b/i, WEAK_CONFIDENCE], + [/\bprohibit/i, STRONG_CONFIDENCE], +]; + +// --------------------------------------------------------------------------- +// Sensitive-content redaction guard (Chamber addition) +// --------------------------------------------------------------------------- + +const SENSITIVE_PATTERNS: readonly RegExp[] = [ + /\bsk-[A-Za-z0-9_-]{16,}\b/, // OpenAI-style API key + /\bAKIA[0-9A-Z]{16}\b/, // AWS access key id + /\bghp_[A-Za-z0-9]{20,}\b/, // GitHub personal access token + /\bgho_[A-Za-z0-9]{20,}\b/, // GitHub OAuth token + /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, // Slack token + /\b(?:password|passwd|pwd)\s*(?:is|=|:)\s*\S{3,}/i, + /\b(?:secret|api[_-]?key|access[_-]?token|auth[_-]?token)\w*\s*(?:is|=|:)\s*\S{8,}/i, + /\bBearer\s+[A-Za-z0-9._-]{20,}/, + /-----BEGIN [A-Z ]*PRIVATE KEY-----/, +]; + +/** + * Returns true when the input text contains a credential-shaped substring. + * Used to drop entries that would otherwise be memorized. + */ +export function containsSensitive(text: string): boolean { + for (const re of SENSITIVE_PATTERNS) { + if (re.test(text)) return true; + } + return false; +} + +// --------------------------------------------------------------------------- +// parseDailyLog +// --------------------------------------------------------------------------- + +const OLD_HEADER_RE = /^###\s+(\d{1,2}:\d{2})(?:\s+[—–-]\s+Session\s+(\S+))?$/; +const PROD_HEADER_RE = /^##\s+(\d{1,2}:\d{2}:\d{2})\s*$/; +const TAGS_RE = /^Tags:\s*(.+)$/; +const CONTENT_RE = /^\*\*\[([^\]]+)\]\*\*\s*(.*)$/; + +const NOISE_SOURCES = new Set([ + 'pre-tool-use', + 'post-tool-use', + 'tool-use', + 'session-start', + 'session-end', +]); + +function parseTags(tagsLine: string): Map { + const tags = new Map(); + for (const part of tagsLine.split(',')) { + const trimmed = part.trim(); + const colonIdx = trimmed.indexOf(':'); + if (colonIdx === -1) continue; + const key = trimmed.slice(0, colonIdx).trim(); + const value = trimmed.slice(colonIdx + 1).trim(); + if (key && value) tags.set(key, value); + } + return tags; +} + +function stripContentWrapper(content: string): string { + const wrappers = [ + /^User prompt:\s*/i, + /^Tool used:\s*/i, + /^Pre-tool:\s*/i, + /^Post-tool:\s*/i, + /^Session\s+\S+\s+(started|ended)\s*/i, + ]; + for (const re of wrappers) { + const match = re.exec(content); + if (match) return content.slice(match[0].length).trim(); + } + return content; +} + +export function parseDailyLog(content: string, date: string): DailyLogEntry[] { + if (!content.trim()) return []; + + const results: DailyLogEntry[] = []; + let current: { time: string; sessionId: string | null; lines: string[] } | null = null; + let currentIsNoise = false; + + const rawLines = content.split('\n'); + + for (let i = 0; i < rawLines.length; i++) { + const line = rawLines[i]!.trimEnd(); + + const oldMatch = OLD_HEADER_RE.exec(line); + if (oldMatch) { + if (current && !currentIsNoise) { + results.push({ + date, + time: current.time, + sessionId: current.sessionId, + lines: current.lines, + }); + } + current = { time: oldMatch[1]!, sessionId: oldMatch[2] ?? null, lines: [] }; + currentIsNoise = false; + continue; + } + + const prodMatch = PROD_HEADER_RE.exec(line); + if (prodMatch) { + if (current && !currentIsNoise) { + results.push({ + date, + time: current.time, + sessionId: current.sessionId, + lines: current.lines, + }); + } + current = { time: prodMatch[1]!, sessionId: null, lines: [] }; + currentIsNoise = false; + continue; + } + + if (!current) continue; + + const tagsMatch = TAGS_RE.exec(line); + if (tagsMatch) { + const tags = parseTags(tagsMatch[1]!); + const sessionId = tags.get('session-id'); + if (sessionId && !current.sessionId) { + current.sessionId = sessionId; + } + const source = tags.get('source'); + if (source && NOISE_SOURCES.has(source)) { + currentIsNoise = true; + current.lines = []; + } + continue; + } + + const contentMatch = CONTENT_RE.exec(line); + if (contentMatch) { + const rawContent = contentMatch[2]!.trim(); + if (rawContent) { + const stripped = stripContentWrapper(rawContent); + if (stripped) current.lines.push(stripped); + } + continue; + } + + if (line.startsWith('- ')) { + current.lines.push(line.slice(2)); + continue; + } + + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('## ')) { + current.lines.push(trimmed); + } + } + + if (current && !currentIsNoise) { + results.push({ + date, + time: current.time, + sessionId: current.sessionId, + lines: current.lines, + }); + } + + return results.filter((e) => e.lines.some((l) => l.trim().length > 0)); +} + +// --------------------------------------------------------------------------- +// classifyEntry +// --------------------------------------------------------------------------- + +export function classifyEntry(line: string): Classification | null { + const prohibition = matchPatterns(line, PROHIBITION_PATTERNS); + if (prohibition) return { type: 'prohibition', confidence: prohibition }; + + const ref = matchPatterns(line, REFERENCE_PATTERNS); + if (ref) return { type: 'reference', confidence: ref }; + + const user = matchPatterns(line, USER_PATTERNS); + if (user) return { type: 'user', confidence: user }; + + const feedback = matchPatterns(line, FEEDBACK_PATTERNS); + if (feedback) return { type: 'feedback', confidence: feedback }; + + const project = matchPatterns(line, PROJECT_PATTERNS); + if (project) return { type: 'project', confidence: project }; + + return null; +} + +function matchPatterns(line: string, patterns: readonly PatternDef[]): number | null { + let best: number | null = null; + for (const [regex, confidence] of patterns) { + if (regex.test(line)) { + if (best === null || confidence > best) best = confidence; + } + } + return best; +} + +// --------------------------------------------------------------------------- +// generateEntryName +// --------------------------------------------------------------------------- + +const FILLER_WORDS = new Set(['the', 'a', 'an']); + +export function generateEntryName(content: string): string { + if (!content.trim()) return ''; + + let text = content.replace(/^[-*•]\s+/, '').trim(); + + const clauseMatch = /^([^,.\-—–]+)/.exec(text); + if (clauseMatch) text = clauseMatch[1]!.trim(); + + const words = text.split(/\s+/); + while (words.length > 1 && FILLER_WORDS.has(words[0]!.toLowerCase())) { + words.shift(); + } + + const titled = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); + + if (titled.length <= 60) return titled; + const truncated = titled.slice(0, 60); + const lastSpace = truncated.lastIndexOf(' '); + return lastSpace > 20 ? truncated.slice(0, lastSpace) : truncated; +} + +// --------------------------------------------------------------------------- +// synthesizeEntry +// --------------------------------------------------------------------------- + +const TYPO_MAP: ReadonlyArray = [ + [/\bii\b/gi, 'I'], + [/\bsi\b/gi, 'is'], + [/\bteh\b/gi, 'the'], + [/\bidk\b/gi, "I don't know"], +]; + +const SYNTHESIS_RULES: ReadonlyArray<{ + readonly match: RegExp; + readonly type: MemoryEntry['type']; + readonly transform: (text: string) => { name: string; description: string }; +}> = [ + { + match: /\bfollow TDD\b/i, + type: 'feedback', + transform: () => ({ + name: 'Follow TDD Workflow', + description: + 'Always follow Test-Driven Development: write tests first, implement to pass, then verify with both automated and manual testing before declaring work complete.', + }), + }, + { + match: /\bstop\s+(doing|making|ignoring|skipping|lying)\b/i, + type: 'prohibition', + transform: (text: string) => { + if (/test/i.test(text)) { + return { + name: 'Never Skip Required Testing', + description: 'Testing requirements are non-negotiable. Do not skip manual or automated testing steps.', + }; + } + return { + name: 'Follow All Quality Requirements', + description: 'Follow all stated quality requirements without shortcuts or omissions.', + }; + }, + }, +]; + +export function fixTypos(text: string): string { + let result = text; + for (const [pattern, replacement] of TYPO_MAP) { + result = result.replace(pattern, replacement); + } + return result; +} + +export function synthesizeEntry( + rawText: string, + type: MemoryEntry['type'], +): { name: string; description: string; content: string } | null { + // Sensitive-content redaction guard — drop entries that contain credentials. + if (containsSensitive(rawText)) return null; + + for (const rule of SYNTHESIS_RULES) { + if (rule.match.test(rawText)) { + const { name, description } = rule.transform(rawText); + return { name, description, content: description }; + } + } + + const cleaned = fixTypos(rawText); + + if (cleaned.length < 15) return null; + + if (type === 'prohibition' || type === 'feedback') { + const normalized = cleanProhibitionFeedback(cleaned); + if (normalized) { + return { + name: generateEntryName(normalized), + description: normalized, + content: normalized, + }; + } + } + + return { + name: generateEntryName(cleaned), + description: cleaned, + content: cleaned, + }; +} + +function cleanProhibitionFeedback(text: string): string | null { + let cleaned = text + .replace(/\?\s*$/g, '.') + .replace(/^why\s+would\s+you\s+/i, '') + .replace(/^what\s+gives?\s*/i, '') + .trim(); + + if (cleaned.length > 0) { + cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1); + } + + if (cleaned.length > 0 && !cleaned.endsWith('.')) cleaned += '.'; + + return cleaned.length > 10 ? cleaned : null; +} + +// --------------------------------------------------------------------------- +// extractFromLog +// --------------------------------------------------------------------------- + +export function extractFromLog(content: string, date: string): ReadonlyArray { + const parsed = parseDailyLog(content, date); + const entries: MemoryEntry[] = []; + + for (const session of parsed) { + for (const line of session.lines) { + // Redaction guard fires before classification — sensitive lines never + // get a chance to match a pattern. + if (containsSensitive(line)) continue; + + const classification = classifyEntry(line); + if (!classification) continue; + + const time = session.time.includes(':') + ? session.time.split(':').length === 3 + ? session.time + : `${session.time}:00` + : `${session.time}:00`; + + const synthesized = synthesizeEntry(line, classification.type); + if (!synthesized) continue; + + entries.push({ + id: randomUUID(), + type: classification.type, + name: synthesized.name, + description: synthesized.description, + content: synthesized.content, + source: `daily-log:${date}`, + createdAt: `${date}T${time}Z`, + }); + } + } + + return entries; +} + +// --------------------------------------------------------------------------- +// extractFromMultipleLogs +// --------------------------------------------------------------------------- + +export function extractFromMultipleLogs( + logs: ReadonlyArray<{ content: string; date: string }>, +): ReadonlyArray { + const allEntries: MemoryEntry[] = []; + + for (const log of logs) { + allEntries.push(...extractFromLog(log.content, log.date)); + } + + allEntries.sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? '')); + + return deduplicateEntries(allEntries); +} diff --git a/packages/services/src/mindMemory/index.test.ts b/packages/services/src/mindMemory/index.test.ts new file mode 100644 index 00000000..71fae331 --- /dev/null +++ b/packages/services/src/mindMemory/index.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; + +describe('mindMemory package scaffold', () => { + it('exposes a public surface module that loads without side effects', async () => { + const mod = await import('./index'); + expect(mod).toBeDefined(); + expect(mod).not.toBeNull(); + }); + + it('exposes a sentinel marker identifying the scaffold version', async () => { + const mod = await import('./index'); + expect(mod.MIND_MEMORY_PACKAGE_VERSION).toBe('0.0.0-scaffold'); + }); +}); + +describe('better-sqlite3 native binding (Phase 0 packaging gate)', () => { + it('opens an in-memory database and round-trips a value', async () => { + const { default: Database } = await import('better-sqlite3'); + const db = new Database(':memory:'); + try { + db.exec('CREATE TABLE t (k TEXT PRIMARY KEY, v TEXT)'); + db.prepare('INSERT INTO t (k, v) VALUES (?, ?)').run('hello', 'world'); + const row = db.prepare('SELECT v FROM t WHERE k = ?').get('hello') as { v: string }; + expect(row.v).toBe('world'); + } finally { + db.close(); + } + }); + + it('supports WAL journal mode (used by per-mind dream.db)', async () => { + const { default: Database } = await import('better-sqlite3'); + const db = new Database(':memory:'); + try { + const mode = db.pragma('journal_mode = WAL', { simple: true }); + // In-memory DBs report 'memory' for journal_mode; the call must not throw. + expect(typeof mode).toBe('string'); + } finally { + db.close(); + } + }); +}); diff --git a/packages/services/src/mindMemory/index.ts b/packages/services/src/mindMemory/index.ts new file mode 100644 index 00000000..6266b611 --- /dev/null +++ b/packages/services/src/mindMemory/index.ts @@ -0,0 +1,38 @@ +/** + * Public surface for the per-mind background memory consolidation engine + * (a.k.a. "Dream Daemon"). + * + * Phase 0 scaffold only — no exports yet. Subsequent phases will add: + * - pure modules: memory-limits, date-utils, consolidation-priorities, + * memory-entries, consolidation, extraction + * - I/O: MindMemoryVault, MindArchiveStore, StructuredLogFormat, + * DailyLogWriter + * - state: dream-schema, dream-state, dream-gates, scheduler + * - orchestrator: LLMClient, CopilotLLMClient, DreamDaemon, + * InternalScheduler, MindMemoryService + * + * See plan: feature/dream-daemon-memory-consolidation. + */ + +export const MIND_MEMORY_PACKAGE_VERSION = '0.0.0-scaffold'; + +export * from './memory-limits'; +export * from './date-utils'; +export * from './memory-entries'; +export * from './consolidation-priorities'; +export * from './consolidation'; +export * from './extraction'; +export * from './StructuredLogFormat'; +export * from './MindMemoryVault'; +export * from './MindArchiveStore'; +export * from './DailyLogWriter'; +export * from './dream-schema'; +export * from './dream-state'; +export * from './dream-gates'; +export * from './consolidation-scheduler'; +export * from './LLMClient'; +export * from './CopilotLLMClient'; +export * from './oneShotSession'; +export * from './DreamDaemon'; +export * from './InternalScheduler'; +export * from './MindMemoryService'; diff --git a/packages/services/src/mindMemory/memory-entries.test.ts b/packages/services/src/mindMemory/memory-entries.test.ts new file mode 100644 index 00000000..e4916621 --- /dev/null +++ b/packages/services/src/mindMemory/memory-entries.test.ts @@ -0,0 +1,259 @@ +import { describe, expect, it } from 'vitest'; + +import { + type MemoryEntry, + deduplicateEntries, + detectDuplicates, + parseMemoryMd, + resolveContradictions, + resynthesizeMemoryEntries, + serializeMemoryMd, + validateMemoryEntry, +} from './memory-entries'; + +describe('validateMemoryEntry', () => { + it('accepts a minimal valid entry', () => { + const entry: MemoryEntry = { + type: 'user', + name: 'Prefer concise commits', + description: 'User prefers concise commit messages', + content: 'concise commits', + }; + expect(validateMemoryEntry(entry)).toBe(true); + }); + + it('rejects entries with an unknown type', () => { + expect( + validateMemoryEntry({ + type: 'gibberish', + name: 'x', + description: 'y', + content: 'z', + }), + ).toBe(false); + }); + + it('rejects entries with empty required strings', () => { + expect( + validateMemoryEntry({ + type: 'user', + name: '', + description: 'x', + content: 'y', + }), + ).toBe(false); + expect( + validateMemoryEntry({ + type: 'user', + name: 'x', + description: '', + content: 'y', + }), + ).toBe(false); + }); + + it('rejects non-objects', () => { + expect(validateMemoryEntry(null)).toBe(false); + expect(validateMemoryEntry('string')).toBe(false); + expect(validateMemoryEntry(42)).toBe(false); + }); + + it('accepts every type from the enum', () => { + for (const type of ['user', 'feedback', 'project', 'reference', 'prohibition'] as const) { + expect( + validateMemoryEntry({ type, name: 'n', description: 'd', content: 'c' }), + ).toBe(true); + } + }); +}); + +describe('parseMemoryMd', () => { + it('returns [] for empty content', () => { + expect(parseMemoryMd('')).toEqual([]); + expect(parseMemoryMd(' \n ')).toEqual([]); + }); + + it('parses a single entry with frontmatter', () => { + const md = `---\nname: Prefer concise commits\ndescription: User prefers brevity\ntype: user\n---\nconcise commit messages`; + const result = parseMemoryMd(md); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: 'Prefer concise commits', + description: 'User prefers brevity', + type: 'user', + content: 'concise commit messages', + }); + }); + + it('parses multiple entries separated by ---', () => { + const md = [ + `---\nname: A\ndescription: dA\ntype: user\n---\ncontentA`, + `---\nname: B\ndescription: dB\ntype: project\n---\ncontentB`, + ].join('\n'); + const result = parseMemoryMd(md); + expect(result).toHaveLength(2); + expect(result[0]?.name).toBe('A'); + expect(result[1]?.name).toBe('B'); + }); + + it('preserves optional fields when present', () => { + const md = `---\nid: abc-123\nname: X\ndescription: dx\ntype: user\nsource: daily-log:2026-05-12\ncreatedAt: 2026-05-12T03:00:00Z\n---\nbody`; + const [entry] = parseMemoryMd(md); + expect(entry?.id).toBe('abc-123'); + expect(entry?.source).toBe('daily-log:2026-05-12'); + expect(entry?.createdAt).toBe('2026-05-12T03:00:00Z'); + }); + + it('skips blocks missing required fields', () => { + const md = `---\ntype: user\n---\nno name`; + expect(parseMemoryMd(md)).toEqual([]); + }); +}); + +describe('serializeMemoryMd', () => { + it('round-trips through parseMemoryMd', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'A', description: 'dA', content: 'cA' }, + { type: 'project', name: 'B', description: 'dB', content: 'cB' }, + ]; + const serialized = serializeMemoryMd(entries); + const reparsed = parseMemoryMd(serialized); + expect(reparsed).toHaveLength(2); + expect(reparsed[0]).toMatchObject(entries[0]!); + expect(reparsed[1]).toMatchObject(entries[1]!); + }); + + it('emits id, source, createdAt only when present', () => { + const md = serializeMemoryMd([ + { type: 'user', name: 'A', description: 'dA', content: 'cA' }, + ]); + expect(md).not.toContain('id:'); + expect(md).not.toContain('source:'); + expect(md).not.toContain('createdAt:'); + }); +}); + +describe('detectDuplicates', () => { + it('flags entries with identical names case-insensitively', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'Prefer Concise Commits', description: 'd', content: 'c' }, + { type: 'user', name: 'prefer concise commits', description: 'd', content: 'c' }, + ]; + expect(detectDuplicates(entries)).toEqual([[0, 1]]); + }); + + it('flags entries with very similar content', () => { + const long = 'we are using postgres for the auth service'; + const entries: MemoryEntry[] = [ + { type: 'project', name: 'A', description: 'd', content: long }, + { type: 'project', name: 'B', description: 'd', content: long }, + ]; + expect(detectDuplicates(entries)).toEqual([[0, 1]]); + }); + + it('returns no pairs when entries are distinct', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'A', description: 'dA', content: 'totally different' }, + { type: 'user', name: 'B', description: 'dB', content: 'completely unrelated' }, + ]; + expect(detectDuplicates(entries)).toEqual([]); + }); +}); + +describe('deduplicateEntries', () => { + it('keeps the later entry when names collide', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'A', description: 'old', content: 'old' }, + { type: 'user', name: 'a', description: 'new', content: 'new' }, + ]; + const out = deduplicateEntries(entries); + expect(out).toHaveLength(1); + expect(out[0]?.description).toBe('new'); + }); + + it('returns all entries when no duplicates exist', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'A', description: 'd', content: 'totally different content here' }, + { type: 'project', name: 'B', description: 'd', content: 'completely unrelated material' }, + ]; + expect(deduplicateEntries(entries)).toHaveLength(2); + }); +}); + +describe('resolveContradictions', () => { + it('keeps the later entry when names match exactly', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'X', description: 'old', content: 'old' }, + { type: 'user', name: 'X', description: 'new', content: 'new' }, + ]; + const out = resolveContradictions(entries); + expect(out).toHaveLength(1); + expect(out[0]?.description).toBe('new'); + }); + + it('keeps the later entry when same type and similar names', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'use postgres', description: 'old', content: 'c' }, + { type: 'user', name: 'use postgres!', description: 'new', content: 'c' }, + ]; + const out = resolveContradictions(entries); + expect(out).toHaveLength(1); + expect(out[0]?.description).toBe('new'); + }); + + it('does not collapse entries of different types even with similar names', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'use postgres', description: 'a', content: 'a' }, + { type: 'project', name: 'use postgres', description: 'b', content: 'b' }, + ]; + // Note: identical name (case-insensitive) STILL collapses regardless of type per SCNS rules. + const out = resolveContradictions(entries); + expect(out).toHaveLength(1); + expect(out[0]?.type).toBe('project'); + }); +}); + +describe('resynthesizeMemoryEntries', () => { + it('uses the synthesizer output when it returns a result', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'old', description: 'old', content: 'raw text' }, + ]; + const out = resynthesizeMemoryEntries( + entries, + () => ({ name: 'New', description: 'New desc', content: 'New content' }), + (t) => t, + ); + expect(out[0]).toMatchObject({ + name: 'New', + description: 'New desc', + content: 'New content', + }); + expect(out[0]?.id).toBeDefined(); + }); + + it('falls back to typo-fix when the synthesizer returns null', () => { + const entries: MemoryEntry[] = [ + { type: 'user', name: 'teh name', description: 'teh desc', content: 'teh body' }, + ]; + const out = resynthesizeMemoryEntries( + entries, + () => null, + (t) => t.replace(/teh/g, 'the'), + ); + expect(out[0]?.name).toBe('the name'); + expect(out[0]?.description).toBe('the desc'); + expect(out[0]?.content).toBe('the body'); + }); + + it('preserves an existing id when present', () => { + const entries: MemoryEntry[] = [ + { id: 'preserved', type: 'user', name: 'A', description: 'd', content: 'c' }, + ]; + const out = resynthesizeMemoryEntries( + entries, + () => ({ name: 'A', description: 'd', content: 'c' }), + (t) => t, + ); + expect(out[0]?.id).toBe('preserved'); + }); +}); diff --git a/packages/services/src/mindMemory/memory-entries.ts b/packages/services/src/mindMemory/memory-entries.ts new file mode 100644 index 00000000..6a0ef92c --- /dev/null +++ b/packages/services/src/mindMemory/memory-entries.ts @@ -0,0 +1,238 @@ +/** + * Memory entry types, parsing, deduplication, and validation. + * + * Pure module: no I/O, no logging. + * + * Zod v4 audit: this module uses `z.object`, `z.string()`, `z.string().min(1)`, + * `z.string().optional()`, `z.enum([...] as const)`, and only the `.success` field + * of `safeParse(...)`. All of these are stable across zod v3→v4. The `.error.issues` + * shape changed in v4 but is not consumed here. + */ +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; + +const MEMORY_ENTRY_TYPES = ['user', 'feedback', 'project', 'reference', 'prohibition'] as const; + +export interface MemoryEntry { + readonly id?: string; + readonly type: (typeof MEMORY_ENTRY_TYPES)[number]; + readonly name: string; + readonly description: string; + readonly content: string; + readonly source?: string; + readonly createdAt?: string; +} + +const MemoryEntrySchema = z.object({ + id: z.string().optional(), + type: z.enum(MEMORY_ENTRY_TYPES), + name: z.string().min(1), + description: z.string().min(1), + content: z.string(), + source: z.string().optional(), + createdAt: z.string().optional(), +}); + +export function validateMemoryEntry(entry: unknown): entry is MemoryEntry { + return MemoryEntrySchema.safeParse(entry).success; +} + +export function parseMemoryMd(content: string): MemoryEntry[] { + if (!content.trim()) return []; + + const entries: MemoryEntry[] = []; + const blocks = splitIntoBlocks(content); + + for (const block of blocks) { + const parsed = parseBlock(block); + if (parsed) entries.push(parsed); + } + + return entries; +} + +function splitIntoBlocks(content: string): string[] { + const blocks: string[] = []; + const regex = /---\n([\s\S]*?)---\n([\s\S]*?)(?=\n---\n|$)/g; + let match: RegExpExecArray | null; + + while ((match = regex.exec(content)) !== null) { + const frontmatter = match[1]!; + const body = match[2]!; + blocks.push(`---\n${frontmatter}---\n${body}`); + } + + return blocks; +} + +function parseBlock(block: string): MemoryEntry | null { + const fmMatch = /^---\n([\s\S]*?)\n?---\n([\s\S]*)$/.exec(block.trim()); + if (!fmMatch) return null; + + const frontmatter = fmMatch[1]!; + const content = fmMatch[2]!.trim(); + + const fields = parseSimpleYaml(frontmatter); + if (!fields.name || !fields.description || !fields.type) return null; + + const entry: Record = { + type: fields.type, + name: fields.name, + description: fields.description, + content, + }; + + if (fields.id) entry['id'] = fields.id; + if (fields.source) entry['source'] = fields.source; + if (fields.createdAt) entry['createdAt'] = fields.createdAt; + + return validateMemoryEntry(entry) ? (entry as unknown as MemoryEntry) : null; +} + +function parseSimpleYaml(text: string): Record { + const result: Record = {}; + for (const line of text.split('\n')) { + const colonIdx = line.indexOf(':'); + if (colonIdx === -1) continue; + const key = line.slice(0, colonIdx).trim(); + const value = line.slice(colonIdx + 1).trim(); + if (key && value) result[key] = value; + } + return result; +} + +export function serializeMemoryMd(entries: ReadonlyArray): string { + return entries.map(serializeEntry).join('\n\n'); +} + +function serializeEntry(entry: MemoryEntry): string { + const fields: string[] = []; + + if (entry.id) fields.push(`id: ${entry.id}`); + fields.push(`name: ${entry.name}`); + fields.push(`description: ${entry.description}`); + fields.push(`type: ${entry.type}`); + + if (entry.source) fields.push(`source: ${entry.source}`); + if (entry.createdAt) fields.push(`createdAt: ${entry.createdAt}`); + + return `---\n${fields.join('\n')}\n---\n${entry.content}`; +} + +export function detectDuplicates( + entries: ReadonlyArray, +): ReadonlyArray { + const pairs: Array = []; + + for (let i = 0; i < entries.length; i++) { + for (let j = i + 1; j < entries.length; j++) { + const a = entries[i]!; + const b = entries[j]!; + + if (a.name.toLowerCase() === b.name.toLowerCase()) { + pairs.push([i, j] as const); + continue; + } + + if (contentSimilarity(a.content, b.content) > 0.8) { + pairs.push([i, j] as const); + } + } + } + + return pairs; +} + +function contentSimilarity(a: string, b: string): number { + const na = normalize(a); + const nb = normalize(b); + if (na.length === 0 && nb.length === 0) return 1; + if (na.length === 0 || nb.length === 0) return 0; + + const longer = na.length >= nb.length ? na : nb; + const shorter = na.length < nb.length ? na : nb; + + let matches = 0; + for (let i = 0; i < shorter.length; i++) { + if (shorter[i] === longer[i]) matches++; + } + + return matches / longer.length; +} + +function normalize(text: string): string { + return text.toLowerCase().replace(/\s+/g, ' ').trim(); +} + +export function deduplicateEntries( + entries: ReadonlyArray, +): ReadonlyArray { + const dupes = detectDuplicates(entries); + const removeSet = new Set(); + + for (const [earlier] of dupes) { + removeSet.add(earlier); + } + + return entries.filter((_, i) => !removeSet.has(i)); +} + +export function resolveContradictions( + entries: ReadonlyArray, +): ReadonlyArray { + const removeSet = new Set(); + + for (let i = 0; i < entries.length; i++) { + for (let j = i + 1; j < entries.length; j++) { + const a = entries[i]!; + const b = entries[j]!; + + if (a.name.toLowerCase() === b.name.toLowerCase()) { + removeSet.add(i); + continue; + } + + if (a.type === b.type && nameSimilarity(a.name, b.name) > 0.8) { + removeSet.add(i); + } + } + } + + return entries.filter((_, i) => !removeSet.has(i)); +} + +function nameSimilarity(a: string, b: string): number { + return contentSimilarity(a, b); +} + +export function resynthesizeMemoryEntries( + entries: ReadonlyArray, + synthesize: ( + content: string, + type: MemoryEntry['type'], + ) => { name: string; description: string; content: string } | null, + fixTyposFn: (text: string) => string, +): MemoryEntry[] { + const results: MemoryEntry[] = []; + for (const entry of entries) { + const synthesized = synthesize(entry.content, entry.type); + if (synthesized) { + results.push({ + ...entry, + id: entry.id ?? randomUUID(), + name: synthesized.name, + description: synthesized.description, + content: synthesized.content, + }); + } else { + results.push({ + ...entry, + id: entry.id ?? randomUUID(), + name: fixTyposFn(entry.name), + description: fixTyposFn(entry.description), + content: fixTyposFn(entry.content), + }); + } + } + return results; +} diff --git a/packages/services/src/mindMemory/memory-limits.test.ts b/packages/services/src/mindMemory/memory-limits.test.ts new file mode 100644 index 00000000..9d89c95e --- /dev/null +++ b/packages/services/src/mindMemory/memory-limits.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from 'vitest'; + +import { + MAX_ENTRYPOINT_BYTES, + MAX_ENTRYPOINT_LINES, + countBytes, + countLines, + truncateEntrypoint, +} from './memory-limits'; + +describe('memory-limits constants', () => { + it('caps the entrypoint at the SCNS spec limits', () => { + expect(MAX_ENTRYPOINT_LINES).toBe(200); + expect(MAX_ENTRYPOINT_BYTES).toBe(25_000); + }); +}); + +describe('countLines', () => { + it('returns 0 for an empty string', () => { + expect(countLines('')).toBe(0); + }); + + it('returns 0 for a string containing only a trailing newline', () => { + expect(countLines('\n')).toBe(0); + }); + + it('returns 1 for a single line with no trailing newline', () => { + expect(countLines('hello')).toBe(1); + }); + + it('treats a trailing newline as terminator, not as an extra line', () => { + expect(countLines('hello\n')).toBe(1); + }); + + it('counts interior newlines as separators', () => { + expect(countLines('a\nb')).toBe(2); + expect(countLines('a\nb\nc\n')).toBe(3); + }); +}); + +describe('countBytes', () => { + it('returns the UTF-8 byte length of an ASCII string', () => { + expect(countBytes('hello')).toBe(5); + }); + + it('counts multi-byte UTF-8 characters by their byte length', () => { + expect(countBytes('héllo')).toBe(6); + expect(countBytes('🦆')).toBe(4); + }); + + it('returns 0 for an empty string', () => { + expect(countBytes('')).toBe(0); + }); +}); + +describe('truncateEntrypoint', () => { + it('passes empty content through unchanged', () => { + const result = truncateEntrypoint(''); + expect(result.content).toBe(''); + expect(result.truncated).toBe(false); + expect(result.warning).toBeNull(); + }); + + it('passes content under both limits through unchanged', () => { + const content = 'one\ntwo\nthree'; + const result = truncateEntrypoint(content); + expect(result.content).toBe(content); + expect(result.truncated).toBe(false); + expect(result.warning).toBeNull(); + }); + + it('truncates content that exceeds the line limit and reports a lines warning', () => { + const content = Array.from({ length: MAX_ENTRYPOINT_LINES + 50 }, (_, i) => `line${i}`).join( + '\n', + ); + const result = truncateEntrypoint(content); + expect(result.truncated).toBe(true); + expect(result.warning).toBe(''); + // Resulting content has MAX_ENTRYPOINT_LINES kept lines + the warning line. + const lines = result.content.split('\n'); + expect(lines).toHaveLength(MAX_ENTRYPOINT_LINES + 1); + expect(lines[MAX_ENTRYPOINT_LINES]).toBe(''); + }); + + it('truncates content that exceeds the byte limit and reports a bytes warning', () => { + const oversized = 'x'.repeat(MAX_ENTRYPOINT_BYTES + 100); + const result = truncateEntrypoint(oversized); + expect(result.truncated).toBe(true); + expect(result.warning).toBe(''); + expect(countBytes(result.content)).toBeLessThanOrEqual(MAX_ENTRYPOINT_BYTES + 100); + }); + + it('reports both lines and bytes when both limits are exceeded', () => { + // Many short lines so we trip BOTH gates. + const content = Array.from({ length: MAX_ENTRYPOINT_LINES + 500 }, () => + 'a'.repeat(200), + ).join('\n'); + const result = truncateEntrypoint(content); + expect(result.truncated).toBe(true); + expect(result.warning).toBe(''); + }); + + it('does not split multi-byte sequences when hard-cutting a single oversized line', () => { + const single = '🦆'.repeat(Math.ceil(MAX_ENTRYPOINT_BYTES / 4) + 50); + const result = truncateEntrypoint(single); + expect(result.truncated).toBe(true); + // The truncated content must be valid UTF-8 (no replacement character at the end). + expect(result.content).not.toMatch(/\uFFFD$/); + }); +}); diff --git a/packages/services/src/mindMemory/memory-limits.ts b/packages/services/src/mindMemory/memory-limits.ts new file mode 100644 index 00000000..d59bfc55 --- /dev/null +++ b/packages/services/src/mindMemory/memory-limits.ts @@ -0,0 +1,80 @@ +/** + * MEMORY.md size enforcement — keeps the entrypoint within SCNS-style spec limits. + * + * Pure module: no I/O, no logging, no global state. + */ + +export const MAX_ENTRYPOINT_LINES = 200; +export const MAX_ENTRYPOINT_BYTES = 25_000; + +export interface TruncateResult { + readonly content: string; + readonly truncated: boolean; + readonly warning: string | null; +} + +export function countLines(content: string): number { + if (content === '') return 0; + const stripped = content.endsWith('\n') ? content.slice(0, -1) : content; + if (stripped === '') return 0; + return stripped.split('\n').length; +} + +export function countBytes(content: string): number { + return Buffer.byteLength(content, 'utf-8'); +} + +export function truncateEntrypoint(content: string): TruncateResult { + if (content === '') { + return { content: '', truncated: false, warning: null }; + } + + const overLines = countLines(content) > MAX_ENTRYPOINT_LINES; + const overBytes = countBytes(content) > MAX_ENTRYPOINT_BYTES; + + if (!overLines && !overBytes) { + return { content, truncated: false, warning: null }; + } + + let result = content; + let hitLines = false; + let hitBytes = false; + + if (overLines) { + const lines = result.split('\n'); + result = lines.slice(0, MAX_ENTRYPOINT_LINES).join('\n'); + hitLines = true; + } + + if (countBytes(result) > MAX_ENTRYPOINT_BYTES) { + result = truncateToByteLimit(result); + hitBytes = true; + } + + const cap = hitLines && hitBytes ? 'lines and bytes' : hitLines ? 'lines' : 'bytes'; + const warning = ``; + + return { + content: `${result}\n${warning}`, + truncated: true, + warning, + }; +} + +function truncateToByteLimit(content: string): string { + const buf = Buffer.from(content, 'utf-8'); + const sliced = buf.subarray(0, MAX_ENTRYPOINT_BYTES); + let str = sliced.toString('utf-8'); + + const lastNl = str.lastIndexOf('\n'); + if (lastNl > 0) { + str = str.slice(0, lastNl); + } else { + str = Buffer.from(content, 'utf-8').subarray(0, MAX_ENTRYPOINT_BYTES).toString('utf-8'); + if (str.endsWith('\uFFFD')) { + str = str.slice(0, -1); + } + } + + return str; +} diff --git a/packages/services/src/mindMemory/oneShotSession.test.ts b/packages/services/src/mindMemory/oneShotSession.test.ts new file mode 100644 index 00000000..e1a3da51 --- /dev/null +++ b/packages/services/src/mindMemory/oneShotSession.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect, vi } from 'vitest'; +import type { + CopilotClient, + CopilotSession, + SessionConfig, +} from '@github/copilot-sdk'; + +import { buildOneShotSession } from './oneShotSession'; + +interface FakeSession { + sendAndWait: ReturnType; + abort: ReturnType; + disconnect: ReturnType; +} + +interface FakeWorld { + client: CopilotClient; + capturedConfig: SessionConfig | undefined; + session: FakeSession; +} + +function makeWorld(overrides: Partial = {}): FakeWorld { + const session: FakeSession = { + sendAndWait: vi.fn().mockResolvedValue({ data: { content: 'pong' } }), + abort: vi.fn().mockResolvedValue(undefined), + disconnect: vi.fn().mockResolvedValue(undefined), + ...overrides, + }; + let capturedConfig: SessionConfig | undefined; + const client = { + createSession: vi.fn(async (cfg: SessionConfig) => { + capturedConfig = cfg; + return session as unknown as CopilotSession; + }), + } as unknown as CopilotClient; + return { + client, + get capturedConfig() { + return capturedConfig; + }, + session, + }; +} + +describe('buildOneShotSession', () => { + it('creates a session with the locked-down memory-consolidation contract', async () => { + const world = makeWorld(); + const controller = new AbortController(); + + await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: controller.signal, + }); + + const cfg = world.capturedConfig; + expect(cfg?.workingDirectory).toBe('/tmp/mind-x'); + expect(cfg?.enableConfigDiscovery).toBe(false); + expect(cfg?.tools).toEqual([]); + expect(cfg?.systemMessage).toEqual({ mode: 'replace', content: '' }); + const result = cfg?.onPermissionRequest?.({} as never, {} as never); + expect(result).toEqual({ + kind: 'reject', + feedback: 'Tool permissions are disabled for memory-consolidation sessions.', + }); + }); + + it('returns the assistant content from sendAndWait', async () => { + const world = makeWorld(); + const oneShot = await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: new AbortController().signal, + }); + + const text = await oneShot.send('hello'); + + expect(text).toBe('pong'); + expect(world.session.sendAndWait).toHaveBeenCalledWith({ prompt: 'hello' }); + }); + + it('returns empty string when the SDK reports no assistant event', async () => { + const world = makeWorld({ + sendAndWait: vi.fn().mockResolvedValue(undefined), + }); + const oneShot = await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: new AbortController().signal, + }); + + expect(await oneShot.send('hello')).toBe(''); + }); + + it('aborts the live session when the caller signal fires', async () => { + const world = makeWorld(); + const controller = new AbortController(); + await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: controller.signal, + }); + + expect(world.session.abort).not.toHaveBeenCalled(); + + controller.abort(); + + expect(world.session.abort).toHaveBeenCalledTimes(1); + }); + + it('aborts immediately when the signal is already aborted', async () => { + const world = makeWorld(); + const controller = new AbortController(); + controller.abort(); + + await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: controller.signal, + }); + + expect(world.session.abort).toHaveBeenCalledTimes(1); + }); + + it('swallows abort errors so the abort handler cannot crash the daemon', async () => { + const world = makeWorld({ + abort: vi.fn().mockRejectedValue(new Error('already aborted')), + }); + const controller = new AbortController(); + await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: controller.signal, + }); + + expect(() => controller.abort()).not.toThrow(); + await Promise.resolve(); + }); + + it('close disconnects the session and removes the abort listener', async () => { + const world = makeWorld(); + const controller = new AbortController(); + const oneShot = await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: controller.signal, + }); + + await oneShot.close(); + expect(world.session.disconnect).toHaveBeenCalledTimes(1); + + // Aborting after close must not call session.abort again. + controller.abort(); + expect(world.session.abort).not.toHaveBeenCalled(); + }); + + it('reports disconnect errors via onDisconnectError instead of throwing', async () => { + const world = makeWorld({ + disconnect: vi.fn().mockRejectedValue(new Error('boom')), + }); + const reported: unknown[] = []; + const oneShot = await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: new AbortController().signal, + onDisconnectError: (err) => reported.push(err), + }); + + await expect(oneShot.close()).resolves.toBeUndefined(); + expect(reported).toHaveLength(1); + expect((reported[0] as Error).message).toBe('boom'); + }); + + it('swallows disconnect errors silently when no reporter is supplied', async () => { + const world = makeWorld({ + disconnect: vi.fn().mockRejectedValue(new Error('boom')), + }); + const oneShot = await buildOneShotSession({ + client: world.client, + workingDirectory: '/tmp/mind-x', + signal: new AbortController().signal, + }); + + await expect(oneShot.close()).resolves.toBeUndefined(); + }); +}); diff --git a/packages/services/src/mindMemory/oneShotSession.ts b/packages/services/src/mindMemory/oneShotSession.ts new file mode 100644 index 00000000..979bbf8b --- /dev/null +++ b/packages/services/src/mindMemory/oneShotSession.ts @@ -0,0 +1,84 @@ +/** + * One-shot Copilot SDK session adapter for the Dream Daemon. + * + * Encapsulates the *contract* a memory-consolidation session must satisfy + * so callers cannot accidentally weaken it: + * + * - tools = [] (empty surface) + * - enableConfigDiscovery = false (no project config leakage) + * - systemMessage replaced (the mind's own SOUL.md is not loaded) + * - PermissionHandler refuses every request (defense-in-depth — an empty + * tool surface should never produce a permission request, but if one + * ever leaks through it is denied loudly) + * - the caller's AbortSignal aborts the in-flight CLI call + * - close() removes the abort listener and disconnects the session + * + * The SDK-typed plumbing lives here so both the desktop adapter + * (apps/desktop/.../buildMindMemoryService.ts) and the live-SDK + * integration test bind to the *same* session contract. + */ +import type { + CopilotClient, + PermissionHandler, + SessionConfig, +} from '@github/copilot-sdk'; + +import type { OneShotSession } from './CopilotLLMClient'; + +const refusingPermissionHandler: PermissionHandler = () => ({ + kind: 'reject', + feedback: 'Tool permissions are disabled for memory-consolidation sessions.', +}); + +export interface BuildOneShotSessionArgs { + readonly client: CopilotClient; + readonly workingDirectory: string; + readonly signal: AbortSignal; + /** + * Invoked when `close()` swallows a disconnect error. Optional so + * tests don't have to plumb a logger; production wiring passes a + * structured-log callback. + */ + readonly onDisconnectError?: (err: unknown) => void; +} + +export async function buildOneShotSession( + args: BuildOneShotSessionArgs, +): Promise { + const { client, workingDirectory, signal, onDisconnectError } = args; + + const sessionConfig: SessionConfig = { + workingDirectory, + enableConfigDiscovery: false, + tools: [], + systemMessage: { mode: 'replace', content: '' }, + onPermissionRequest: refusingPermissionHandler, + }; + + const session = await client.createSession(sessionConfig); + + const onAbort = (): void => { + session.abort().catch(() => { /* best-effort: abort can race with natural completion */ }); + }; + + if (signal.aborted) { + onAbort(); + } else { + signal.addEventListener('abort', onAbort, { once: true }); + } + + return { + async send(prompt: string): Promise { + const event = await session.sendAndWait({ prompt }); + return event?.data.content ?? ''; + }, + async close(): Promise { + signal.removeEventListener('abort', onAbort); + try { + await session.disconnect(); + } catch (err) { + onDisconnectError?.(err); + } + }, + }; +} diff --git a/packages/services/src/mindMemory/rollback.test.ts b/packages/services/src/mindMemory/rollback.test.ts new file mode 100644 index 00000000..e7b148bf --- /dev/null +++ b/packages/services/src/mindMemory/rollback.test.ts @@ -0,0 +1,234 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { + STRUCTURED_LOG_SENTINEL, + serializeTurn, + type CompletedTurn, +} from './StructuredLogFormat'; +import { rollbackToUnstructured } from './rollback'; + +const WORKING_MEMORY = '.working-memory'; +const LOG_FILE = 'log.md'; +const LEGACY_FILE = 'log.legacy.md'; + +function turnAt(seq: number): CompletedTurn { + return { + turnId: `turn-${seq}`, + sessionId: `session-${seq}`, + model: 'gpt-test', + status: 'completed', + startedAt: `2026-04-${String(seq).padStart(2, '0')}T11:59:00.000Z`, + endedAt: `2026-04-${String(seq).padStart(2, '0')}T12:00:00.000Z`, + prompt: `User prompt #${seq}`, + finalAssistantMessage: `Assistant reply #${seq}`, + }; +} + +describe('rollbackToUnstructured', () => { + let tmpDir: string; + let workingMemoryDir: string; + let logPath: string; + let legacyPath: string; + const fixedNow = new Date('2026-04-15T08:00:00.000Z'); + const captured: string[] = []; + const testLogger = { + info: (m: string) => captured.push(`INFO ${m}`), + warn: (m: string) => captured.push(`WARN ${m}`), + }; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-rollback-')); + workingMemoryDir = path.join(tmpDir, WORKING_MEMORY); + fs.mkdirSync(workingMemoryDir, { recursive: true }); + logPath = path.join(workingMemoryDir, LOG_FILE); + legacyPath = path.join(workingMemoryDir, LEGACY_FILE); + captured.length = 0; + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function seedStructuredLog(turns: readonly CompletedTurn[]): void { + const body = turns.map(serializeTurn).join(''); + fs.writeFileSync(logPath, `${STRUCTURED_LOG_SENTINEL}\n\n${body}`, 'utf-8'); + } + + it('converts N structured frames into rendered markdown and removes the sentinel', async () => { + seedStructuredLog([turnAt(1), turnAt(2), turnAt(3)]); + + const result = await rollbackToUnstructured(tmpDir, { now: () => fixedNow }); + + expect(result).toEqual({ + framesConverted: 3, + legacyExisted: false, + outcome: 'rolled-back', + }); + + const after = fs.readFileSync(logPath, 'utf-8'); + expect(after).not.toContain(STRUCTURED_LOG_SENTINEL); + expect(after).toContain('## Resumed unstructured logging — 2026-04-15T08:00:00.000Z'); + expect(after).toContain('## 2026-04-01T12:00:00.000Z — turn turn-1 (gpt-test)'); + expect(after).toContain('**User**: User prompt #1'); + expect(after).toContain('**Assistant**: Assistant reply #1'); + expect(after).toContain('**User**: User prompt #3'); + expect(after).toContain('**Assistant**: Assistant reply #3'); + }); + + it('folds existing log.legacy.md content into the merged output and removes the legacy file', async () => { + fs.writeFileSync(legacyPath, '# Legacy notes\n\nFirst-ever line.\n', 'utf-8'); + seedStructuredLog([turnAt(7)]); + + const result = await rollbackToUnstructured(tmpDir, { now: () => fixedNow, logger: testLogger }); + + expect(result).toEqual({ + framesConverted: 1, + legacyExisted: true, + outcome: 'rolled-back', + }); + + const after = fs.readFileSync(logPath, 'utf-8'); + expect(after.startsWith('# Legacy notes\n\nFirst-ever line.')).toBe(true); + expect(after).toContain('---'); + expect(after).toContain('## Resumed unstructured logging'); + expect(after).toContain('**User**: User prompt #7'); + expect(fs.existsSync(legacyPath)).toBe(false); + }); + + it('is a no-op when log.md is missing', async () => { + const result = await rollbackToUnstructured(tmpDir); + expect(result).toEqual({ + framesConverted: 0, + legacyExisted: false, + outcome: 'no-op-missing', + }); + }); + + it('is a no-op when log.md is present but empty', async () => { + fs.writeFileSync(logPath, '', 'utf-8'); + const result = await rollbackToUnstructured(tmpDir); + expect(result).toEqual({ + framesConverted: 0, + legacyExisted: false, + outcome: 'no-op-empty', + }); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(''); + }); + + it('warns and leaves the file untouched when log.md has no sentinel (already unstructured)', async () => { + const original = '# Already unstructured\n\nUser said something.\n'; + fs.writeFileSync(logPath, original, 'utf-8'); + + const result = await rollbackToUnstructured(tmpDir, { logger: testLogger }); + + expect(result).toEqual({ + framesConverted: 0, + legacyExisted: false, + outcome: 'no-op-no-sentinel', + }); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(original); + expect(captured.some((m) => m.startsWith('WARN') && /no sentinel/.test(m))).toBe(true); + }); + + it('is idempotent — calling rollback twice yields the same content (second call is no-op-no-sentinel)', async () => { + seedStructuredLog([turnAt(1), turnAt(2)]); + + await rollbackToUnstructured(tmpDir, { now: () => fixedNow }); + const afterFirst = fs.readFileSync(logPath, 'utf-8'); + + const second = await rollbackToUnstructured(tmpDir, { now: () => fixedNow, logger: testLogger }); + + expect(second.outcome).toBe('no-op-no-sentinel'); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(afterFirst); + }); + + it('Flow 4 regression: empty-model frame round-trips through rollback without data loss', async () => { + const turn: CompletedTurn = { + turnId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', + sessionId: 'sess-empty-model', + model: '', + status: 'completed', + startedAt: '2026-04-01T11:59:00.000Z', + endedAt: '2026-04-01T12:00:00.000Z', + prompt: 'Important user content', + finalAssistantMessage: 'Important assistant content', + }; + const raw = `${STRUCTURED_LOG_SENTINEL}\n\n${serializeTurn(turn)}`; + fs.writeFileSync(logPath, raw, 'utf-8'); + + const result = await rollbackToUnstructured(tmpDir, { now: () => fixedNow }); + + expect(result.outcome).toBe('rolled-back'); + expect(result.framesConverted).toBe(1); + const after = fs.readFileSync(logPath, 'utf-8'); + expect(after).toContain('Important user content'); + expect(after).toContain('Important assistant content'); + expect(after).not.toContain(STRUCTURED_LOG_SENTINEL); + }); + + it('preserves log.md unchanged when sentinel is present but all frames are malformed (no-op-malformed)', async () => { + const malformedFrame = + '## not-a-date turn:11111111-1111-4111-8111-111111111111 status:completed\n' + + 'session: s\nmodel: m\n\n### user\nq\n\n### assistant\na\n'; + const original = `${STRUCTURED_LOG_SENTINEL}\n\n${malformedFrame}`; + fs.writeFileSync(logPath, original, 'utf-8'); + + const result = await rollbackToUnstructured(tmpDir, { now: () => fixedNow, logger: testLogger }); + + expect(result.outcome).toBe('no-op-malformed'); + expect(result.framesConverted).toBe(0); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(original); + expect(captured.some((m) => m.startsWith('WARN') && /malformed/.test(m))).toBe(true); + }); + + it('atomicity: simulated rename failure leaves log.md and log.legacy.md byte-equal to prior state', async () => { + fs.writeFileSync(legacyPath, 'legacy bytes', 'utf-8'); + seedStructuredLog([turnAt(5)]); + const beforeLog = fs.readFileSync(logPath, 'utf-8'); + const beforeLegacy = fs.readFileSync(legacyPath, 'utf-8'); + + const failingRename = async () => { throw new Error('synthetic rename failure'); }; + + await expect( + rollbackToUnstructured(tmpDir, { now: () => fixedNow, rename: failingRename }), + ).rejects.toThrow(/synthetic rename failure/); + + expect(fs.readFileSync(logPath, 'utf-8')).toBe(beforeLog); + expect(fs.readFileSync(legacyPath, 'utf-8')).toBe(beforeLegacy); + const lingering = fs.readdirSync(workingMemoryDir).filter((entry) => entry.endsWith('.tmp')); + expect(lingering).toEqual([]); + }); + + it('zero-frames sentinel-only log rolls back to legacy content alone (no spurious "Resumed" header)', async () => { + fs.writeFileSync(legacyPath, 'pre-existing legacy notes', 'utf-8'); + fs.writeFileSync(logPath, `${STRUCTURED_LOG_SENTINEL}\n\n`, 'utf-8'); + + const result = await rollbackToUnstructured(tmpDir, { now: () => fixedNow }); + + expect(result).toEqual({ + framesConverted: 0, + legacyExisted: true, + outcome: 'rolled-back', + }); + + const after = fs.readFileSync(logPath, 'utf-8'); + expect(after).not.toContain('Resumed unstructured logging'); + expect(after).toContain('pre-existing legacy notes'); + expect(fs.existsSync(legacyPath)).toBe(false); + }); + + it('zero-frames sentinel-only log with no legacy yields an empty log.md (no spurious header)', async () => { + fs.writeFileSync(logPath, `${STRUCTURED_LOG_SENTINEL}\n\n`, 'utf-8'); + + const result = await rollbackToUnstructured(tmpDir, { now: () => fixedNow }); + + expect(result).toEqual({ + framesConverted: 0, + legacyExisted: false, + outcome: 'rolled-back', + }); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(''); + }); +}); diff --git a/packages/services/src/mindMemory/rollback.ts b/packages/services/src/mindMemory/rollback.ts new file mode 100644 index 00000000..f80801d1 --- /dev/null +++ b/packages/services/src/mindMemory/rollback.ts @@ -0,0 +1,199 @@ +// Phase 4 — opt-out rollback for the dream daemon. Converts a structured +// (sentinel-prefixed) `log.md` back into freeform markdown and folds in any +// pre-existing `log.legacy.md` content so the user is left with a single +// human-readable file. Designed to run AFTER `MindManager.reloadMind` has +// torn down the writer/observer for the mind, so there's no concurrent +// writer racing the rewrite. + +import * as fs from 'fs'; +import * as fsp from 'fs/promises'; +import * as path from 'path'; +import { Logger } from '../logger'; +import { + parseLog, + type ParsedTurn, +} from './StructuredLogFormat'; + +const WORKING_MEMORY_DIRNAME = '.working-memory'; +const LOG_FILENAME = 'log.md'; +const LEGACY_FILENAME = 'log.legacy.md'; + +export interface RollbackResult { + /** Number of structured frames successfully converted to unstructured markdown. */ + framesConverted: number; + /** + * True if `log.legacy.md` was present (and thus folded into the merged log.md). + * **Only meaningful when `outcome === 'rolled-back'`** — no-op outcomes always report `false` + * without checking the filesystem. + */ + legacyExisted: boolean; + /** + * One of: + * - `'no-op-missing'` — log.md absent. + * - `'no-op-empty'` — log.md present but zero bytes. + * - `'no-op-no-sentinel'` — log.md present but not structured (already unstructured). + * - `'no-op-malformed'` — log.md has sentinel + non-empty body but parser produced + * zero turns (all frames malformed). File is preserved byte-identical to avoid + * data loss; toggle is still successful at the config level. + * - `'rolled-back'` — log.md was structured and was rewritten. + */ + outcome: 'no-op-missing' | 'no-op-empty' | 'no-op-no-sentinel' | 'no-op-malformed' | 'rolled-back'; +} + +export interface RollbackLogger { + info(message: string): void; + warn(message: string, ...rest: unknown[]): void; +} + +export interface RollbackDeps { + logger?: RollbackLogger; + /** Override for tests that need to simulate a rename failure. */ + rename?: (from: string, to: string) => Promise; + /** Override clock for deterministic merged-section header timestamps in tests. */ + now?: () => Date; +} + +function isErrnoCode(err: unknown, code: string): boolean { + return typeof err === 'object' && err !== null && (err as { code?: string }).code === code; +} + +async function readOrNull(absPath: string): Promise { + try { + return await fsp.readFile(absPath, 'utf-8'); + } catch (err) { + if (isErrnoCode(err, 'ENOENT')) return null; + throw err; + } +} + +function renderTurnAsMarkdown(turn: ParsedTurn): string { + // Format approved per plan.md Phase 4 spec: + // ## {ISO} — turn {turnId} ({model}) + // **User**: {prompt} + // + // **Assistant**: {finalAssistantMessage} + return [ + `## ${turn.timestamp} — turn ${turn.turnId} (${turn.model})`, + '', + `**User**: ${turn.prompt}`, + '', + `**Assistant**: ${turn.assistant}`, + '', + ].join('\n'); +} + +function composeMergedContent( + legacyContent: string | null, + turns: readonly ParsedTurn[], + resumedAt: string, +): string { + // Zero-frames sentinel-only rollback: don't emit a "Resumed" header that + // claims content was resumed when nothing was. Just preserve legacy (or + // empty file) and let the caller move on. + if (turns.length === 0) { + if (legacyContent && legacyContent.length > 0) { + return legacyContent.endsWith('\n') ? legacyContent : `${legacyContent}\n`; + } + return ''; + } + + const renderedFrames = turns.map(renderTurnAsMarkdown).join('\n'); + const resumedSection = `## Resumed unstructured logging — ${resumedAt}\n\n${renderedFrames}`; + + if (legacyContent && legacyContent.length > 0) { + const legacyTrimmed = legacyContent.endsWith('\n') ? legacyContent.slice(0, -1) : legacyContent; + return `${legacyTrimmed}\n\n---\n\n${resumedSection}`; + } + + return resumedSection; +} + +export async function rollbackToUnstructured( + mindPath: string, + deps: RollbackDeps = {}, +): Promise { + const log: RollbackLogger = deps.logger ?? Logger.create('rollbackToUnstructured'); + const rename = deps.rename ?? fsp.rename; + const now = deps.now ?? (() => new Date()); + + const workingMemoryDir = path.resolve(mindPath, WORKING_MEMORY_DIRNAME); + const logPath = path.join(workingMemoryDir, LOG_FILENAME); + const legacyPath = path.join(workingMemoryDir, LEGACY_FILENAME); + + const currentContent = await readOrNull(logPath); + if (currentContent === null) { + return { framesConverted: 0, legacyExisted: false, outcome: 'no-op-missing' }; + } + if (currentContent.length === 0) { + return { framesConverted: 0, legacyExisted: false, outcome: 'no-op-empty' }; + } + + const parsed = parseLog(currentContent); + if (!parsed.sentinel) { + log.warn( + `rollbackToUnstructured: log.md at ${logPath} has no sentinel — already unstructured. Leaving file untouched.`, + ); + return { framesConverted: 0, legacyExisted: false, outcome: 'no-op-no-sentinel' }; + } + + // Sentinel-with-content-but-no-parseable-frames: preserve the raw file + // to avoid data loss. The user's chat history is in there (just unparseable); + // overwriting with an empty file would destroy it. The toggle still + // succeeds at the config level — this branch only refuses the rewrite. + if (parsed.turns.length === 0 && parsed.malformed > 0) { + log.warn( + `rollbackToUnstructured: log.md at ${logPath} has ${parsed.malformed} malformed frame(s) and no parseable turns. Preserving file as-is to avoid data loss.`, + ); + return { framesConverted: 0, legacyExisted: false, outcome: 'no-op-malformed' }; + } + + const legacyContent = await readOrNull(legacyPath); + const legacyExisted = legacyContent !== null; + + const merged = composeMergedContent(legacyContent, parsed.turns, now().toISOString()); + + // Atomic rewrite: write to tmp, fsync, rename. If anything fails, the + // original log.md and log.legacy.md remain byte-identical. + const tmpPath = `${logPath}.rollback.${process.pid}.${Date.now()}.tmp`; + const handle = await fsp.open(tmpPath, 'wx'); + try { + await handle.writeFile(merged, 'utf-8'); + await handle.sync(); + } finally { + // SF-3: post-sync close is virtually infallible (data is on disk), but a + // throw here would propagate and skip the rename. Swallow defensively so + // a phantom close failure doesn't poison a successful write. + await handle.close().catch(() => { /* fd will be released by GC */ }); + } + + try { + await rename(tmpPath, logPath); + } catch (err) { + if (fs.existsSync(tmpPath)) { + try { fs.rmSync(tmpPath, { force: true }); } catch { /* best-effort */ } + } + throw err; + } + + if (legacyExisted) { + try { + await fsp.unlink(legacyPath); + } catch (err) { + // Non-fatal — the merged log.md already contains the legacy content. + // We log so the operator knows the file is orphaned, but we don't + // re-raise: rollback succeeded from the user's perspective. + log.warn(`rollbackToUnstructured: failed to remove ${legacyPath} after merge:`, err); + } + } + + log.info( + `rollbackToUnstructured: converted ${parsed.turns.length} frame(s) for ${mindPath}` + + (legacyExisted ? ' (legacy log folded in)' : ''), + ); + + return { + framesConverted: parsed.turns.length, + legacyExisted, + outcome: 'rolled-back', + }; +} diff --git a/packages/services/src/mindProfile/MindProfileService.test.ts b/packages/services/src/mindProfile/MindProfileService.test.ts index 09156284..cac6b5e4 100644 --- a/packages/services/src/mindProfile/MindProfileService.test.ts +++ b/packages/services/src/mindProfile/MindProfileService.test.ts @@ -105,6 +105,104 @@ describe('MindProfileService', () => { fs.rmSync(root, { recursive: true, force: true }); } }); + + it('exposes dreamDaemonEnabled=false when no .chamber.json is present', () => { + const { root, service } = createProfileFixture(); + try { + const profile = service.getProfile('mind-1'); + expect(profile.dreamDaemonEnabled).toBe(false); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it('exposes dreamDaemonEnabled=true when .chamber.json opts in to consolidation', () => { + const { root, service } = createProfileFixture(); + try { + fs.writeFileSync( + path.join(root, '.chamber.json'), + JSON.stringify({ workingMemory: { consolidation: { enabled: true } } }), + ); + const profile = service.getProfile('mind-1'); + expect(profile.dreamDaemonEnabled).toBe(true); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + describe('feature-flag gate (dreamDaemonFeatureEnabled)', () => { + // Mirrors IdentityLoader's gate: app-level flag is authoritative over + // per-mind .chamber.json opt-in. When the flag is off, the profile + // payload must report `dreamDaemonEnabled: false` so the (now-hidden) + // toggle UI never sees a stale ON state. + it('forces dreamDaemonEnabled=false even when .chamber.json says true and accessor returns false', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-profile-')); + try { + fs.mkdirSync(path.join(root, '.github', 'agents'), { recursive: true }); + fs.writeFileSync(path.join(root, 'SOUL.md'), '# Moneypenny\n'); + fs.writeFileSync( + path.join(root, '.chamber.json'), + JSON.stringify({ workingMemory: { consolidation: { enabled: true } } }), + ); + + const provider: MindProfileMindProvider = { + getMindPath: () => root, + restartMind: async () => ({}), + }; + const normalizer: AvatarNormalizer = { + normalize: async ({ outputPath }) => { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, Buffer.from('avatar')); + }, + }; + const service = new MindProfileService( + provider, + new IdentityLoader(), + normalizer, + () => false, + ); + + const profile = service.getProfile('mind-1'); + expect(profile.dreamDaemonEnabled).toBe(false); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it('honors .chamber.json enabled:true when the accessor returns true', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-profile-')); + try { + fs.mkdirSync(path.join(root, '.github', 'agents'), { recursive: true }); + fs.writeFileSync(path.join(root, 'SOUL.md'), '# Moneypenny\n'); + fs.writeFileSync( + path.join(root, '.chamber.json'), + JSON.stringify({ workingMemory: { consolidation: { enabled: true } } }), + ); + + const provider: MindProfileMindProvider = { + getMindPath: () => root, + restartMind: async () => ({}), + }; + const normalizer: AvatarNormalizer = { + normalize: async ({ outputPath }) => { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, Buffer.from('avatar')); + }, + }; + const service = new MindProfileService( + provider, + new IdentityLoader(), + normalizer, + () => true, + ); + + const profile = service.getProfile('mind-1'); + expect(profile.dreamDaemonEnabled).toBe(true); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + }); }); function createProfileFixture() { diff --git a/packages/services/src/mindProfile/MindProfileService.ts b/packages/services/src/mindProfile/MindProfileService.ts index 16a1fdf7..a020cd70 100644 --- a/packages/services/src/mindProfile/MindProfileService.ts +++ b/packages/services/src/mindProfile/MindProfileService.ts @@ -10,23 +10,37 @@ import type { AgentProfileSaveResult, } from '@chamber/shared/types'; import type { IdentityLoader } from '../chat/IdentityLoader'; +import { loadChamberMindConfig } from '../mind/chamberMindConfig'; import type { AvatarNormalizer, MindProfileMindProvider } from './types'; const AVATAR_RELATIVE_PATH = path.join('.chamber', 'avatar.png'); const MAX_PROFILE_FILE_BYTES = 512_000; export class MindProfileService { + private readonly dreamDaemonFeatureEnabled: () => boolean; + constructor( private readonly minds: MindProfileMindProvider, private readonly identityLoader: IdentityLoader, private readonly avatarNormalizer: AvatarNormalizer, - ) {} + /** + * Returns the current value of the app-level `dreamDaemon` feature flag. + * When false, `getProfile` reports `dreamDaemonEnabled: false` regardless + * of `.chamber.json workingMemory.consolidation.enabled`, so the renderer + * (and any conditional rendering keyed off this field) never observes a + * stale ON state from a mind opted-in under an insiders build. + */ + dreamDaemonFeatureEnabled: () => boolean = () => true, + ) { + this.dreamDaemonFeatureEnabled = dreamDaemonFeatureEnabled; + } getProfile(mindId: string, needsRestart = false): AgentProfile { const mindPath = this.requireMindPath(mindId); const identity = this.identityLoader.load(mindPath); const displayName = identity?.name ?? path.basename(mindPath); const avatarPath = path.join(mindPath, AVATAR_RELATIVE_PATH); + const chamberConfig = loadChamberMindConfig(mindPath); return { mindId, @@ -37,6 +51,8 @@ export class MindProfileService { soul: this.readProfileFile(mindPath, 'soul', 'SOUL.md'), agentFiles: this.listAgentFiles(mindPath), needsRestart, + dreamDaemonEnabled: + this.dreamDaemonFeatureEnabled() && chamberConfig.workingMemory.consolidation.enabled, }; } diff --git a/packages/shared/src/electron-types.ts b/packages/shared/src/electron-types.ts index f446bd87..210d5ca3 100644 --- a/packages/shared/src/electron-types.ts +++ b/packages/shared/src/electron-types.ts @@ -74,6 +74,7 @@ export interface ElectronAPI { list: () => Promise; setActive: (mindId: string) => Promise; setModel: (mindId: string, model: string | null) => Promise; + setDreamDaemon: (mindId: string, enabled: boolean) => Promise; selectDirectory: () => Promise; openWindow: (mindId: string) => Promise; onMindChanged: (callback: (minds: MindContext[]) => void) => () => void; @@ -110,7 +111,7 @@ export interface ElectronAPI { getDefaultPath: () => Promise; pickPath: () => Promise; listTemplates: () => Promise; - create: (config: { name: string; role: string; voice: string; voiceDescription: string; basePath: string }) => Promise<{ success: boolean; mindId?: string; mindPath?: string; error?: string }>; + create: (config: { name: string; role: string; voice: string; voiceDescription: string; basePath: string; enableDreamDaemon?: boolean }) => Promise<{ success: boolean; mindId?: string; mindPath?: string; error?: string }>; createFromTemplate: (request: { templateId: string; marketplaceId?: string; basePath: string }) => Promise<{ success: boolean; mindId?: string; mindPath?: string; error?: string }>; onProgress: (callback: (progress: { step: string; detail: string }) => void) => () => void; }; diff --git a/packages/shared/src/feature-flags.test.ts b/packages/shared/src/feature-flags.test.ts index 9e324191..73caea4d 100644 --- a/packages/shared/src/feature-flags.test.ts +++ b/packages/shared/src/feature-flags.test.ts @@ -14,6 +14,7 @@ describe('feature flags', () => { it('keeps preview features disabled by default', () => { expect(DEFAULT_APP_FEATURE_FLAGS.switchboardRelay).toBe(false); expect(DEFAULT_APP_FEATURE_FLAGS.byoLlm).toBe(false); + expect(DEFAULT_APP_FEATURE_FLAGS.dreamDaemon).toBe(false); }); it('enables preview features for insiders versions', () => { @@ -21,6 +22,7 @@ describe('feature flags', () => { switchboardRelay: true, byoLlm: true, chamberCopilot: true, + dreamDaemon: true, }); }); @@ -33,6 +35,7 @@ describe('feature flags', () => { switchboardRelay: true, byoLlm: true, chamberCopilot: true, + dreamDaemon: true, }); }); @@ -43,11 +46,13 @@ describe('feature flags', () => { switchboardRelay: false, byoLlm: true, chamberCopilot: false, + dreamDaemon: false, }, })).toEqual({ switchboardRelay: false, byoLlm: true, chamberCopilot: false, + dreamDaemon: false, }); }); @@ -67,6 +72,7 @@ describe('feature flags', () => { switchboardRelay: true, byoLlm: false, chamberCopilot: false, + dreamDaemon: false, }); }); @@ -76,15 +82,15 @@ describe('feature flags', () => { updatedAt: '2026-05-17T21:00:00Z', ignored: true, channels: { - stable: { switchboardRelay: false, byoLlm: false, chamberCopilot: false }, - insiders: { switchboardRelay: true, byoLlm: true, chamberCopilot: true, futureFlag: true }, + stable: { switchboardRelay: false, byoLlm: false, chamberCopilot: false, dreamDaemon: false }, + insiders: { switchboardRelay: true, byoLlm: true, chamberCopilot: true, dreamDaemon: false, futureFlag: true }, }, })).toEqual({ version: 1, updatedAt: '2026-05-17T21:00:00Z', channels: { stable: DEFAULT_APP_FEATURE_FLAGS, - insiders: { switchboardRelay: true, byoLlm: true, chamberCopilot: true }, + insiders: { switchboardRelay: true, byoLlm: true, chamberCopilot: true, dreamDaemon: false }, }, }); }); @@ -95,6 +101,16 @@ describe('feature flags', () => { expect(parseRemoteFeatureFlagPolicy(null)).toBeNull(); }); + it('rejects remote policies missing the dreamDaemon field', () => { + expect(parseRemoteFeatureFlagPolicy({ + version: 1, + channels: { + stable: { switchboardRelay: false, byoLlm: false, chamberCopilot: false }, + insiders: { switchboardRelay: true, byoLlm: true, chamberCopilot: true, dreamDaemon: false }, + }, + })).toBeNull(); + }); + it('keeps the published GitHub Pages policy valid', () => { const policyPath = path.resolve(process.cwd(), 'docs', 'flags', 'v1', 'flags.json'); const policy = parseRemoteFeatureFlagPolicy(JSON.parse(fs.readFileSync(policyPath, 'utf-8')) as unknown); @@ -104,7 +120,7 @@ describe('feature flags', () => { updatedAt: '2026-05-17T21:00:00Z', channels: { stable: DEFAULT_APP_FEATURE_FLAGS, - insiders: { switchboardRelay: true, byoLlm: true, chamberCopilot: true }, + insiders: { switchboardRelay: true, byoLlm: true, chamberCopilot: true, dreamDaemon: false }, }, }); }); diff --git a/packages/shared/src/feature-flags.ts b/packages/shared/src/feature-flags.ts index 76407cf1..9e444967 100644 --- a/packages/shared/src/feature-flags.ts +++ b/packages/shared/src/feature-flags.ts @@ -2,6 +2,7 @@ export interface AppFeatureFlags { readonly switchboardRelay: boolean; readonly byoLlm: boolean; readonly chamberCopilot: boolean; + readonly dreamDaemon: boolean; } export type FeatureFlagChannel = 'stable' | 'insiders'; @@ -16,6 +17,7 @@ export const DEFAULT_APP_FEATURE_FLAGS: AppFeatureFlags = { switchboardRelay: false, byoLlm: false, chamberCopilot: false, + dreamDaemon: false, }; export function getAppFeatureFlags(options: { @@ -29,6 +31,7 @@ export function getAppFeatureFlags(options: { switchboardRelay: insiders, byoLlm: insiders, chamberCopilot: insiders, + dreamDaemon: insiders, }; } @@ -60,6 +63,7 @@ export function parseFeatureFlags(value: unknown): AppFeatureFlags | null { switchboardRelay: value.switchboardRelay === true, byoLlm: value.byoLlm === true, chamberCopilot: value.chamberCopilot === true, + dreamDaemon: value.dreamDaemon === true, }; } @@ -68,7 +72,8 @@ export function parseCompleteFeatureFlags(value: unknown): AppFeatureFlags | nul if ( typeof value.switchboardRelay !== 'boolean' || typeof value.byoLlm !== 'boolean' || - typeof value.chamberCopilot !== 'boolean' + typeof value.chamberCopilot !== 'boolean' || + typeof value.dreamDaemon !== 'boolean' ) { return null; } @@ -76,6 +81,7 @@ export function parseCompleteFeatureFlags(value: unknown): AppFeatureFlags | nul switchboardRelay: value.switchboardRelay, byoLlm: value.byoLlm, chamberCopilot: value.chamberCopilot, + dreamDaemon: value.dreamDaemon, }; } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 19234621..21f8d8ee 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -7,3 +7,4 @@ export { IPC, type IpcChannel } from './ipc-channels'; export { parseIpcArgs } from './ipc-validation'; export { Logger, type LogLevel } from './logger'; export { escapeXml } from './escapeXml'; +export type { CompletedTurn, TurnCompletionObserver, TurnStatus } from './turn-observer'; diff --git a/packages/shared/src/ipc-channels.ts b/packages/shared/src/ipc-channels.ts index 227ea018..f53f0ef0 100644 --- a/packages/shared/src/ipc-channels.ts +++ b/packages/shared/src/ipc-channels.ts @@ -33,6 +33,7 @@ export const IPC = { LIST: 'mind:list', SET_ACTIVE: 'mind:setActive', SET_MODEL: 'mind:setModel', + SET_DREAM_DAEMON: 'mind:setDreamDaemon', SELECT_DIRECTORY: 'mind:selectDirectory', OPEN_WINDOW: 'mind:openWindow', CHANGED: 'mind:changed', diff --git a/packages/shared/src/turn-observer.test.ts b/packages/shared/src/turn-observer.test.ts new file mode 100644 index 00000000..123368ca --- /dev/null +++ b/packages/shared/src/turn-observer.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expectTypeOf } from 'vitest'; +import type { + CompletedTurn, + TurnCompletionObserver, + TurnStatus, +} from './turn-observer'; + +describe('turn-observer types', () => { + it('TurnStatus is a closed union of completed | aborted | error', () => { + expectTypeOf().toEqualTypeOf<'completed' | 'aborted' | 'error'>(); + }); + + it('CompletedTurn contains the full payload required by Phase 6', () => { + const turn: CompletedTurn = { + turnId: 't-1', + sessionId: 's-1', + model: 'm-1', + status: 'completed', + startedAt: '2026-05-12T17:00:00.000Z', + endedAt: '2026-05-12T17:00:01.000Z', + prompt: 'hello', + finalAssistantMessage: 'hi back', + }; + expectTypeOf(turn.turnId).toBeString(); + expectTypeOf(turn.sessionId).toBeString(); + expectTypeOf(turn.model).toBeString(); + expectTypeOf(turn.status).toEqualTypeOf(); + expectTypeOf(turn.startedAt).toBeString(); + expectTypeOf(turn.endedAt).toBeString(); + expectTypeOf(turn.prompt).toBeString(); + expectTypeOf(turn.finalAssistantMessage).toBeString(); + }); + + it('TurnCompletionObserver.onTurnCompleted may be sync or async', () => { + const sync: TurnCompletionObserver = { onTurnCompleted: () => undefined }; + const async: TurnCompletionObserver = { onTurnCompleted: async () => undefined }; + expectTypeOf(sync.onTurnCompleted).parameter(0).toEqualTypeOf(); + expectTypeOf(async.onTurnCompleted).parameter(0).toEqualTypeOf(); + expectTypeOf(sync.onTurnCompleted).returns.toEqualTypeOf>(); + }); +}); diff --git a/packages/shared/src/turn-observer.ts b/packages/shared/src/turn-observer.ts new file mode 100644 index 00000000..90a53082 --- /dev/null +++ b/packages/shared/src/turn-observer.ts @@ -0,0 +1,40 @@ +/** + * Shared turn-completion contract. + * + * Phase 6 of the Dream Daemon spike pulled `CompletedTurn` out of + * `@chamber/services/mindMemory/StructuredLogFormat` and into shared so that + * ChatService (the producer) and DailyLogWriter (the first consumer) depend + * on a single canonical shape rather than each maintaining its own copy. + * + * The corresponding observer interface is defined here too so any future + * observer (e.g. DreamDaemon's TurnRecorder, A2A task tracker) imports the + * same protocol from shared. + */ + +export type TurnStatus = 'completed' | 'aborted' | 'error'; + +export interface CompletedTurn { + readonly turnId: string; + readonly sessionId: string; + readonly model: string; + readonly status: TurnStatus; + readonly startedAt: string; + readonly endedAt: string; + readonly prompt: string; + readonly finalAssistantMessage: string; +} + +/** + * Observer notified when ChatService finishes a turn successfully. + * + * Contract: + * - Called once per turn that reached the SDK `done` state. NOT called when + * the turn was aborted by the user or errored out. + * - Implementations must not throw across the boundary in a way that blocks + * other observers — ChatService wraps each call in try/catch and forwards + * async failures to its `Logger.warn`. Observer latency must not gate + * subsequent turns or surface back into the streaming path. + */ +export interface TurnCompletionObserver { + onTurnCompleted(turn: CompletedTurn): void | Promise; +} diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 3c8a4bc3..81479cee 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -183,6 +183,13 @@ export interface AgentProfile { soul: AgentProfileFile; agentFiles: AgentProfileFile[]; needsRestart: boolean; + /** + * True when this mind has opted in to the dream-daemon working-memory + * consolidation pipeline (`.chamber.json` → + * `workingMemory.consolidation.enabled`). Defaults to false when no + * `.chamber.json` is present, matching `loadChamberMindConfig`. + */ + dreamDaemonEnabled: boolean; } export interface AgentProfileSaveRequest { diff --git a/scripts/ensure-native-abi.cjs b/scripts/ensure-native-abi.cjs new file mode 100644 index 00000000..383046ec --- /dev/null +++ b/scripts/ensure-native-abi.cjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +'use strict'; + +// Thin CLI wrapper around scripts/lib/ensure-native-abi.cjs. +// Usage: node scripts/ensure-native-abi.cjs +// Wired into npm `pretest` and `presmoke:desktop` lifecycle hooks. + +const { + readSentinel, + decideAction, + writeSentinel, + rebuild, + TARGETS, +} = require('./lib/ensure-native-abi.cjs'); + +const target = process.argv[2]; + +if (!target || !TARGETS.includes(target)) { + console.error( + `[ensure-native-abi] usage: node scripts/ensure-native-abi.cjs <${TARGETS.join('|')}>`, + ); + process.exit(2); +} + +// process.versions.modules is the V8 ABI version (NODE_MODULE_VERSION). It's +// what a native addon must be compiled against to load in the current runtime. +// Pinning the sentinel to {target, moduleVersion} catches Node-major upgrades +// that keep target=='node' but flip the ABI. +const moduleVersion = process.versions.modules; +const current = readSentinel(); +const action = decideAction({ target, current, moduleVersion }); + +if (action === 'noop') { + console.log( + `[ensure-native-abi] better-sqlite3 already built for ${target}:${moduleVersion} — skipping rebuild`, + ); + process.exit(0); +} + +console.log( + `[ensure-native-abi] better-sqlite3 ABI target=${target}:${moduleVersion}, current=${current ?? 'unknown'} — rebuilding...`, +); + +try { + rebuild(target); + writeSentinel({ target, moduleVersion }); + console.log(`[ensure-native-abi] better-sqlite3 now built for ${target}:${moduleVersion}`); +} catch (err) { + console.error(`[ensure-native-abi] rebuild failed: ${err && err.message ? err.message : err}`); + process.exit(1); +} diff --git a/scripts/lib/ensure-native-abi.cjs b/scripts/lib/ensure-native-abi.cjs new file mode 100644 index 00000000..6c5b4507 --- /dev/null +++ b/scripts/lib/ensure-native-abi.cjs @@ -0,0 +1,98 @@ +'use strict'; + +// Pure-logic core for the ensure-native-abi guard. +// +// Why this exists: +// better-sqlite3 is a native N-API module. Node 24 and Electron 41 ship +// different V8 ABIs (137 vs 145). electron-forge silently rebuilds the +// binary against the Electron ABI on `npm start` / `npm run package`, +// but vitest (Node) and Playwright `_electron.launch` (Electron) have no +// such hook — so a developer who switches between `npm test` and +// `npm run smoke:desktop` hits "Cannot read properties of undefined" +// crashes from the wrong-ABI .node file. +// +// What the sentinel records: +// `${target}:${moduleVersion}` — e.g. `node:137`, `electron:125`. Both +// axes must match the current runtime for the guard to short-circuit. +// Recording only the framework (`node` vs `electron`) is not enough: +// Node 23 and Node 24 share target=='node' but differ in MODULE_VERSION +// (145 vs 137), and a developer who upgrades Node would otherwise sail +// past the guard with a stale binary. (Caveat C-1 from the v0.60.0 +// ship review — this is the fix.) + +const fs = require('node:fs'); +const path = require('node:path'); +const { execSync } = require('node:child_process'); + +const TARGETS = Object.freeze(['node', 'electron']); + +const DEFAULT_SENTINEL_PATH = path.join( + 'node_modules', + 'better-sqlite3', + 'build', + 'Release', + '.abi-target', +); + +function readSentinel(sentinelPath = DEFAULT_SENTINEL_PATH) { + try { + return fs.readFileSync(sentinelPath, 'utf8').trim(); + } catch { + return null; + } +} + +function assertTarget(target) { + if (!TARGETS.includes(target)) { + throw new Error( + `ensure-native-abi: unknown target "${target}". Expected one of: ${TARGETS.join(', ')}`, + ); + } +} + +function assertModuleVersion(moduleVersion) { + // process.versions.modules is always a numeric string (e.g. "137"). A bad + // value here would corrupt the sentinel — fail loudly rather than write garbage. + if (typeof moduleVersion !== 'string' || !/^[0-9]+$/.test(moduleVersion)) { + throw new Error( + `ensure-native-abi: invalid moduleVersion ${JSON.stringify(moduleVersion)} — expected a numeric string from process.versions.modules`, + ); + } +} + +function sentinelValue(target, moduleVersion) { + return `${target}:${moduleVersion}`; +} + +function decideAction({ target, current, moduleVersion }) { + assertTarget(target); + assertModuleVersion(moduleVersion); + return current === sentinelValue(target, moduleVersion) ? 'noop' : 'rebuild'; +} + +function writeSentinel({ target, moduleVersion }, sentinelPath = DEFAULT_SENTINEL_PATH) { + assertTarget(target); + assertModuleVersion(moduleVersion); + fs.mkdirSync(path.dirname(sentinelPath), { recursive: true }); + fs.writeFileSync(sentinelPath, `${sentinelValue(target, moduleVersion)}\n`); +} + +function rebuildCommand(target) { + if (target === 'node') return 'npm rebuild better-sqlite3'; + if (target === 'electron') return 'npx --no-install electron-rebuild -f -w better-sqlite3'; + throw new Error(`ensure-native-abi: unknown target "${target}"`); +} + +function rebuild(target, runner = (cmd) => execSync(cmd, { stdio: 'inherit' })) { + runner(rebuildCommand(target)); +} + +module.exports = { + TARGETS, + DEFAULT_SENTINEL_PATH, + readSentinel, + decideAction, + writeSentinel, + rebuildCommand, + rebuild, +}; diff --git a/tests/e2e/electron/dream-daemon-bidir.spec.ts b/tests/e2e/electron/dream-daemon-bidir.spec.ts new file mode 100644 index 00000000..7b7fea3a --- /dev/null +++ b/tests/e2e/electron/dream-daemon-bidir.spec.ts @@ -0,0 +1,647 @@ +import { expect, test } from '@playwright/test'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { setTimeout as delay } from 'node:timers/promises'; + +import { findRendererPage, launchElectronApp, type LaunchedElectronApp } from './electronApp'; + +// v0.60.0 dream-daemon opt-in UX + bidirectional migration validation. +// +// One spec, four flows. Mirrors the spec in the validation request: +// Flow 1 — Genesis OFF (default Switch left untouched) +// Flow 2 — Genesis ON (Switch toggled before "That's my purpose") +// Flow 3 — Post-genesis OFF→ON via profile modal +// Flow 4 — Post-genesis ON→OFF via profile modal (rollback path) +// +// Real Copilot SDK is required for Genesis (SOUL.md generation) and chat +// turns. Skipped unless CHAMBER_E2E_LIVE_GENESIS=1, exactly like the +// existing genesis-ernest-chat smoke. CHAMBER_LOG_LEVEL=debug is set so +// MindMemoryService activate/release no-op debug lines are visible. + +const cdpPort = Number(process.env.CHAMBER_E2E_DREAM_DAEMON_CDP_PORT ?? 9351); +const liveGenesisEnabled = process.env.CHAMBER_E2E_LIVE_GENESIS === '1'; + +const offMindName = 'OffMind'; +const onMindName = 'OnMind'; +const offSlug = 'offmind'; +const onSlug = 'onmind'; + +interface FlowEvidence { + flow: string; + passed: boolean; + failures: string[]; + notes: string[]; +} + +test.describe('electron dream-daemon bidirectional toggle smoke', () => { + test.skip(!liveGenesisEnabled, 'Set CHAMBER_E2E_LIVE_GENESIS=1 to run the live dream-daemon smoke.'); + test.setTimeout(45 * 60_000); + + let app: LaunchedElectronApp | undefined; + let userDataPath = ''; + let genesisBasePath = ''; + let offMindPath = ''; + let onMindPath = ''; + const tempRoots: string[] = []; + + test.beforeAll(async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-dream-daemon-bidir-')); + userDataPath = path.join(root, 'user-data'); + genesisBasePath = path.join(root, 'agents'); + offMindPath = path.join(genesisBasePath, offSlug); + onMindPath = path.join(genesisBasePath, onSlug); + tempRoots.push(root); + + app = await launchElectronApp({ + cdpPort, + env: { + CHAMBER_E2E_USER_DATA: userDataPath, + CHAMBER_E2E_GENESIS_BASE_PATH: genesisBasePath, + CHAMBER_LOG_LEVEL: 'debug', + }, + }); + }); + + test.afterAll(async () => { + await app?.close(); + for (const root of tempRoots) { + await removeTempRoot(root); + } + }); + + test('OFF/ON Switch on Genesis + post-genesis bidirectional toggle with rollback', async () => { + const page = await findRendererPage(app?.browser, app?.logs ?? []); + await page.waitForLoadState('domcontentloaded'); + + const consoleMessages: Array<{ type: string; text: string }> = []; + page.on('console', (message) => { + consoleMessages.push({ type: message.type(), text: message.text() }); + }); + const renderedConsoleErrors = (): string[] => consoleMessages.filter((m) => m.type === 'error').map((m) => m.text); + + const evidence: FlowEvidence[] = []; + + // --------------------------------------------------------------------- + // Flow 1 — Genesis with daemon Switch left OFF (default) + // --------------------------------------------------------------------- + const flow1Snapshot = snapshotLogs(app); + const flow1: FlowEvidence = { flow: 'Flow 1 (Genesis OFF)', passed: true, failures: [], notes: [] }; + evidence.push(flow1); + + try { + await driveGenesisCustom(page, { name: offMindName, voiceDescription: 'a calm, methodical operator', purpose: 'Operator', toggleDaemon: false }); + await waitForMindByName(page, offMindName); + + // Disk: log.md exists and is empty (sentinel never seeded). .chamber.json + // is absent — MindScaffold writes it ONLY on opt-in. + const offLogPath = path.join(offMindPath, '.working-memory', 'log.md'); + const offChamberJson = path.join(offMindPath, '.chamber.json'); + const offLogContent = readFileOrNull(offLogPath); + flow1.notes.push(`log.md exists: ${offLogContent !== null}, length: ${offLogContent?.length ?? 'n/a'}`); + flow1.notes.push(`.chamber.json exists: ${fs.existsSync(offChamberJson)}`); + if (offLogContent === null) flow1.failures.push('log.md missing for OFF mind'); + if (offLogContent && offLogContent.length !== 0) { + flow1.failures.push(`log.md is not empty (${offLogContent.length} bytes) — sentinel may have been seeded`); + } + // chamber.json is allowed to be present with enabled=false OR absent. Per + // current implementation it is absent (and loadChamberMindConfig defaults + // to enabled=false). + if (fs.existsSync(offChamberJson)) { + const cfg = JSON.parse(fs.readFileSync(offChamberJson, 'utf-8')); + const enabled = cfg?.workingMemory?.consolidation?.enabled; + flow1.notes.push(`.chamber.json content: enabled=${enabled}`); + if (enabled === true) flow1.failures.push('.chamber.json says enabled=true after Genesis OFF'); + } + + // Dream db must NOT exist + const offDreamDb = path.join(offMindPath, '.working-memory', '.state', 'dream.db'); + flow1.notes.push(`dream.db exists: ${fs.existsSync(offDreamDb)}`); + if (fs.existsSync(offDreamDb)) flow1.failures.push('dream.db should not exist for OFF mind'); + + // Console: NO MindMemoryService activation logs reference the off mind path. + // Activate success path is silent so absence is the assertion. + const flow1Logs = logsSince(app, flow1Snapshot); + const offActivationLines = flow1Logs.filter((l) => /\[MindMemoryService\]/i.test(l) && l.includes(offMindPath)); + flow1.notes.push(`MindMemoryService log lines mentioning OFF mind path: ${offActivationLines.length}`); + if (offActivationLines.some((l) => /already activated|activate/i.test(l))) { + flow1.failures.push(`Unexpected MindMemoryService activate trace for OFF mind: ${offActivationLines.join(' | ')}`); + } + + // ARIA: open profile modal, switch should be OFF + const offMindContext = await getMindContext(page, offMindName); + flow1.notes.push(`OFF mindId: ${offMindContext.mindId}`); + await openProfileModal(page, offMindName); + const offSwitchInitial = await readSwitchAria(page); + flow1.notes.push(`Profile Switch initial aria-checked for OFF mind: ${offSwitchInitial}`); + if (offSwitchInitial !== 'false') { + flow1.failures.push(`Profile Switch aria-checked expected "false" but was "${offSwitchInitial}"`); + } + await closeProfileModal(page); + } catch (err) { + flow1.failures.push(`Exception: ${err instanceof Error ? err.stack ?? err.message : String(err)}`); + } finally { + flow1.passed = flow1.failures.length === 0; + reportFlow(flow1); + } + + // --------------------------------------------------------------------- + // Flow 2 — Genesis with daemon Switch toggled ON + // --------------------------------------------------------------------- + const flow2Snapshot = snapshotLogs(app); + const flow2: FlowEvidence = { flow: 'Flow 2 (Genesis ON)', passed: true, failures: [], notes: [] }; + evidence.push(flow2); + + try { + await driveGenesisCustom(page, { name: onMindName, voiceDescription: 'a precise, observant analyst', purpose: 'Analyst', toggleDaemon: true }); + await waitForMindByName(page, onMindName); + + // Disk: log.md must start with sentinel and .chamber.json must say enabled=true + const onLogPath = path.join(onMindPath, '.working-memory', 'log.md'); + const onChamberJson = path.join(onMindPath, '.chamber.json'); + const onLogContent = readFileOrNull(onLogPath); + flow2.notes.push(`log.md exists: ${onLogContent !== null}, starts with sentinel: ${(onLogContent ?? '').startsWith('')}`); + if (!(onLogContent ?? '').startsWith('')) { + flow2.failures.push('log.md missing sentinel after Genesis ON'); + } + if (!fs.existsSync(onChamberJson)) flow2.failures.push('.chamber.json missing after Genesis ON'); + else { + const cfg = JSON.parse(fs.readFileSync(onChamberJson, 'utf-8')); + flow2.notes.push(`.chamber.json: ${JSON.stringify(cfg)}`); + if (cfg?.workingMemory?.consolidation?.enabled !== true) { + flow2.failures.push(`.chamber.json consolidation.enabled expected true but was ${JSON.stringify(cfg?.workingMemory?.consolidation?.enabled)}`); + } + } + + // dream.db should be created on activation + const onDreamDb = path.join(onMindPath, '.working-memory', '.state', 'dream.db'); + flow2.notes.push(`dream.db exists after Genesis ON: ${fs.existsSync(onDreamDb)}`); + if (!fs.existsSync(onDreamDb)) flow2.failures.push('dream.db not created after Genesis ON activation'); + + // ARIA: profile Switch should be ON + const onMindContext = await getMindContext(page, onMindName); + flow2.notes.push(`ON mindId: ${onMindContext.mindId}`); + await openProfileModal(page, onMindName); + const onSwitchInitial = await readSwitchAria(page); + flow2.notes.push(`Profile Switch initial aria-checked for ON mind: ${onSwitchInitial}`); + if (onSwitchInitial !== 'true') flow2.failures.push(`Profile Switch aria-checked expected "true" but was "${onSwitchInitial}"`); + await closeProfileModal(page); + + // Send a chat turn via the IPC bridge — exercises real SDK + DailyLogWriter + const chatResult = await sendOneShotTurn(page, onMindContext.mindId, 'Reply with the single word: ACK'); + flow2.notes.push(`chat assistantText length=${chatResult.assistantText.length}, error=${chatResult.errorMessage || ''}, doneCount=${chatResult.doneCount}`); + if (chatResult.errorMessage) flow2.failures.push(`Chat turn failed: ${chatResult.errorMessage}`); + + // Allow DailyLogWriter to flush (write happens async after done event). + await delay(1500); + const onLogAfterChat = readFileOrNull(onLogPath) ?? ''; + flow2.notes.push(`log.md size after chat: ${onLogAfterChat.length}`); + flow2.notes.push(`log.md preview: ${preview(onLogAfterChat)}`); + if (!onLogAfterChat.startsWith('')) { + flow2.failures.push('log.md no longer starts with sentinel after chat turn (corrupted?)'); + } + if (!/\n### user\n/.test(onLogAfterChat)) flow2.failures.push('log.md missing "### user" frame marker'); + if (!/\n### assistant\n/.test(onLogAfterChat)) flow2.failures.push('log.md missing "### assistant" frame marker'); + + // No console errors emerged from the renderer during chat + const errs = renderedConsoleErrors(); + if (errs.length > 0) flow2.notes.push(`renderer console errors: ${errs.length} (first: ${errs[0].slice(0, 200)})`); + + // Capture relevant main-process log lines for the report. + const flow2Logs = logsSince(app, flow2Snapshot); + flow2.notes.push(`relevant main logs (sample): ${sampleRelevant(flow2Logs).join(' | ')}`); + } catch (err) { + flow2.failures.push(`Exception: ${err instanceof Error ? err.stack ?? err.message : String(err)}`); + } finally { + flow2.passed = flow2.failures.length === 0; + reportFlow(flow2); + } + + // --------------------------------------------------------------------- + // Flow 3 — toggle the OFF mind ON via profile modal + // --------------------------------------------------------------------- + const flow3Snapshot = snapshotLogs(app); + const flow3: FlowEvidence = { flow: 'Flow 3 (Post-genesis OFF→ON)', passed: true, failures: [], notes: [] }; + evidence.push(flow3); + + try { + const offMindContext = await getMindContext(page, offMindName); + // Set up a renderer-side mind:changed counter so we can attribute the reload sequence. + await page.evaluate((mindId) => { + const w = window as unknown as { + __chamberDreamTest?: { + unsubscribe?: () => void; + mindId?: string; + events: Array<{ ts: number; presentMindIds: string[] }>; + }; + }; + if (w.__chamberDreamTest?.unsubscribe) w.__chamberDreamTest.unsubscribe(); + const events: Array<{ ts: number; presentMindIds: string[] }> = []; + const unsubscribe = window.electronAPI.mind.onMindChanged((minds: { mindId: string }[]) => { + events.push({ ts: Date.now(), presentMindIds: minds.map((m) => m.mindId) }); + }); + w.__chamberDreamTest = { unsubscribe, mindId, events }; + }, offMindContext.mindId); + + await openProfileModal(page, offMindName); + const ariaBefore = await readSwitchAria(page); + flow3.notes.push(`Switch aria-checked before toggle: ${ariaBefore}`); + if (ariaBefore !== 'false') flow3.failures.push(`Pre-toggle aria-checked expected "false" but was "${ariaBefore}"`); + + const switchLocator = page.getByRole('switch', { name: 'Enable dream daemon' }); + await switchLocator.click(); + + // Wait for ARIA to flip — mind reload + Copilot client cold-start can + // take 30+ seconds, so give it 90s. + await expect(switchLocator).toHaveAttribute('aria-checked', 'true', { timeout: 90_000 }); + const ariaAfter = await switchLocator.getAttribute('aria-checked'); + flow3.notes.push(`Switch aria-checked after toggle: ${ariaAfter}`); + + // Disk: .chamber.json should now exist with enabled=true + const offChamberJson = path.join(offMindPath, '.chamber.json'); + const cfgRaw = readFileOrNull(offChamberJson); + flow3.notes.push(`.chamber.json after toggle: ${cfgRaw ?? ''}`); + if (!cfgRaw) flow3.failures.push('.chamber.json missing after toggle ON'); + else { + const cfg = JSON.parse(cfgRaw); + if (cfg?.workingMemory?.consolidation?.enabled !== true) { + flow3.failures.push(`.chamber.json enabled expected true but was ${JSON.stringify(cfg?.workingMemory?.consolidation?.enabled)}`); + } + } + + // mind reload sequence: 2 onMindChanged events (unloaded + loaded) + // First event should not contain the mindId; second event should. + const reloadEvents = await page.evaluate(() => { + const w = window as unknown as { __chamberDreamTest?: { events: Array<{ ts: number; presentMindIds: string[] }>; mindId: string } }; + return w.__chamberDreamTest ?? { events: [], mindId: '' }; + }); + flow3.notes.push(`onMindChanged event count: ${reloadEvents.events.length}`); + const sawUnloaded = reloadEvents.events.some((e) => !e.presentMindIds.includes(offMindContext.mindId)); + const sawLoaded = reloadEvents.events.some((e) => e.presentMindIds.includes(offMindContext.mindId)); + flow3.notes.push(`saw unloaded event: ${sawUnloaded}, saw loaded event: ${sawLoaded}`); + if (!sawUnloaded) flow3.failures.push('No mind:unloaded event observed (mind never absent from list during reload)'); + if (!sawLoaded) flow3.failures.push('No mind:loaded event observed after reload'); + + // Disk: dream.db should now exist + const offDreamDb = path.join(offMindPath, '.working-memory', '.state', 'dream.db'); + flow3.notes.push(`dream.db after toggle ON: ${fs.existsSync(offDreamDb)}`); + if (!fs.existsSync(offDreamDb)) flow3.failures.push('dream.db missing after toggle ON (MindMemoryService failed to activate?)'); + + await closeProfileModal(page); + + // Send a chat turn — must produce structured frames. + const chatResult = await sendOneShotTurn(page, offMindContext.mindId, 'Reply with the single word: GO'); + flow3.notes.push(`chat assistantText length=${chatResult.assistantText.length}, error=${chatResult.errorMessage || ''}`); + if (chatResult.errorMessage) flow3.failures.push(`Chat turn after toggle ON failed: ${chatResult.errorMessage}`); + + await delay(1500); + const offLogPath = path.join(offMindPath, '.working-memory', 'log.md'); + const offLogAfter = readFileOrNull(offLogPath) ?? ''; + flow3.notes.push(`log.md after chat (size=${offLogAfter.length}): ${preview(offLogAfter)}`); + if (!offLogAfter.startsWith('')) { + flow3.failures.push('log.md missing sentinel after toggle ON + chat (eager migrateIfNeeded should have seeded it)'); + } + if (!/\n### user\n/.test(offLogAfter)) flow3.failures.push('log.md missing "### user" frame after toggle ON + chat'); + if (!/\n### assistant\n/.test(offLogAfter)) flow3.failures.push('log.md missing "### assistant" frame after toggle ON + chat'); + + const flow3Logs = logsSince(app, flow3Snapshot); + flow3.notes.push(`relevant main logs (sample): ${sampleRelevant(flow3Logs).join(' | ')}`); + } catch (err) { + flow3.failures.push(`Exception: ${err instanceof Error ? err.stack ?? err.message : String(err)}`); + } finally { + flow3.passed = flow3.failures.length === 0; + reportFlow(flow3); + } + + // --------------------------------------------------------------------- + // Flow 4 — toggle the ON mind OFF via profile modal (rollback path) + // --------------------------------------------------------------------- + const flow4Snapshot = snapshotLogs(app); + const flow4: FlowEvidence = { flow: 'Flow 4 (Post-genesis ON→OFF rollback)', passed: true, failures: [], notes: [] }; + evidence.push(flow4); + + try { + const onMindContext = await getMindContext(page, onMindName); + + // Reset onMindChanged counter for this mind. + await page.evaluate((mindId) => { + const w = window as unknown as { + __chamberDreamTest?: { + unsubscribe?: () => void; + mindId?: string; + events: Array<{ ts: number; presentMindIds: string[] }>; + }; + }; + if (w.__chamberDreamTest?.unsubscribe) w.__chamberDreamTest.unsubscribe(); + const events: Array<{ ts: number; presentMindIds: string[] }> = []; + const unsubscribe = window.electronAPI.mind.onMindChanged((minds: { mindId: string }[]) => { + events.push({ ts: Date.now(), presentMindIds: minds.map((m) => m.mindId) }); + }); + w.__chamberDreamTest = { unsubscribe, mindId, events }; + }, onMindContext.mindId); + + await openProfileModal(page, onMindName); + const ariaBefore = await readSwitchAria(page); + flow4.notes.push(`Switch aria-checked before toggle: ${ariaBefore}`); + if (ariaBefore !== 'true') flow4.failures.push(`Pre-toggle aria-checked expected "true" but was "${ariaBefore}"`); + + const switchLocator = page.getByRole('switch', { name: 'Enable dream daemon' }); + await switchLocator.click(); + await expect(switchLocator).toHaveAttribute('aria-checked', 'false', { timeout: 90_000 }); + const ariaAfter = await switchLocator.getAttribute('aria-checked'); + flow4.notes.push(`Switch aria-checked after toggle: ${ariaAfter}`); + + // Disk: .chamber.json now enabled=false + const onChamberJson = path.join(onMindPath, '.chamber.json'); + const cfgRaw = readFileOrNull(onChamberJson); + flow4.notes.push(`.chamber.json after rollback: ${cfgRaw ?? ''}`); + if (!cfgRaw) flow4.failures.push('.chamber.json removed unexpectedly'); + else { + const cfg = JSON.parse(cfgRaw); + if (cfg?.workingMemory?.consolidation?.enabled !== false) { + flow4.failures.push(`.chamber.json enabled expected false but was ${JSON.stringify(cfg?.workingMemory?.consolidation?.enabled)}`); + } + } + + // Console: rollback log line + const flow4Logs = logsSince(app, flow4Snapshot); + const rollbackLogLines = flow4Logs.filter((l) => /\[rollbackToUnstructured\]/.test(l)); + flow4.notes.push(`rollback log lines: ${rollbackLogLines.length}`); + flow4.notes.push(`rollback log preview: ${rollbackLogLines.map(preview).join(' | ')}`); + if (!rollbackLogLines.some((l) => /converted \d+ frame\(s\)/.test(l))) { + flow4.failures.push('Expected "[rollbackToUnstructured] converted N frame(s)" log line not found'); + } + + // log.md no longer has sentinel; should have rendered turn markdown + const onLogPath = path.join(onMindPath, '.working-memory', 'log.md'); + const onLogAfterRollback = readFileOrNull(onLogPath) ?? ''; + flow4.notes.push(`log.md after rollback (size=${onLogAfterRollback.length}): ${preview(onLogAfterRollback)}`); + if (onLogAfterRollback.includes('')) { + flow4.failures.push('log.md still contains sentinel after rollback'); + } + if (!/^## \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*— turn .*\(/m.test(onLogAfterRollback)) { + flow4.failures.push('log.md missing rendered turn header (## — turn ())'); + } + if (!/\*\*User\*\*:/.test(onLogAfterRollback)) flow4.failures.push('log.md missing **User**: block'); + if (!/\*\*Assistant\*\*:/.test(onLogAfterRollback)) flow4.failures.push('log.md missing **Assistant**: block'); + + // legacy file should not exist (Genesis-time seed never had legacy content) + const legacyPath = path.join(onMindPath, '.working-memory', 'log.legacy.md'); + flow4.notes.push(`log.legacy.md exists after rollback: ${fs.existsSync(legacyPath)}`); + if (fs.existsSync(legacyPath)) flow4.failures.push('log.legacy.md should be removed (or absent) after rollback'); + + // mind reload events + const reloadEvents = await page.evaluate(() => { + const w = window as unknown as { __chamberDreamTest?: { events: Array<{ ts: number; presentMindIds: string[] }>; mindId: string } }; + return w.__chamberDreamTest ?? { events: [], mindId: '' }; + }); + flow4.notes.push(`onMindChanged event count: ${reloadEvents.events.length}`); + const sawUnloaded = reloadEvents.events.some((e) => !e.presentMindIds.includes(onMindContext.mindId)); + const sawLoaded = reloadEvents.events.some((e) => e.presentMindIds.includes(onMindContext.mindId)); + flow4.notes.push(`saw unloaded event: ${sawUnloaded}, saw loaded event: ${sawLoaded}`); + if (!sawUnloaded) flow4.failures.push('No mind:unloaded event observed during rollback toggle'); + if (!sawLoaded) flow4.failures.push('No mind:loaded event observed after rollback toggle'); + + await closeProfileModal(page); + + // Follow-up turn: appends unstructured to log.md (no sentinel re-introduced). + const chatResult = await sendOneShotTurn(page, onMindContext.mindId, 'Reply with the single word: STOP'); + flow4.notes.push(`follow-up chat assistantText length=${chatResult.assistantText.length}, error=${chatResult.errorMessage || ''}`); + if (chatResult.errorMessage) flow4.failures.push(`Follow-up chat turn failed: ${chatResult.errorMessage}`); + await delay(1500); + + const onLogAfterFollowUp = readFileOrNull(onLogPath) ?? ''; + flow4.notes.push(`log.md after follow-up (size=${onLogAfterFollowUp.length}): ${preview(onLogAfterFollowUp)}`); + if (onLogAfterFollowUp.includes('')) { + flow4.failures.push('Follow-up turn re-introduced sentinel — DailyLogWriter not torn down'); + } + // Note: post-rollback the mind is in opted-out mode. A turn through the + // chat IPC does NOT write to log.md (no observer is attached). We simply + // verify the file did not regress. + + flow4.notes.push(`relevant main logs (sample): ${sampleRelevant(flow4Logs).join(' | ')}`); + } catch (err) { + flow4.failures.push(`Exception: ${err instanceof Error ? err.stack ?? err.message : String(err)}`); + } finally { + flow4.passed = flow4.failures.length === 0; + reportFlow(flow4); + } + + // --------------------------------------------------------------------- + // Final consolidated assertion + // --------------------------------------------------------------------- + const failedFlows = evidence.filter((e) => !e.passed); + if (failedFlows.length > 0) { + const summary = failedFlows + .map((e) => `${e.flow}\n failures:\n - ${e.failures.join('\n - ')}\n notes:\n - ${e.notes.join('\n - ')}`) + .join('\n\n'); + throw new Error(`Dream daemon validation failed:\n${summary}`); + } + }); +}); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function snapshotLogs(app: LaunchedElectronApp | undefined): number { + return app?.logs.length ?? 0; +} + +function logsSince(app: LaunchedElectronApp | undefined, start: number): string[] { + return (app?.logs ?? []).slice(start); +} + +function readFileOrNull(p: string): string | null { + try { + return fs.readFileSync(p, 'utf-8'); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw err; + } +} + +function preview(s: string, max = 240): string { + const flat = s.replace(/\s+/g, ' ').trim(); + return flat.length <= max ? flat : flat.slice(0, max) + '…'; +} + +function sampleRelevant(lines: string[]): string[] { + return lines + .filter((l) => + /\[MindMemoryService\]|\[MindManager\]|\[rollbackToUnstructured\]|\[chamberMindConfig\]|\[DailyLogWriter\]|mind:loaded|mind:unloaded|mindMemory/i.test(l), + ) + .slice(0, 8) + .map((l) => l.replace(/\r?\n/g, ' ').slice(0, 200)); +} + +function reportFlow(f: FlowEvidence): void { + const status = f.passed ? 'PASS' : 'FAIL'; + + console.log(`[dream-daemon-bidir] ${status} — ${f.flow}`); + for (const n of f.notes) console.log(` · ${n}`); + for (const x of f.failures) console.log(` ✗ ${x}`); +} + +interface DriveGenesisOptions { + name: string; + voiceDescription: string; + purpose: string; + toggleDaemon: boolean; +} + +async function driveGenesisCustom(page: Awaited>, opts: DriveGenesisOptions): Promise { + // The genesis wizard is reachable from three different entry states: + // 1. We're already in the wizard (VoidScreen) — "Begin" button is visible. + // 2. We're on the LandingScreen (first launch, no minds) — "New Agent" button is visible. + // 3. We're in the main app (at least one mind exists) — the sidebar shows "Add Agent", + // which dispatches SHOW_LANDING and brings us to state (2). + const beginButton = page.getByRole('button', { name: 'Begin', exact: true }); + const newAgentButton = page.getByRole('button', { name: /New Agent/i }); + + const beginVisible = await beginButton.isVisible().catch(() => false); + if (!beginVisible) { + const newAgentVisible = await newAgentButton.isVisible().catch(() => false); + if (!newAgentVisible) { + // State (3) — sidebar route. + const addAgentButton = page.getByRole('button', { name: /Add Agent/i }); + await addAgentButton.waitFor({ state: 'visible', timeout: 30_000 }); + await addAgentButton.click(); + await newAgentButton.waitFor({ state: 'visible', timeout: 10_000 }); + } + await newAgentButton.click(); + } + await beginButton.waitFor({ state: 'visible', timeout: 30_000 }); + await beginButton.click(); + + // VoiceScreen — pick "Someone else..." then enter name + backstory. + await page.getByRole('button', { name: /Someone else/i }).click(); + await page.getByPlaceholder('e.g. Tony Stark, Moneypenny, Gandalf...').fill(opts.name); + await page.getByPlaceholder(/Era, source material/).fill(opts.voiceDescription); + await page.getByRole('button', { name: /Research this voice/i }).click(); + await expect(page.getByLabel('Research brief')).toHaveValue(/.+/, { timeout: 60_000 }); + await page.getByRole('button', { name: /Continue to purpose/i }).click(); + + // RoleScreen — pick "Something else..." then type purpose. Optionally toggle the dream-daemon Switch BEFORE submit. + await page.getByRole('button', { name: /Something else/i }).click(); + await page.getByPlaceholder(/Creative Director, Debate Coach/).fill(opts.purpose); + + if (opts.toggleDaemon) { + const daemonSwitch = page.getByRole('switch', { name: 'Enable dream daemon' }); + await expect(daemonSwitch).toHaveAttribute('aria-checked', 'false'); + await daemonSwitch.click(); + await expect(daemonSwitch).toHaveAttribute('aria-checked', 'true'); + } + + await page.getByRole('button', { name: /That's my purpose/i }).click(); + + // BootScreen → done. The chat input becomes visible only once Genesis completes + // and the new mind is selected. Wait long enough for SOUL generation + capability bootstrap. + await expect(page.getByPlaceholder('Message your agent… (paste an image to attach)')).toBeEnabled({ timeout: 10 * 60_000 }); +} + +async function waitForMindByName(page: Awaited>, name: string): Promise { + await expect.poll( + async () => + await page.evaluate(async (target) => { + const minds = await window.electronAPI.mind.list(); + return minds.some((m) => m.identity.name === target); + }, name), + { timeout: 30_000 }, + ).toBe(true); +} + +interface MindCtx { + mindId: string; + mindPath: string; +} + +async function getMindContext(page: Awaited>, name: string): Promise { + return await page.evaluate(async (target) => { + const minds = await window.electronAPI.mind.list(); + const mind = minds.find((m) => m.identity.name === target); + if (!mind) throw new Error(`Mind ${target} not found`); + return { mindId: mind.mindId, mindPath: mind.mindPath }; + }, name); +} + +async function openProfileModal(page: Awaited>, name: string): Promise { + const trigger = page.getByRole('button', { name: `Edit ${name} profile`, exact: true }); + // The trigger is hover-revealed (opacity-0). force:true bypasses the visibility check. + await trigger.click({ force: true }); + await expect(page.getByRole('dialog').getByText('Agent profile')).toBeVisible({ timeout: 10_000 }); +} + +async function readSwitchAria(page: Awaited>): Promise { + return await page.getByRole('switch', { name: 'Enable dream daemon' }).getAttribute('aria-checked'); +} + +async function closeProfileModal(page: Awaited>): Promise { + // The dialog has two "Close" elements (footer text button + Radix icon button with aria-label="Close"). + // Press Escape — Radix Dialog dismisses on Escape and avoids selector ambiguity. + await page.keyboard.press('Escape'); + await expect(page.getByRole('dialog')).toBeHidden({ timeout: 5_000 }).catch(() => undefined); +} + +interface ChatResult { + assistantText: string; + errorMessage: string; + doneCount: number; +} + +async function sendOneShotTurn( + page: Awaited>, + mindId: string, + prompt: string, +): Promise { + return await page.evaluate(async ({ mindId: id, prompt: text }) => { + const messageId = `dream-daemon-${Date.now()}-${Math.random().toString(36).slice(2)}`; + let assistantText = ''; + let errorMessage = ''; + let doneCount = 0; + let resolveTerminal: () => void = () => undefined; + const terminal = new Promise((resolve) => { resolveTerminal = resolve; }); + const unsubscribe = window.electronAPI.chat.onEvent((eventMindId, eventMessageId, event) => { + if (eventMindId !== id || eventMessageId !== messageId) return; + if (event.type === 'chunk' || event.type === 'message_final') { + assistantText += (event as { content?: string }).content ?? ''; + } + if (event.type === 'error') { + errorMessage = (event as { message?: string }).message ?? 'unknown error'; + resolveTerminal(); + } + if (event.type === 'done') { + doneCount += 1; + resolveTerminal(); + } + }); + try { + const send = window.electronAPI.chat.send(id, text, messageId); + const timeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Chat turn timed out after 180s')), 180_000); + }); + await Promise.race([Promise.all([send, terminal]), timeout]); + } catch (err) { + errorMessage = err instanceof Error ? err.message : String(err); + } finally { + unsubscribe(); + } + return { assistantText, errorMessage, doneCount }; + }, { mindId, prompt }); +} + +async function removeTempRoot(root: string): Promise { + for (let attempt = 0; attempt < 10; attempt += 1) { + try { + fs.rmSync(root, { recursive: true, force: true }); + return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EPERM' || attempt === 9) { + + console.warn(`[dream-daemon-bidir] Failed to remove temp root ${root}:`, error); + return; + } + await delay(250); + } + } +} diff --git a/tests/integration/mindMemory.composition.test.ts b/tests/integration/mindMemory.composition.test.ts new file mode 100644 index 00000000..ba4d0d8c --- /dev/null +++ b/tests/integration/mindMemory.composition.test.ts @@ -0,0 +1,257 @@ +/** + * Phase 13 composition smoke for MindMemoryService. + * + * Goal: catch native-module / DI failures cheaply, without launching + * Electron. Builds the full service graph against a tmpdir mindPath using: + * - real `createInternalScheduler` + * - real `createMindMemoryVault` / `createMindArchiveStore` + * - real `defaultConfigReader` (.chamber.json on disk) + * - real better-sqlite3 dbFactory at /.working-memory/.state/dream.db + * - a fake daemon factory + chat-observer registry (the real DreamDaemon + * needs a CopilotClient, which needs Electron — out of scope for a + * non-Electron smoke). + * + * The expensive integration that exercises the real CopilotLLMClient lives + * in Phase 14. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { + createMindMemoryService, + createInternalScheduler, + createMindMemoryVault, + createMindArchiveStore, + defaultConfigReader, + dreamDbPath, + type DreamDaemon, + type DreamRunResult, + type MindMemoryService, + type ChatObserverRegistry, + type DaemonFactoryOptions, +} from '@chamber/services'; +import type { TurnCompletionObserver } from '@chamber/shared'; + +const runtimeRequire = createRequire(__filename); + +function makeFakeDaemon(): DreamDaemon & { closeCalls: number } { + let closeCalls = 0; + const daemon: DreamDaemon = { + async run(): Promise { + return { status: 'skipped', reason: 'no-turns' }; + }, + async forceRun(): Promise { + return { status: 'skipped', reason: 'no-turns' }; + }, + getStatus() { + return { phase: 'idle', locked: false, lastRunAt: null, lastResult: null }; + }, + notifyTurnCompleted() { + /* no-op */ + }, + async close(): Promise { + closeCalls += 1; + }, + }; + return Object.defineProperty(daemon, 'closeCalls', { + get: () => closeCalls, + enumerable: true, + }) as DreamDaemon & { closeCalls: number }; +} + +function makeFakeChatRegistry(): ChatObserverRegistry & { + readonly observers: TurnCompletionObserver[]; +} { + const observers: TurnCompletionObserver[] = []; + return { + addObserver(o: TurnCompletionObserver): void { + observers.push(o); + }, + removeObserver(o: TurnCompletionObserver): void { + const i = observers.indexOf(o); + if (i !== -1) observers.splice(i, 1); + }, + get observers() { + return observers; + }, + }; +} + +function writeChamberConfig(mindPath: string, enabled: boolean): void { + const cfg = { + workingMemory: { + consolidation: { + enabled, + cron: '0 3 * * *', + lastKTurns: 10, + perTurnMaxBytes: 2048, + memoryMaxBytes: 8192, + }, + }, + }; + writeFileSync(path.join(mindPath, '.chamber.json'), JSON.stringify(cfg, null, 2), 'utf-8'); +} + +interface Harness { + readonly mindPath: string; + readonly mindId: string; + readonly service: MindMemoryService; + readonly scheduler: ReturnType; + readonly chat: ReturnType; + readonly daemonFactoryCalls: { count: number }; + readonly openDbs: import('better-sqlite3').Database[]; + cleanup(): Promise; +} + +function buildHarness(mindRoot: string, mindId: string): Harness { + const mindPath = path.join(mindRoot, mindId); + mkdirSync(mindPath, { recursive: true }); + + const scheduler = createInternalScheduler(); + const chat = makeFakeChatRegistry(); + const daemonFactoryCalls = { count: 0 }; + const openDbs: import('better-sqlite3').Database[] = []; + + const Database = runtimeRequire('better-sqlite3') as typeof import('better-sqlite3'); + + const service = createMindMemoryService({ + scheduler, + chatService: chat, + configReader: defaultConfigReader, + dbFactory: (dbPath: string) => { + // Mirror the production wiring contract: ensure the parent dir exists + // before opening. dreamDbPath is /.working-memory/.state/dream.db + mkdirSync(path.dirname(dbPath), { recursive: true }); + const db = new Database(dbPath); + openDbs.push(db); + return db; + }, + vaultFactory: createMindMemoryVault, + archiveFactory: createMindArchiveStore, + daemonFactory: (_opts: DaemonFactoryOptions) => { + void _opts; + daemonFactoryCalls.count += 1; + return makeFakeDaemon(); + }, + }); + + return { + mindPath, + mindId, + service, + scheduler, + chat, + daemonFactoryCalls, + openDbs, + async cleanup() { + try { + await service.close(); + } catch { + /* noop */ + } + try { + scheduler.close(); + } catch { + /* noop */ + } + // Final guard — anything still open from a half-failed activation. + for (const db of openDbs) { + try { + if (db.open) db.close(); + } catch { + /* noop */ + } + } + }, + }; +} + +describe('MindMemoryService composition (Phase 13 smoke)', () => { + let mindRoot: string; + let harness: Harness | null = null; + + beforeEach(() => { + mindRoot = mkdtempSync(path.join(tmpdir(), 'chamber-mindmem-smoke-')); + }); + + afterEach(async () => { + if (harness) { + await harness.cleanup(); + harness = null; + } + rmSync(mindRoot, { recursive: true, force: true }); + }); + + it('loads better-sqlite3 native module without throwing', () => { + expect(() => runtimeRequire('better-sqlite3')).not.toThrow(); + }); + + it('activateMind on a disabled config is a no-op (no db, no observer, no scheduler entry)', async () => { + harness = buildHarness(mindRoot, 'mind-disabled'); + writeChamberConfig(harness.mindPath, false); + + await harness.service.activateMind(harness.mindId, harness.mindPath); + + expect(existsSync(dreamDbPath(harness.mindPath))).toBe(false); + expect(harness.chat.observers).toHaveLength(0); + expect(harness.scheduler.list().size).toBe(0); + expect(harness.daemonFactoryCalls.count).toBe(0); + }); + + it('activateMind on an enabled config wires db, observer, and scheduler entry; releaseMind tears them all down', async () => { + harness = buildHarness(mindRoot, 'mind-enabled'); + writeChamberConfig(harness.mindPath, true); + + await harness.service.activateMind(harness.mindId, harness.mindPath); + + const dbFile = dreamDbPath(harness.mindPath); + expect(existsSync(dbFile)).toBe(true); + expect(harness.chat.observers).toHaveLength(1); + const entries = harness.scheduler.list(); + expect(entries.size).toBe(1); + expect(entries.get(harness.mindId)).toBe('0 3 * * *'); + expect(harness.daemonFactoryCalls.count).toBe(1); + + await harness.service.releaseMind(harness.mindId); + + expect(harness.chat.observers).toHaveLength(0); + expect(harness.scheduler.list().size).toBe(0); + // dream.db file persists on disk (state survives mind release), but the + // handle should be closed — `db.open` flips to false on close. + expect(harness.openDbs).toHaveLength(1); + expect(harness.openDbs[0]?.open).toBe(false); + }); + + it('activateMind is idempotent and close() is safe when called multiple times', async () => { + harness = buildHarness(mindRoot, 'mind-idem'); + writeChamberConfig(harness.mindPath, true); + + await harness.service.activateMind(harness.mindId, harness.mindPath); + await harness.service.activateMind(harness.mindId, harness.mindPath); + + expect(harness.scheduler.list().size).toBe(1); + expect(harness.daemonFactoryCalls.count).toBe(1); + + await harness.service.close(); + await expect(harness.service.close()).resolves.toBeUndefined(); + expect(harness.scheduler.list().size).toBe(0); + expect(harness.openDbs[0]?.open).toBe(false); + }); + + it('uses the production dream.db path under /.working-memory/.state/', async () => { + harness = buildHarness(mindRoot, 'mind-path'); + writeChamberConfig(harness.mindPath, true); + + await harness.service.activateMind(harness.mindId, harness.mindPath); + + const expected = path.join(harness.mindPath, '.working-memory', '.state', 'dream.db'); + expect(dreamDbPath(harness.mindPath)).toBe(expected); + expect(existsSync(expected)).toBe(true); + }); +}); + +// Reference vi to keep the import-set stable across future edits even if all +// existing tests stop using it directly. +void vi; diff --git a/tests/integration/mindMemory.integration.test.ts b/tests/integration/mindMemory.integration.test.ts new file mode 100644 index 00000000..3ae39982 --- /dev/null +++ b/tests/integration/mindMemory.integration.test.ts @@ -0,0 +1,477 @@ +/** + * Phase 14 — Dream Daemon end-to-end integration. + * + * Builds the full per-mind consolidation graph against a real on-disk + * mind directory and a real better-sqlite3 dream.db, then drives a + * 7-day simulated run with a deterministic in-test LLM client. The + * test substitutes the LLM client (the only seam that would otherwise + * require Electron / network) — every other collaborator is the real + * production implementation. + * + * Properties verified (matches the Phase 14 deliverables in plan.md): + * + * 1. memory.md exists at the end and stays under `memoryMaxBytes` (8192). + * 2. weekly/.md rollup is materialised after 7 daily ticks. + * 3. log.md is pruned (sentinel preserved + only post-cutoff turns survive). + * 4. archive/consolidated/ accumulates one file per source turn. + * 5. Re-running the daemon when no new turns have arrived is skipped + * with a non-success result (no double-processing). + * 6. Two parallel `daemon.run()` calls produce exactly one cycle — + * the second one short-circuits with a `locked` skip. + * 7. dream_state.last_consolidated_turn_id advances monotonically. + * 8. A turn appended between snapshot and prune survives in log.md. + */ + +import { + describe, + it, + expect, + beforeEach, + afterEach, +} from 'vitest'; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs'; +import { readdir } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { createRequire } from 'node:module'; + +import { + createMindMemoryService, + createInternalScheduler, + createMindMemoryVault, + createMindArchiveStore, + createDreamDaemon, + defaultConfigReader, + migrate as migrateDreamDb, + readState, + listRuns, + type DaemonFactoryOptions, + type DreamDaemon, + type DreamDaemonConfig, + type LLMClient, + type SynthesizeRequest, + type MindMemoryService, + type ChatObserverRegistry, +} from '@chamber/services'; +import type { TurnCompletionObserver, CompletedTurn } from '@chamber/shared'; + +const runtimeRequire = createRequire(__filename); + +// --------------------------------------------------------------------------- +// Test-local LLM client +// --------------------------------------------------------------------------- + +/** + * In-test LLMClient. The default response is the canonical daily-log + * vault delta `extractFromLog` accepts (header `## HH:MM:SS`, content + * lines `**[type]** body`). `pauseNext()` lets a single test arrange + * for the LLM call to suspend so a turn can be appended mid-cycle. + */ +interface TestLLMController { + readonly client: LLMClient; + readonly calls: SynthesizeRequest[]; + pauseNext(): () => void; +} + +function makeTestLLMController(canned: string): TestLLMController { + const calls: SynthesizeRequest[] = []; + let pendingPause: { release: () => void; promise: Promise } | null = null; + + const client: LLMClient = { + async synthesize(req: SynthesizeRequest): Promise { + calls.push(req); + if (pendingPause) { + const p = pendingPause; + pendingPause = null; + await p.promise; + } + return canned; + }, + }; + + return { + client, + calls, + pauseNext(): () => void { + let release: () => void = () => { + /* placeholder */ + }; + const promise = new Promise((res) => { + release = res; + }); + pendingPause = { release, promise }; + return release; + }, + }; +} + +const CANNED_VAULT_DELTA = [ + '## 12:00:00', + '**[user-prompt]** I prefer kebab-case file names.', + '**[user-prompt]** Always follow TDD when implementing features.', + '**[user-prompt]** Never skip required testing steps before claiming done.', + '', +].join('\n'); + +// --------------------------------------------------------------------------- +// Fakes (only ChatService — everything else is real) +// --------------------------------------------------------------------------- + +function makeChatRegistry(): ChatObserverRegistry & { + readonly observers: TurnCompletionObserver[]; +} { + const observers: TurnCompletionObserver[] = []; + return { + addObserver(o: TurnCompletionObserver): void { + observers.push(o); + }, + removeObserver(o: TurnCompletionObserver): void { + const i = observers.indexOf(o); + if (i !== -1) observers.splice(i, 1); + }, + get observers() { + return observers; + }, + }; +} + +// --------------------------------------------------------------------------- +// Harness +// --------------------------------------------------------------------------- + +const MIND_ID = 'mind-integration'; + +interface Harness { + readonly mindRoot: string; + readonly mindPath: string; + readonly service: MindMemoryService; + readonly scheduler: ReturnType; + readonly chat: ReturnType; + readonly llm: TestLLMController; + readonly openDbs: import('better-sqlite3').Database[]; + daemon(): DreamDaemon; + db(): import('better-sqlite3').Database; + cleanup(): Promise; +} + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +const INTEGRATION_CONFIG: DreamDaemonConfig = { + memoryMaxBytes: 8192, + llmTimeoutMs: 60_000, + lockTtlMs: 300_000, + minTurnsBetweenRuns: 1, + minDailyIntervalMs: 0, + weeklyRollupAfterDailies: 7, + monthlyRollupAfterWeeklies: 4, + weeklyMinIntervalMs: 7 * MS_PER_DAY, + monthlyMinIntervalMs: 30 * MS_PER_DAY, +}; + +function writeChamberConfig(mindPath: string): void { + const cfg = { + workingMemory: { + consolidation: { + enabled: true, + cron: '0 3 * * *', + lastKTurns: 10, + perTurnMaxBytes: 2048, + memoryMaxBytes: 8192, + }, + }, + }; + writeFileSync(path.join(mindPath, '.chamber.json'), JSON.stringify(cfg, null, 2), 'utf-8'); +} + +function buildHarness(): Harness { + const mindRoot = mkdtempSync(path.join(tmpdir(), 'chamber-mindmem-int-')); + const mindPath = path.join(mindRoot, MIND_ID); + mkdirSync(mindPath, { recursive: true }); + writeChamberConfig(mindPath); + + const scheduler = createInternalScheduler(); + const chat = makeChatRegistry(); + const llm = makeTestLLMController(CANNED_VAULT_DELTA); + const openDbs: import('better-sqlite3').Database[] = []; + const Database = runtimeRequire('better-sqlite3') as typeof import('better-sqlite3'); + + let capturedDaemon: DreamDaemon | null = null; + let capturedDb: import('better-sqlite3').Database | null = null; + + const service = createMindMemoryService({ + scheduler, + chatService: chat, + configReader: defaultConfigReader, + dbFactory: (dbPath: string) => { + mkdirSync(path.dirname(dbPath), { recursive: true }); + const db = new Database(dbPath); + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + migrateDreamDb(db); + openDbs.push(db); + capturedDb = db; + return db; + }, + vaultFactory: createMindMemoryVault, + archiveFactory: createMindArchiveStore, + daemonFactory: (opts: DaemonFactoryOptions) => { + // Real daemon, real vault/archive/db — only the LLM is substituted. + const daemon = createDreamDaemon({ + mindId: opts.mindId, + mindPath: opts.mindPath, + vault: opts.vault, + archiveStore: opts.archive, + db: opts.db, + llmClient: llm.client, + config: INTEGRATION_CONFIG, + }); + capturedDaemon = daemon; + return daemon; + }, + }); + + return { + mindRoot, + mindPath, + service, + scheduler, + chat, + llm, + openDbs, + daemon(): DreamDaemon { + if (!capturedDaemon) throw new Error('daemon not yet constructed (call activateMind first)'); + return capturedDaemon; + }, + db(): import('better-sqlite3').Database { + if (!capturedDb) throw new Error('db not yet opened (call activateMind first)'); + return capturedDb; + }, + async cleanup() { + try { + await service.close(); + } catch { + /* noop */ + } + try { + scheduler.close(); + } catch { + /* noop */ + } + for (const db of openDbs) { + try { + if (db.open) db.close(); + } catch { + /* noop */ + } + } + rmSync(mindRoot, { recursive: true, force: true }); + }, + }; +} + +function makeTurn( + dayIndex: number, + withinDay: number, + prompt: string, + assistant: string, +): CompletedTurn { + const turnId = `t-d${String(dayIndex).padStart(2, '0')}-${String(withinDay).padStart(2, '0')}`; + const startedAt = new Date(Date.now()).toISOString(); + return { + turnId, + sessionId: `s-day-${dayIndex}`, + model: 'gpt-test', + status: 'completed', + startedAt, + endedAt: startedAt, + prompt, + finalAssistantMessage: assistant, + }; +} + +const DAILY_PROMPTS: ReadonlyArray = [ + ['I prefer kebab-case file names.', 'Got it — I will use kebab-case.'], + ['Always follow TDD when implementing features.', 'Acknowledged — TDD by default.'], + ['Never skip required testing steps before claiming done.', 'Understood.'], + ['Use Tailwind for styling.', 'OK — Tailwind it is.'], +]; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('Dream Daemon — multi-day integration', () => { + let harness: Harness | null = null; + + beforeEach(() => { + harness = null; + }); + + afterEach(async () => { + if (harness) { + await harness.cleanup(); + harness = null; + } + }); + + it('consolidates 7 simulated days end-to-end with bounded memory.md, weekly rollup, pruned log, and monotonic state', async () => { + harness = buildHarness(); + await harness.service.activateMind(MIND_ID, harness.mindPath); + + const observer = harness.chat.observers[0]; + expect(observer).toBeDefined(); + const db = harness.db(); + + let prevLastTurnId: string | null = null; + const lastTurnIdsByDay: string[] = []; + + // Simulate 7 days. Each day: append several turns, then trigger one + // consolidation cycle via the scheduler entry (the same fn croner + // would invoke at 03:00). + for (let day = 1; day <= 7; day++) { + for (let i = 0; i < DAILY_PROMPTS.length; i++) { + const [p, a] = DAILY_PROMPTS[i]!; + await observer!.onTurnCompleted(makeTurn(day, i, p, a)); + } + + // Activity counter must have advanced (proves the onTurnRecorded + // hook is wired into DailyLogWriter). + expect(readState(db).turnsSinceLastRun).toBe(DAILY_PROMPTS.length); + + await harness.scheduler.runNow(MIND_ID); + + const state = readState(db); + expect(state.lastConsolidatedTurnId).not.toBeNull(); + lastTurnIdsByDay.push(state.lastConsolidatedTurnId!); + + if (prevLastTurnId !== null) { + // Monotonic advance: the new id must be a turn from this day. + expect(state.lastConsolidatedTurnId!.startsWith(`t-d${String(day).padStart(2, '0')}-`)).toBe(true); + } + prevLastTurnId = state.lastConsolidatedTurnId; + + // Activity counter is reset after each successful run. + expect(readState(db).turnsSinceLastRun).toBe(0); + } + + // -- Property 1: memory.md exists and is bounded ----------------- + const memoryPath = path.join(harness.mindPath, '.working-memory', 'memory.md'); + expect(existsSync(memoryPath)).toBe(true); + const memoryBuf = readFileSync(memoryPath); + expect(memoryBuf.byteLength).toBeGreaterThan(0); + expect(memoryBuf.byteLength).toBeLessThanOrEqual(INTEGRATION_CONFIG.memoryMaxBytes); + const memoryText = memoryBuf.toString('utf-8'); + // Curated content from the canned vault delta should be visible. + expect(memoryText.toLowerCase()).toContain('kebab-case'); + + // -- Property 2: weekly rollup materialised ---------------------- + const weeklyDir = path.join(harness.mindPath, '.working-memory', 'archive', 'weekly'); + expect(existsSync(weeklyDir)).toBe(true); + const weeklyFiles = await readdir(weeklyDir); + expect(weeklyFiles.length).toBeGreaterThanOrEqual(1); + expect(weeklyFiles.some((f) => /^\d{4}-W\d{2}\.md$/.test(f))).toBe(true); + + // -- Property 3: log.md is pruned (sentinel preserved) ----------- + const logPath = path.join(harness.mindPath, '.working-memory', 'log.md'); + expect(existsSync(logPath)).toBe(true); + const logText = readFileSync(logPath, 'utf-8'); + expect(logText).toContain('chamber-structured-log/v1'); + // After the final cycle, no in-scope turn ids remain (everything + // already archived). Survivors would be turns appended *after* the + // last snapshot — none in this test, so no `turn:t-dXX-YY` headers. + expect(/turn:t-d\d{2}-\d{2}/.test(logText)).toBe(false); + + // -- Property 4: archive/consolidated/ accumulates --------------- + const consolidatedDir = path.join(harness.mindPath, '.working-memory', 'archive', 'consolidated'); + expect(existsSync(consolidatedDir)).toBe(true); + const consolidatedFiles = await readdir(consolidatedDir); + expect(consolidatedFiles.length).toBe(7 * DAILY_PROMPTS.length); + + // -- Property 5: re-run with no new turns is skipped -------------- + const llmCallsBefore = harness.llm.calls.length; + await harness.scheduler.runNow(MIND_ID); + expect(harness.llm.calls.length).toBe(llmCallsBefore); // synthesize NOT invoked + const stateAfterIdleRun = readState(db); + expect(stateAfterIdleRun.lastConsolidatedTurnId).toBe(prevLastTurnId); + + // -- Property 7: monotonic last_consolidated_turn_id -------------- + for (let i = 1; i < lastTurnIdsByDay.length; i++) { + // Day-prefixed ids — newer-day prefix lexicographically greater. + expect(lastTurnIdsByDay[i]!.localeCompare(lastTurnIdsByDay[i - 1]!)).toBeGreaterThan(0); + } + + // -- Run history shows the success cycles plus the trailing skip -- + const runs = listRuns(db, { limit: 100 }); + const successes = runs.filter((r) => r.status === 'success'); + expect(successes.length).toBe(7); + }); + + it('parallel daemon.run() calls — only one cycle executes; the loser short-circuits with a skip', async () => { + harness = buildHarness(); + await harness.service.activateMind(MIND_ID, harness.mindPath); + + const observer = harness.chat.observers[0]!; + await observer.onTurnCompleted(makeTurn(1, 1, 'I prefer kebab-case file names.', 'ok')); + + const release = harness.llm.pauseNext(); + + const daemon = harness.daemon(); + const a = daemon.run(); + // Yield once so the first run() acquires the in-process mutex / DB lock. + await Promise.resolve(); + await Promise.resolve(); + const b = daemon.run(); + + // Release the LLM call so the first run can complete. + release(); + + const [ra, rb] = await Promise.all([a, b]); + const outcomes = [ra.status, rb.status].sort(); + // Exactly one success, one skip. + expect(outcomes).toEqual(['skipped', 'success']); + + // Synthesize was called exactly once across both runs. + expect(harness.llm.calls.length).toBe(1); + }); + + it('a turn appended between snapshot and prune survives in log.md', async () => { + harness = buildHarness(); + await harness.service.activateMind(MIND_ID, harness.mindPath); + + const observer = harness.chat.observers[0]!; + // Pre-cycle turns. + await observer.onTurnCompleted(makeTurn(1, 1, 'I prefer kebab-case file names.', 'ok')); + await observer.onTurnCompleted(makeTurn(1, 2, 'Always follow TDD when implementing features.', 'ok')); + + const release = harness.llm.pauseNext(); + const daemon = harness.daemon(); + + const runPromise = daemon.run(); + + // Wait until synthesize has been called — that proves snapshot has + // already been taken (snapshot precedes synthesize in runCycleLocked). + while (harness.llm.calls.length === 0) { + await new Promise((r) => setImmediate(r)); + } + + // Append a tail turn AFTER snapshot but BEFORE prune. + const tail = makeTurn(1, 9, 'tail prompt that arrives mid-cycle', 'tail assistant'); + await observer.onTurnCompleted(tail); + + release(); + const result = await runPromise; + expect(result.status).toBe('success'); + + // The tail turn must still be in log.md (not archived, not pruned). + const logPath = path.join(harness.mindPath, '.working-memory', 'log.md'); + const logText = readFileSync(logPath, 'utf-8'); + expect(logText).toContain('chamber-structured-log/v1'); + expect(logText).toContain(tail.turnId); + + // And it should NOT have been archived (only snapshot turns are). + const consolidatedDir = path.join(harness.mindPath, '.working-memory', 'archive', 'consolidated'); + const consolidatedFiles = await readdir(consolidatedDir); + expect(consolidatedFiles.length).toBe(2); + expect(consolidatedFiles.some((f) => f.includes(tail.turnId))).toBe(false); + }); +}); diff --git a/tests/integration/mindScaffold.integration.test.ts b/tests/integration/mindScaffold.integration.test.ts new file mode 100644 index 00000000..dd989ad3 --- /dev/null +++ b/tests/integration/mindScaffold.integration.test.ts @@ -0,0 +1,398 @@ +/** + * MindScaffold bootstrap integration smoke. + * + * Goal: lock in the dream-daemon contract that a freshly-scaffolded mind is + * born with a structured `log.md` (chamber-structured-log/v1 sentinel) and a + * registry pointing at `ianphil/genesis-frontier`. Exercises the full + * `MindScaffold.create()` path against a tmpdir with the network and SDK + * mocked, then verifies the on-disk state plus downstream consumer behaviour + * (DailyLogWriter, WorkingMemoryComposer). + * + * Why integration? The MindScaffold unit tests mock `fs` and can't see the + * actual byte-level contract on disk. This test runs real `fs` against a + * tmpdir so a future regression (e.g. someone "optimizing" the seed write + * away, or the WORKING_MEMORY_FILES loop re-blanking log.md) gets caught + * here. + * + * Filesystem hygiene: every assertion group runs against a fresh tmpdir, + * cleaned up in `afterEach`. ZERO writes to `~/agents` or any user-visible + * location. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { + MindScaffold, + STRUCTURED_LOG_SENTINEL, + createDailyLogWriter, + createWorkingMemoryComposer, + type CompletedTurn, + type CopilotClientFactory, + type GitHubRegistryClient, +} from '@chamber/services'; + +let tmpRoot: string; + +beforeEach(() => { + tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-mindscaffold-int-')); +}); + +afterEach(() => { + fs.rmSync(tmpRoot, { recursive: true, force: true }); +}); + +interface RegistryCallLog { + fetchTree: Array<[string, string, string]>; + fetchBlob: Array<[string, string, string]>; + fetchJsonContent: Array<[string, string, string, string]>; +} + +function makeFakeRegistryClient(callLog: RegistryCallLog): GitHubRegistryClient { + // A minimal valid upgrade.js so initGit's commit doesn't fail and the + // bootstrapCapabilities exec path can either run or be skipped cleanly. + // Returning {skills:{}} from fetchJsonContent means skillNames is empty and + // bootstrapCapabilities exits before invoking execSync on upgrade.js. + const tree = [ + { path: '.github/skills/upgrade/upgrade.js', type: 'blob', sha: 'sha-upgrade-js' }, + { path: '.github/skills/upgrade/skill.json', type: 'blob', sha: 'sha-upgrade-json' }, + ]; + return { + fetchTree: vi.fn(async (owner: string, repo: string, branch: string) => { + callLog.fetchTree.push([owner, repo, branch]); + return tree; + }), + fetchBlob: vi.fn(async (owner: string, repo: string, sha: string) => { + callLog.fetchBlob.push([owner, repo, sha]); + if (sha === 'sha-upgrade-js') { + return Buffer.from('// stub upgrade bootloader\nprocess.exit(0);\n', 'utf8'); + } + return Buffer.from('{"name":"upgrade","version":"1.0.0"}\n', 'utf8'); + }), + fetchJsonContent: vi.fn(async (owner: string, repo: string, filePath: string, ref: string) => { + callLog.fetchJsonContent.push([owner, repo, filePath, ref]); + // No remote skills besides the bootloader itself → bootstrapCapabilities + // early-returns without execSync. + return { skills: {} }; + }), + } as unknown as GitHubRegistryClient; +} + +interface SoulPaths { + soul: string; + agent: string; + memory: string; + rules: string; + index: string; +} + +// A fake session that, on `send()`, synchronously writes the minimal set of +// files genesis-prompt would otherwise drive the LLM to write. This keeps +// `validate()` happy so the test exercises the full create() path including +// initGit and bootstrapCapabilities. +function makeFakeClientFactory(seedFiles: (paths: SoulPaths) => void): CopilotClientFactory { + return { + createClient: vi.fn(async (mindPath: string) => { + const slug = path.basename(mindPath); + const paths: SoulPaths = { + soul: path.join(mindPath, 'SOUL.md'), + agent: path.join(mindPath, '.github', 'agents', `${slug}.agent.md`), + memory: path.join(mindPath, '.working-memory', 'memory.md'), + rules: path.join(mindPath, '.working-memory', 'rules.md'), + index: path.join(mindPath, 'mind-index.md'), + }; + const session = { + send: vi.fn(async () => { + seedFiles(paths); + }), + disconnect: vi.fn(async () => undefined), + on: vi.fn((event: string, callback: () => void) => { + if (event === 'session.idle') setTimeout(callback, 0); + return vi.fn(); + }), + rpc: { permissions: { setApproveAll: vi.fn(async () => ({ success: true })) } }, + }; + return { createSession: vi.fn(async () => session) }; + }), + destroyClient: vi.fn(async () => undefined), + } as unknown as CopilotClientFactory; +} + +function defaultSeedFiles(paths: SoulPaths): void { + fs.writeFileSync(paths.soul, '# Test Soul\n\nA mind for tests.\n'); + fs.writeFileSync(paths.agent, '---\nname: test\ndescription: test\n---\n'); + fs.writeFileSync(paths.memory, '# Memory\n'); + fs.writeFileSync(paths.rules, '# Rules\n'); + fs.writeFileSync(paths.index, '# Mind Index\n'); +} + +describe('MindScaffold.create — bootstrap integration', () => { + it('opt-in (enableDreamDaemon=true): produces a sentinel-prefixed log.md AND writes .chamber.json with consolidation.enabled=true', async () => { + const callLog: RegistryCallLog = { fetchTree: [], fetchBlob: [], fetchJsonContent: [] }; + const scaffold = new MindScaffold( + makeFakeRegistryClient(callLog), + makeFakeClientFactory(defaultSeedFiles), + ); + + const mindPath = await scaffold.create({ + name: 'Sentinel Mind', + role: 'integration tester', + voice: 'plain', + voiceDescription: 'plain', + basePath: tmpRoot, + enableDreamDaemon: true, + }); + + const logPath = path.join(mindPath, '.working-memory', 'log.md'); + expect(fs.existsSync(logPath)).toBe(true); + const content = fs.readFileSync(logPath, 'utf-8'); + const firstNonBlank = content.split('\n').find((l) => l.trim() !== ''); + expect(firstNonBlank).toBe(STRUCTURED_LOG_SENTINEL); + + // Persist the opt-in choice so MindMemoryService.activateMind reads it + // back on the next mind load — without this, the user toggled the + // Switch but the daemon would never start. + const chamberJsonPath = path.join(mindPath, '.chamber.json'); + expect(fs.existsSync(chamberJsonPath)).toBe(true); + const chamberConfig = JSON.parse(fs.readFileSync(chamberJsonPath, 'utf-8')) as { + workingMemory?: { consolidation?: { enabled?: unknown } }; + }; + expect(chamberConfig.workingMemory?.consolidation?.enabled).toBe(true); + }); + + it('opt-out (enableDreamDaemon=false): log.md is empty AND .chamber.json is absent', async () => { + // Default flow for users who don't opt in. The mind still works (chat, + // tools, memory, rules) — it just doesn't run the dream daemon and + // doesn't materialize structured-log frames on each turn. + const callLog: RegistryCallLog = { fetchTree: [], fetchBlob: [], fetchJsonContent: [] }; + const scaffold = new MindScaffold( + makeFakeRegistryClient(callLog), + makeFakeClientFactory(defaultSeedFiles), + ); + + const mindPath = await scaffold.create({ + name: 'Quiet Mind', + role: 'integration tester', + voice: 'plain', + voiceDescription: 'plain', + basePath: tmpRoot, + enableDreamDaemon: false, + }); + + const logPath = path.join(mindPath, '.working-memory', 'log.md'); + expect(fs.existsSync(logPath)).toBe(true); + expect(fs.readFileSync(logPath, 'utf-8')).toBe(''); + expect(fs.existsSync(path.join(mindPath, '.chamber.json'))).toBe(false); + }); + + it('records source: ianphil/genesis-frontier in registry.json', async () => { + const callLog: RegistryCallLog = { fetchTree: [], fetchBlob: [], fetchJsonContent: [] }; + const scaffold = new MindScaffold( + makeFakeRegistryClient(callLog), + makeFakeClientFactory(defaultSeedFiles), + ); + + const mindPath = await scaffold.create({ + name: 'Frontier Mind', + role: 'integration tester', + voice: 'plain', + voiceDescription: 'plain', + basePath: tmpRoot, + }); + + const registry = JSON.parse( + fs.readFileSync(path.join(mindPath, '.github', 'registry.json'), 'utf-8'), + ); + expect(registry.source).toBe('ianphil/genesis-frontier'); + expect(registry.channel).toBe('main'); + }); + + it('pulls the upgrade skill from ianphil/genesis-frontier and writes upgrade.js on disk', async () => { + const callLog: RegistryCallLog = { fetchTree: [], fetchBlob: [], fetchJsonContent: [] }; + const scaffold = new MindScaffold( + makeFakeRegistryClient(callLog), + makeFakeClientFactory(defaultSeedFiles), + ); + + const mindPath = await scaffold.create({ + name: 'Upgrade Mind', + role: 'integration tester', + voice: 'plain', + voiceDescription: 'plain', + basePath: tmpRoot, + }); + + // Network coordinates + expect(callLog.fetchTree).toEqual([['ianphil', 'genesis-frontier', 'main']]); + // On-disk artifact + const upgradeJs = path.join(mindPath, '.github', 'skills', 'upgrade', 'upgrade.js'); + expect(fs.existsSync(upgradeJs)).toBe(true); + expect(fs.readFileSync(upgradeJs, 'utf-8')).toContain('stub upgrade bootloader'); + }); + + it('lays down the full IDEA + .github + working-memory structure', async () => { + const callLog: RegistryCallLog = { fetchTree: [], fetchBlob: [], fetchJsonContent: [] }; + const scaffold = new MindScaffold( + makeFakeRegistryClient(callLog), + makeFakeClientFactory(defaultSeedFiles), + ); + + const mindPath = await scaffold.create({ + name: 'Structure Mind', + role: 'integration tester', + voice: 'plain', + voiceDescription: 'plain', + basePath: tmpRoot, + }); + + for (const folder of ['inbox', 'domains', 'expertise', 'initiatives', 'Archive']) { + expect(fs.existsSync(path.join(mindPath, folder))).toBe(true); + } + expect(fs.existsSync(path.join(mindPath, '.github', 'agents'))).toBe(true); + expect(fs.existsSync(path.join(mindPath, '.github', 'skills'))).toBe(true); + expect(fs.existsSync(path.join(mindPath, '.working-memory', 'memory.md'))).toBe(true); + expect(fs.existsSync(path.join(mindPath, '.working-memory', 'rules.md'))).toBe(true); + expect(fs.existsSync(path.join(mindPath, '.working-memory', 'log.md'))).toBe(true); + }); + + it('lets DailyLogWriter append a structured frame without producing log.legacy.md', async () => { + const callLog: RegistryCallLog = { fetchTree: [], fetchBlob: [], fetchJsonContent: [] }; + const scaffold = new MindScaffold( + makeFakeRegistryClient(callLog), + makeFakeClientFactory(defaultSeedFiles), + ); + + const mindPath = await scaffold.create({ + name: 'Writer Mind', + role: 'integration tester', + voice: 'plain', + voiceDescription: 'plain', + basePath: tmpRoot, + }); + + const writer = createDailyLogWriter({ mindId: 'writer-mind', mindPath }); + const turn: CompletedTurn = { + turnId: '00000000-0000-4000-8000-000000000001', + sessionId: 'sess-int-1', + model: 'claude-opus-4.7', + status: 'completed', + startedAt: '2026-05-13T14:00:00Z', + endedAt: '2026-05-13T14:00:05Z', + prompt: 'hello', + finalAssistantMessage: 'hi back', + }; + await writer.write(turn); + + const logContent = fs.readFileSync( + path.join(mindPath, '.working-memory', 'log.md'), + 'utf-8', + ); + expect(logContent).toContain(STRUCTURED_LOG_SENTINEL); + expect(logContent).toContain('turn:00000000-0000-4000-8000-000000000001'); + expect(logContent).toContain('### user'); + expect(logContent).toContain('### assistant'); + // The sentinel pre-seed means no rotation happens — log.legacy.md must not exist. + expect(fs.existsSync(path.join(mindPath, '.working-memory', 'log.legacy.md'))).toBe(false); + }); + + it('WorkingMemoryComposer treats the fresh mind as structured (no info or warn fired)', async () => { + const callLog: RegistryCallLog = { fetchTree: [], fetchBlob: [], fetchJsonContent: [] }; + const scaffold = new MindScaffold( + makeFakeRegistryClient(callLog), + makeFakeClientFactory(defaultSeedFiles), + ); + + const mindPath = await scaffold.create({ + name: 'Composer Mind', + role: 'integration tester', + voice: 'plain', + voiceDescription: 'plain', + basePath: tmpRoot, + }); + + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + composer.compose(mindPath, { enabled: true, lastKTurns: 10, perTurnMaxBytes: 2048, memoryMaxBytes: 8192 }); + + expect(warn).not.toHaveBeenCalled(); + // info may be called for benign reasons (e.g. memory.md truncation) but + // must NEVER be called with the "unstructured" message for a fresh mind. + for (const call of info.mock.calls) { + expect(call[0]).not.toMatch(/unstructured/i); + } + }); + + // Cross-cutting migration story for minds that already exist on disk in the + // pre-fix shape: an unstructured `log.md` (no sentinel). Locks in three + // promises end-to-end: + // 1. Composer reads the unstructured file at `info` level — never `warn` + // (the migration-window contract). + // 2. DailyLogWriter on the first chat turn rotates the legacy content out + // to `log.legacy.md` and seeds a fresh sentinel-prefixed log.md. + // 3. After rotation, the composer is silent (no further unstructured + // messages on subsequent prompt rebuilds). + it('migrates an existing pre-fix mind: composer info → first turn rotates → composer silent', async () => { + const callLog: RegistryCallLog = { fetchTree: [], fetchBlob: [], fetchJsonContent: [] }; + const scaffold = new MindScaffold( + makeFakeRegistryClient(callLog), + makeFakeClientFactory(defaultSeedFiles), + ); + + const mindPath = await scaffold.create({ + name: 'Legacy Mind', + role: 'integration tester', + voice: 'plain', + voiceDescription: 'plain', + basePath: tmpRoot, + }); + + // Simulate a mind that was scaffolded BEFORE the fix shipped: unstructured + // log.md with no sentinel. This is the on-disk shape for every existing + // user upgrading into this release. + const logPath = path.join(mindPath, '.working-memory', 'log.md'); + const legacyPath = path.join(mindPath, '.working-memory', 'log.legacy.md'); + const legacyContent = '# pre-fix freeform notes\n\nthis is what existing minds have.\n'; + fs.writeFileSync(logPath, legacyContent); + expect(fs.existsSync(legacyPath)).toBe(false); + + const warn = vi.fn(); + const info = vi.fn(); + const composer = createWorkingMemoryComposer({ logger: { warn, info } }); + + // Step 1: opening the mind triggers a system-prompt rebuild → info, never warn. + composer.compose(mindPath, { enabled: true, lastKTurns: 10, perTurnMaxBytes: 2048, memoryMaxBytes: 8192 }); + expect(warn).not.toHaveBeenCalled(); + const unstructuredInfoCalls = info.mock.calls.filter((c) => /unstructured/i.test(c[0])); + expect(unstructuredInfoCalls.length).toBe(1); + + // Step 2: first chat turn → DailyLogWriter rotates and seeds the sentinel. + const writer = createDailyLogWriter({ mindId: 'legacy-mind', mindPath }); + const turn: CompletedTurn = { + turnId: '00000000-0000-4000-8000-000000000099', + sessionId: 'sess-legacy-1', + model: 'claude-opus-4.7', + status: 'completed', + startedAt: '2026-05-13T15:00:00Z', + endedAt: '2026-05-13T15:00:05Z', + prompt: 'first turn after upgrade', + finalAssistantMessage: 'welcome back', + }; + await writer.write(turn); + + expect(fs.existsSync(legacyPath)).toBe(true); + expect(fs.readFileSync(legacyPath, 'utf-8')).toBe(legacyContent); + const rotatedLog = fs.readFileSync(logPath, 'utf-8'); + expect(rotatedLog).toContain(STRUCTURED_LOG_SENTINEL); + expect(rotatedLog).toContain('turn:00000000-0000-4000-8000-000000000099'); + + // Step 3: subsequent prompt rebuild is silent — migration is complete. + info.mockClear(); + warn.mockClear(); + composer.compose(mindPath, { enabled: true, lastKTurns: 10, perTurnMaxBytes: 2048, memoryMaxBytes: 8192 }); + expect(warn).not.toHaveBeenCalled(); + for (const call of info.mock.calls) { + expect(call[0]).not.toMatch(/unstructured/i); + } + }); +}); diff --git a/tests/regression/ensure-native-abi.test.ts b/tests/regression/ensure-native-abi.test.ts new file mode 100644 index 00000000..fcb07529 --- /dev/null +++ b/tests/regression/ensure-native-abi.test.ts @@ -0,0 +1,283 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const lib = require('../../scripts/lib/ensure-native-abi.cjs') as { + TARGETS: readonly string[]; + DEFAULT_SENTINEL_PATH: string; + readSentinel: (p?: string) => string | null; + decideAction: (input: { + target: string; + current: string | null; + moduleVersion: string; + }) => 'noop' | 'rebuild'; + writeSentinel: (input: { target: string; moduleVersion: string }, p?: string) => void; + rebuildCommand: (target: string) => string; + rebuild: (target: string, runner?: (cmd: string) => void) => void; +}; + +describe('ensure-native-abi guard', () => { + let tmpDir: string; + let sentinelPath: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chamber-abi-test-')); + sentinelPath = path.join(tmpDir, 'build', 'Release', '.abi-target'); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + describe('TARGETS', () => { + it('is locked to node and electron only', () => { + expect([...lib.TARGETS]).toEqual(['node', 'electron']); + }); + }); + + describe('decideAction', () => { + it('returns "noop" when current matches target AND module ABI version', () => { + expect( + lib.decideAction({ target: 'node', current: 'node:137', moduleVersion: '137' }), + ).toBe('noop'); + expect( + lib.decideAction({ target: 'electron', current: 'electron:125', moduleVersion: '125' }), + ).toBe('noop'); + }); + + it('returns "rebuild" when target matches but module ABI version differs', () => { + // The exact bug C-1 caught: Node 22→24 keeps target='node' but flips MODULE_VERSION. + expect( + lib.decideAction({ target: 'node', current: 'node:145', moduleVersion: '137' }), + ).toBe('rebuild'); + expect( + lib.decideAction({ target: 'electron', current: 'electron:125', moduleVersion: '127' }), + ).toBe('rebuild'); + }); + + it('returns "rebuild" on a legacy single-token sentinel (pre-ABI format)', () => { + // A sentinel written by an older version of this script has no `:NNN` suffix. + // We must rebuild rather than trust it — we cannot prove the ABI matches. + expect( + lib.decideAction({ target: 'node', current: 'node', moduleVersion: '137' }), + ).toBe('rebuild'); + expect( + lib.decideAction({ target: 'electron', current: 'electron', moduleVersion: '125' }), + ).toBe('rebuild'); + }); + + it('returns "rebuild" when the framework target differs (even with matching ABI)', () => { + expect( + lib.decideAction({ target: 'node', current: 'electron:137', moduleVersion: '137' }), + ).toBe('rebuild'); + expect( + lib.decideAction({ target: 'electron', current: 'node:125', moduleVersion: '125' }), + ).toBe('rebuild'); + }); + + it('returns "rebuild" when no sentinel exists yet (current is null)', () => { + expect( + lib.decideAction({ target: 'node', current: null, moduleVersion: '137' }), + ).toBe('rebuild'); + expect( + lib.decideAction({ target: 'electron', current: null, moduleVersion: '125' }), + ).toBe('rebuild'); + }); + + it('throws on unknown target rather than silently skipping', () => { + expect(() => + lib.decideAction({ target: 'wasm', current: 'node:137', moduleVersion: '137' }), + ).toThrow(/unknown target/); + expect(() => + lib.decideAction({ target: '', current: 'node:137', moduleVersion: '137' }), + ).toThrow(/unknown target/); + }); + + it('throws on missing or malformed moduleVersion', () => { + // A bad moduleVersion would corrupt the sentinel — fail loud rather than write garbage. + expect(() => + lib.decideAction({ + target: 'node', + current: 'node:137', + moduleVersion: '', + }), + ).toThrow(/moduleVersion/); + expect(() => + lib.decideAction({ + target: 'node', + current: 'node:137', + moduleVersion: 'undefined', + }), + ).toThrow(/moduleVersion/); + expect(() => + lib.decideAction({ + target: 'node', + current: 'node:137', + // @ts-expect-error: deliberately wrong type to test runtime guard + moduleVersion: 137, + }), + ).toThrow(/moduleVersion/); + }); + }); + + describe('readSentinel', () => { + it('returns null when the sentinel file does not exist', () => { + expect(lib.readSentinel(sentinelPath)).toBeNull(); + }); + + it('returns the trimmed sentinel contents when present', () => { + fs.mkdirSync(path.dirname(sentinelPath), { recursive: true }); + fs.writeFileSync(sentinelPath, 'electron:125\n'); + expect(lib.readSentinel(sentinelPath)).toBe('electron:125'); + }); + + it('returns a legacy single-token sentinel verbatim (decideAction handles rejection)', () => { + // readSentinel stays a dumb file reader; semantic interpretation belongs to decideAction. + fs.mkdirSync(path.dirname(sentinelPath), { recursive: true }); + fs.writeFileSync(sentinelPath, 'node\n'); + expect(lib.readSentinel(sentinelPath)).toBe('node'); + }); + + it('returns null when the sentinel directory is unreadable / missing parents', () => { + const deepMissing = path.join(tmpDir, 'never', 'made', '.abi-target'); + expect(lib.readSentinel(deepMissing)).toBeNull(); + }); + }); + + describe('writeSentinel', () => { + it('writes ${target}:${moduleVersion} with a trailing newline, creating parents', () => { + lib.writeSentinel({ target: 'node', moduleVersion: '137' }, sentinelPath); + const raw = fs.readFileSync(sentinelPath, 'utf8'); + expect(raw).toBe('node:137\n'); + }); + + it('round-trips with readSentinel for both targets', () => { + lib.writeSentinel({ target: 'electron', moduleVersion: '125' }, sentinelPath); + expect(lib.readSentinel(sentinelPath)).toBe('electron:125'); + }); + + it('refuses to write an unknown target', () => { + expect(() => + lib.writeSentinel({ target: 'wasm', moduleVersion: '137' }, sentinelPath), + ).toThrow(/unknown target/); + expect(fs.existsSync(sentinelPath)).toBe(false); + }); + + it('refuses to write a missing or malformed moduleVersion', () => { + expect(() => + lib.writeSentinel({ target: 'node', moduleVersion: '' }, sentinelPath), + ).toThrow(/moduleVersion/); + expect(() => + lib.writeSentinel({ target: 'node', moduleVersion: 'NaN' }, sentinelPath), + ).toThrow(/moduleVersion/); + expect(fs.existsSync(sentinelPath)).toBe(false); + }); + + it('overwrites an existing sentinel rather than appending', () => { + lib.writeSentinel({ target: 'node', moduleVersion: '137' }, sentinelPath); + lib.writeSentinel({ target: 'electron', moduleVersion: '125' }, sentinelPath); + expect(lib.readSentinel(sentinelPath)).toBe('electron:125'); + }); + }); + + describe('rebuildCommand', () => { + it('uses `npm rebuild better-sqlite3` for the node target', () => { + expect(lib.rebuildCommand('node')).toBe('npm rebuild better-sqlite3'); + }); + + it('uses electron-rebuild scoped to better-sqlite3 for the electron target', () => { + const cmd = lib.rebuildCommand('electron'); + expect(cmd).toContain('electron-rebuild'); + expect(cmd).toContain('better-sqlite3'); + expect(cmd).toContain('-f'); + expect(cmd).toContain('-w better-sqlite3'); + }); + + it('throws on unknown target', () => { + expect(() => lib.rebuildCommand('wasm')).toThrow(/unknown target/); + }); + }); + + describe('rebuild', () => { + it('invokes the runner with the resolved command for node', () => { + const calls: string[] = []; + lib.rebuild('node', (cmd: string) => calls.push(cmd)); + expect(calls).toEqual([lib.rebuildCommand('node')]); + }); + + it('invokes the runner with the resolved command for electron', () => { + const calls: string[] = []; + lib.rebuild('electron', (cmd: string) => calls.push(cmd)); + expect(calls).toEqual([lib.rebuildCommand('electron')]); + }); + + it('propagates runner failures so the CLI can exit non-zero', () => { + expect(() => + lib.rebuild('node', () => { + throw new Error('toolchain missing'); + }), + ).toThrow(/toolchain missing/); + }); + }); + + describe('integration: full guard sequence', () => { + it('rebuilds on first run, then noops on the second run with the same target+ABI', () => { + // First run: no sentinel yet → rebuild path + const first = lib.decideAction({ + target: 'node', + current: lib.readSentinel(sentinelPath), + moduleVersion: '137', + }); + expect(first).toBe('rebuild'); + const calls: string[] = []; + lib.rebuild('node', (cmd) => calls.push(cmd)); + lib.writeSentinel({ target: 'node', moduleVersion: '137' }, sentinelPath); + + // Second run: sentinel matches → noop, no rebuild invoked + const second = lib.decideAction({ + target: 'node', + current: lib.readSentinel(sentinelPath), + moduleVersion: '137', + }); + expect(second).toBe('noop'); + expect(calls).toHaveLength(1); + }); + + it('rebuilds when switching target from node → electron', () => { + lib.writeSentinel({ target: 'node', moduleVersion: '137' }, sentinelPath); + const action = lib.decideAction({ + target: 'electron', + current: lib.readSentinel(sentinelPath), + moduleVersion: '125', + }); + expect(action).toBe('rebuild'); + }); + + it('rebuilds when Node ABI shifts under the same target (the C-1 bug)', () => { + // Simulates: binary was built on Node 23 (MODULE_VERSION 145), developer upgraded + // to Node 24 (MODULE_VERSION 137). Old guard silently said noop. New guard rebuilds. + lib.writeSentinel({ target: 'node', moduleVersion: '145' }, sentinelPath); + const action = lib.decideAction({ + target: 'node', + current: lib.readSentinel(sentinelPath), + moduleVersion: '137', + }); + expect(action).toBe('rebuild'); + }); + + it('treats a legacy single-token sentinel as a rebuild signal', () => { + // Older sentinel left by pre-fix versions of the guard. Force a one-time rebuild + // so the new format is written and future runs can short-circuit. + fs.mkdirSync(path.dirname(sentinelPath), { recursive: true }); + fs.writeFileSync(sentinelPath, 'node\n'); + const action = lib.decideAction({ + target: 'node', + current: lib.readSentinel(sentinelPath), + moduleVersion: '137', + }); + expect(action).toBe('rebuild'); + }); + }); +});