From 2071a8ab61f83b996f738b5ab0b57612ebf62ff1 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Mon, 15 Jun 2026 14:29:30 +0200 Subject: [PATCH 1/2] feat(memory): add memory dashboard plugin --- .ai-devkit.json | 34 +- .../2026-06-14-feature-memory-dashboard.md | 211 + .../2026-06-14-feature-memory-dashboard.md | 191 + .../2026-06-14-feature-memory-dashboard.md | 162 + .../2026-06-14-feature-memory-dashboard.md | 121 + .../2026-06-14-feature-memory-dashboard.md | 122 + package-lock.json | 3457 +++++++---------- .../plugin/memory-dashboard-package.test.ts | 22 + packages/memory-dashboard/.eslintrc.json | 22 + packages/memory-dashboard/.swcrc | 21 + packages/memory-dashboard/README.md | 23 + packages/memory-dashboard/package.json | 77 + packages/memory-dashboard/project.json | 29 + packages/memory-dashboard/src/command.ts | 106 + packages/memory-dashboard/src/index.ts | 1 + packages/memory-dashboard/src/server.ts | 289 ++ packages/memory-dashboard/src/standalone.ts | 108 + packages/memory-dashboard/src/ui/app.js | 594 +++ .../memory-dashboard/src/ui/dashboard.html | 85 + packages/memory-dashboard/src/ui/tailwind.css | 209 + .../memory-dashboard/tests/command.test.ts | 163 + .../memory-dashboard/tests/server.test.ts | 299 ++ .../memory-dashboard/tests/standalone.test.ts | 92 + packages/memory-dashboard/tsconfig.json | 35 + packages/memory-dashboard/vitest.config.ts | 20 + packages/memory/src/api.ts | 49 +- packages/memory/src/handlers/list.ts | 160 + packages/memory/src/handlers/summary.ts | 94 + packages/memory/src/types/index.ts | 25 +- .../memory/tests/integration/list.test.ts | 98 + .../memory/tests/integration/summary.test.ts | 74 + 31 files changed, 4893 insertions(+), 2100 deletions(-) create mode 100644 docs/ai/design/2026-06-14-feature-memory-dashboard.md create mode 100644 docs/ai/implementation/2026-06-14-feature-memory-dashboard.md create mode 100644 docs/ai/planning/2026-06-14-feature-memory-dashboard.md create mode 100644 docs/ai/requirements/2026-06-14-feature-memory-dashboard.md create mode 100644 docs/ai/testing/2026-06-14-feature-memory-dashboard.md create mode 100644 packages/cli/src/__tests__/services/plugin/memory-dashboard-package.test.ts create mode 100644 packages/memory-dashboard/.eslintrc.json create mode 100644 packages/memory-dashboard/.swcrc create mode 100644 packages/memory-dashboard/README.md create mode 100644 packages/memory-dashboard/package.json create mode 100644 packages/memory-dashboard/project.json create mode 100644 packages/memory-dashboard/src/command.ts create mode 100644 packages/memory-dashboard/src/index.ts create mode 100644 packages/memory-dashboard/src/server.ts create mode 100644 packages/memory-dashboard/src/standalone.ts create mode 100644 packages/memory-dashboard/src/ui/app.js create mode 100644 packages/memory-dashboard/src/ui/dashboard.html create mode 100644 packages/memory-dashboard/src/ui/tailwind.css create mode 100644 packages/memory-dashboard/tests/command.test.ts create mode 100644 packages/memory-dashboard/tests/server.test.ts create mode 100644 packages/memory-dashboard/tests/standalone.test.ts create mode 100644 packages/memory-dashboard/tsconfig.json create mode 100644 packages/memory-dashboard/vitest.config.ts create mode 100644 packages/memory/src/handlers/list.ts create mode 100644 packages/memory/src/handlers/summary.ts create mode 100644 packages/memory/tests/integration/list.test.ts create mode 100644 packages/memory/tests/integration/summary.test.ts diff --git a/.ai-devkit.json b/.ai-devkit.json index 8cb00d2d..fe759356 100644 --- a/.ai-devkit.json +++ b/.ai-devkit.json @@ -9,7 +9,7 @@ "antigravity" ], "createdAt": "2025-12-28T13:35:45.251Z", - "updatedAt": "2026-05-28T06:26:09.553Z", + "updatedAt": "2026-06-14T11:53:00.073Z", "phases": [ "requirements", "design", @@ -57,6 +57,38 @@ { "registry": "vercel-labs/agent-skills", "name": "react-best-practices" + }, + { + "registry": "codeaholicguy/ai-devkit", + "name": "agent-communication" + }, + { + "registry": "codeaholicguy/ai-devkit", + "name": "dev-worktree" + }, + { + "registry": "codeaholicguy/ai-devkit", + "name": "dev-requirements" + }, + { + "registry": "codeaholicguy/ai-devkit", + "name": "dev-design" + }, + { + "registry": "codeaholicguy/ai-devkit", + "name": "dev-planning" + }, + { + "registry": "codeaholicguy/ai-devkit", + "name": "dev-implementation" + }, + { + "registry": "codeaholicguy/ai-devkit", + "name": "dev-testing" + }, + { + "registry": "codeaholicguy/ai-devkit", + "name": "dev-review" } ] } diff --git a/docs/ai/design/2026-06-14-feature-memory-dashboard.md b/docs/ai/design/2026-06-14-feature-memory-dashboard.md new file mode 100644 index 00000000..b407b0ec --- /dev/null +++ b/docs/ai/design/2026-06-14-feature-memory-dashboard.md @@ -0,0 +1,211 @@ +--- +phase: design +title: System Design & Architecture +description: Define the technical architecture, components, and data models +--- + +# System Design & Architecture + +## Architecture Overview + +```mermaid +flowchart TD + User[User] -->|ai-devkit memory-dashboard| Cli[AI DevKit CLI] + Cli --> PluginLoader[Configured plugin loader] + PluginLoader --> Manifest[@ai-devkit/memory-dashboard package manifest] + Manifest --> Command[dist/command.js register()] + Command --> Runtime[AiDevkitRuntime] + Runtime -->|getMemoryDbPath| MemoryDb[(SQLite memory.db)] + Command --> Server[Local dashboard HTTP server] + Server --> MemoryApi[@ai-devkit/memory read APIs] + MemoryApi --> MemoryDb + Browser[Browser UI] -->|localhost HTTP/JSON| Server + Browser --> List[Memory list and detail] + Browser --> Facets[Filters and grouping] + Browser --> Graph[Interactive Cytoscape tag/scope graph] +``` + +The dashboard is a first-party AI DevKit plugin package. AI DevKit loads its manifest from `package.json`, registers the `memory-dashboard` command, and passes the existing plugin runtime to the command entrypoint. The command starts a localhost-only HTTP server that serves static UI assets and JSON endpoints backed by read-only APIs in `@ai-devkit/memory`. + +### Technology Stack +- Node.js HTTP server using the built-in `node:http` module for the MVP. +- TypeScript source compiled to JavaScript before plugin execution. +- Static browser UI built from framework-free HTML, Tailwind-generated CSS, and a JavaScript client module. +- Cytoscape.js for the graph view so the memory graph is interactive, styled independently from data, and visually as important as the list. +- Existing `@ai-devkit/memory` database and query utilities for server-side data access. + +## Data Models + +### Memory Dashboard Item +```typescript +interface MemoryDashboardItem { + id: string; + title: string; + content: string; + tags: string[]; + scope: string; + createdAt: string; + updatedAt: string; + score?: number; +} +``` + +### Memory Query +```typescript +interface MemoryDashboardQuery { + query?: string; + tags?: string[]; + scope?: string; + limit?: number; + offset?: number; + sort?: 'updated-desc' | 'created-desc' | 'title-asc'; +} +``` + +### Summary and Facets +```typescript +interface MemoryDashboardSummary { + totalItems: number; + scopes: Array<{ scope: string; count: number }>; + tags: Array<{ tag: string; count: number }>; + recency: Array<{ bucket: 'today' | 'week' | 'month' | 'older'; count: number }>; +} +``` + +### Graph +```typescript +interface MemoryGraph { + nodes: Array<{ + id: string; + label: string; + type: 'memory' | 'tag' | 'scope'; + count?: number; + item?: MemoryDashboardItem; + }>; + edges: Array<{ + source: string; + target: string; + type: 'has-tag' | 'in-scope'; + }>; +} +``` + +## API Design + +### Plugin Command +```bash +ai-devkit memory-dashboard [--host 127.0.0.1] [--port 0] [--open] +``` + +- `--host` defaults to `127.0.0.1`. +- `--port 0` lets the OS choose a free port by default. +- The command prints the dashboard URL and keeps the server running until interrupted. +- `--open` opts into browser launch. The default is URL-only so headless shells, CI, and agent sessions behave predictably. + +### Local HTTP Endpoints +```text +GET / -> dashboard HTML +GET /assets/* -> static JS/CSS/vendor graph assets +GET /api/memory -> list/search memory items with query parameters +GET /api/summary -> counts by scope, tag, and recency +GET /api/graph -> graph nodes and edges +``` + +`GET /api/memory` accepts `query`, `scope`, repeated `tag`, `limit`, `offset`, and `sort` query parameters. Limits are clamped to a safe maximum. Invalid values return a JSON `400` response. + +`GET /api/graph` accepts the same filtering parameters plus an optional `maxItems`. The server derives graph nodes from the bounded filtered item set and aggregates high-cardinality tag or scope nodes with counts where necessary. + +### Memory Package Additions +Add read-only helpers to `@ai-devkit/memory` so the dashboard does not duplicate database connection and schema mapping details: + +```typescript +interface ListKnowledgeInput { + query?: string; + tags?: string[]; + scope?: string; + limit?: number; + offset?: number; + sort?: 'updated-desc' | 'created-desc' | 'title-asc'; +} + +interface ListKnowledgeResult { + items: KnowledgeItem[]; + total: number; +} + +interface KnowledgeSummaryResult { + totalItems: number; + scopes: Array<{ scope: string; count: number }>; + tags: Array<{ tag: string; count: number }>; + recency: Array<{ bucket: string; count: number }>; +} +``` + +The helper APIs may open the database with normal write capability only to preserve current schema initialization behavior for a missing database file. Dashboard operations themselves must remain read-only: no store, update, delete, merge, or schema-changing dashboard endpoint is exposed. + +## Component Breakdown + +### `packages/memory-dashboard` +- `package.json` declares package metadata, build scripts, and `aiDevkit.commands`. +- `src/command.ts` exports `register(command, runtime)`. +- `src/server.ts` creates the HTTP server, resolves static assets, serves the local Cytoscape browser module, and wires API routes. +- `src/ui/dashboard.html` contains the dashboard shell and Tailwind utility classes. +- `src/ui/tailwind.css` is the Tailwind input and component layer for graph/list/detail surfaces. +- `src/ui/app.js` owns query/filter/grouping state, list/detail rendering, and Cytoscape graph interactions. + +### `packages/memory` +- Add read-only list and summary APIs. +- Keep database access server-side and Node-only. +- Reuse existing database initialization and row mapping conventions. + +### Dashboard UI +- Main layout: desktop workbench with top search/filter controls, a 4-row paginated records rail, and a graph/detail workspace visible in the first fold. +- Dense operational UI rather than a marketing landing page. +- Graph view uses existing metadata and does not require persistent graph storage. +- Cytoscape renders memory, tag, and scope nodes with semantic styling, a natural force-directed layout, selected-neighborhood highlighting/refocus, fit, zoom, and layout controls. +- Initial state shows total count, scope/tag facets, recent memory items, and an empty-state message when no records exist. +- Search/filter/grouping state is represented in URL query parameters so refreshes preserve the current view. + +## Design Decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Package location | `packages/memory-dashboard` | Matches monorepo workspace and user request. | +| Integration model | AI DevKit plugin command | Uses the existing plugin host instead of hard-coding dashboard into core CLI. | +| Server binding | `127.0.0.1` by default | Keeps memory data local to the user's machine. | +| Data access | Add read-only APIs to `@ai-devkit/memory` | Avoids duplicating SQLite schema knowledge in the dashboard package. | +| MVP permissions | Read-only | Reduces risk while browse/assessment UX is validated. | +| Graph model | Derived item/tag/scope graph | Gives immediate value with current metadata and no schema migration. | +| Browser opening | URL-only by default with `--open` opt-in | Avoids GUI assumptions in terminals, CI, and agent sessions while still supporting local convenience. | +| Frontend stack | Framework-free static UI with Tailwind output | Keeps runtime simple while making the split HTML/CSS/JS assets maintainable. | +| Graph library | Cytoscape.js | Provides graph layouts, styling, selection, gestures, and canvas rendering without maintaining a custom SVG layout. | + +### Alternatives Considered +- **Add dashboard to core CLI package**: rejected because the user wants an installable plugin package and plugin foundation now exists. +- **Read SQLite directly from the dashboard package**: rejected because it duplicates memory package internals and makes schema changes harder. +- **Use the existing `memory search` CLI as a subprocess**: rejected because it makes pagination, facets, and graph data awkward and slower. +- **Build edit/delete workflows in MVP**: deferred because data mutation requires stronger confirmation UX and backup behavior. +- **Open browser automatically by default**: rejected because it is brittle in headless shells and less predictable for agents. +- **Use React/Vite for the MVP**: deferred because the initial dashboard can meet the minimum requirement with static HTML, Tailwind output, and simpler packaging. + +## Non-Functional Requirements + +### Performance +- Default result limit should prevent loading very large memory databases in one response. +- Search and filters should rely on SQLite queries, not client-side scanning after full database load. +- Graph endpoint may cap memory item nodes and aggregate high-cardinality tags if needed. +- API endpoints should keep default responses small enough for responsive local browser rendering. + +### Security +- Bind to localhost by default. +- Do not expose the dashboard on external interfaces unless the user explicitly passes a different host. +- Do not log full memory contents unless the user requests debugging. +- Treat memory content as local sensitive developer data. +- Local HTTP responses must use JSON serialization and DOM text insertion patterns that avoid rendering memory content as HTML. + +### Reliability +- Missing database files should show an empty dashboard. +- Invalid query parameters should return JSON errors without crashing the server. +- Server shutdown should close database connections cleanly. +- Port conflicts should produce a clear error unless the user leaves `--port 0` enabled. +- Browser launch failures with `--open` should warn and keep the printed URL usable. diff --git a/docs/ai/implementation/2026-06-14-feature-memory-dashboard.md b/docs/ai/implementation/2026-06-14-feature-memory-dashboard.md new file mode 100644 index 00000000..1412d34c --- /dev/null +++ b/docs/ai/implementation/2026-06-14-feature-memory-dashboard.md @@ -0,0 +1,191 @@ +--- +phase: implementation +title: Implementation Guide +description: Technical implementation notes, patterns, and code guidelines +--- + +# Implementation Guide + +## Development Setup +- Active worktree: `.worktrees/feature-memory-dashboard` +- Branch: `feature-memory-dashboard` +- Feature docs validated with `npx ai-devkit@latest lint --feature memory-dashboard`. +- Memory package verification uses: + - `cd packages/memory && npx vitest run tests/integration/list.test.ts --config vitest.config.ts` + - `cd packages/memory && npx vitest run tests/integration/summary.test.ts --config vitest.config.ts` + - `cd packages/memory && npm run typecheck` + - `cd packages/memory && npm test` +- Memory dashboard package verification uses: + - `cd packages/memory-dashboard && npm test` + - `cd packages/memory-dashboard && npm run typecheck` + - `cd packages/memory-dashboard && npm run build` + +## Code Structure +### Implemented +- `packages/memory/src/handlers/list.ts` + - Adds `listKnowledge()` for bounded read-only memory listing. + - Supports optional text query, scope filter, tag filters, limit, offset, and sort. +- `packages/memory/src/handlers/summary.ts` + - Adds `getKnowledgeSummary()` for total, scope, tag, and recency counts. +- `packages/memory/src/types/index.ts` + - Adds `ListKnowledgeInput`, `ListKnowledgeResult`, `ListKnowledgeSort`, and `KnowledgeSummaryResult`. +- `packages/memory/src/api.ts` + - Exports `listKnowledge`, `getKnowledgeSummary`, `memoryListCommand()`, and `memorySummaryCommand()`. +- `packages/memory/tests/integration/list.test.ts` + - Covers bounded list output, normalized fields, query filtering, scope filtering, and tag filtering. +- `packages/memory/tests/integration/summary.test.ts` + - Covers empty summary output plus scope, tag, and recency counts. +- `packages/memory-dashboard` + - Adds the `@ai-devkit/memory-dashboard` package scaffold, Nx project config, TypeScript/SWC/Vitest config, README, and AI DevKit plugin manifest. +- `packages/memory-dashboard/src/command.ts` + - Registers `memory-dashboard` with `--host`, `--port`, and `--open`. + - Resolves the configured memory database path through `runtime.getMemoryDbPath()`. + - Starts the dashboard server and prints the URL. +- `packages/memory-dashboard/src/standalone.ts` + - Adds a package-local standalone launcher for dashboard development without installing the plugin into AI DevKit. + - Resolves the same default/configured memory database path as the plugin runtime. + - Supports `--host`, `--port`, `--open`, and `--db-path`. +- `packages/memory-dashboard/src/server.ts` + - Starts a localhost HTTP server. + - Serves dashboard HTML from `src/ui/dashboard.html` at `/`. + - Serves static Tailwind CSS and JavaScript assets from `/assets/styles.css` and `/assets/app.js`. + - Serves the local Cytoscape browser module from `/assets/vendor/cytoscape.esm.min.mjs`. + - Serves `/favicon.ico` as a no-content response to avoid noisy browser errors. + - Serves `/api/memory`, `/api/summary`, and `/api/graph`. + - Returns JSON errors for invalid requests. +- `packages/memory-dashboard/tests/command.test.ts` + - Covers command option parsing, help output, and runtime memory DB path usage. +- `packages/memory-dashboard/tests/standalone.test.ts` + - Covers standalone memory DB path resolution, configured relative paths, `--db-path`, and option parsing. +- `packages/memory-dashboard/tests/server.test.ts` + - Covers HTML serving, static Tailwind/client asset contents, Cytoscape integration markers, favicon behavior, memory list API, summary API, graph API, invalid query handling, missing DB empty responses, and shutdown. +- `packages/memory-dashboard/src/ui/dashboard.html` + - Contains the split dashboard shell with Tailwind utility classes. +- `packages/memory-dashboard/src/ui/tailwind.css` + - Contains Tailwind v4 input plus component classes for memory rows, summary cards, detail text, and graph canvas sizing. +- `packages/memory-dashboard/src/ui/app.js` + - Contains the browser controller for URL state, API loading, list/detail rendering, and Cytoscape graph rendering/interactions. +- `packages/cli/src/__tests__/services/plugin/memory-dashboard-package.test.ts` + - Covers the concrete plugin package manifest contract. + +## Implementation Notes +### Read-Only Memory APIs +- `memoryListCommand()` follows existing command-helper patterns: it opens the configured DB path, delegates to a handler, and closes the singleton database connection in `finally`. +- `listKnowledge()` keeps filtering in SQLite. It uses FTS when a text query is present, exact scope matching after `normalizeScope()`, and normalized tag filters. +- List limits are clamped between `1` and `200`; offset is clamped to a non-negative bounded value. +- `getKnowledgeSummary()` returns deterministic scope and tag ordering so dashboard facets and tests are stable. +- Recency buckets are fixed as `today`, `week`, `month`, and `older`. + +### Plugin Package and Server +- `@ai-devkit/memory-dashboard` follows the existing package conventions: `project.json`, SWC build, declaration-only `tsc`, Vitest, and package-local ESLint config. +- The plugin manifest lives in `package.json` under `aiDevkit.commands` and points at `./dist/command.js`. +- `createMemoryDashboardAction()` is injectable so tests can verify command behavior without leaving a long-running server process. +- `npm run build` now compiles TypeScript, emits declarations, builds Tailwind CSS into `dist/ui/styles.css`, and copies `dashboard.html` and `app.js` into `dist/ui`. +- `npm run dev:standalone` builds the package and runs `node dist/standalone.js`, which is the preferred local development path when the plugin has not been installed into AI DevKit. +- The server binds to the requested host/port and reports the actual assigned port when `--port 0` is used. +- `/api/graph` derives memory, tag, and scope nodes from a bounded filtered result set, defaults to 250 memory items, and emits `has-tag` and `in-scope` edges. +- The dashboard shell uses a desktop workbench layout with top search/scope/tag/group controls, summary stats, a 4-row paginated records rail, and an interactive Cytoscape graph/detail workspace in the first fold. +- The client script keeps search, scope, tag, and group state in URL query parameters through `URLSearchParams` and `history.replaceState`. +- The client fetches summary, list, and graph data after state changes, then re-renders summary cards, facets, grouped records, selected detail, and Cytoscape graph output. +- Cytoscape uses semantic memory/tag/scope node styling, a natural force-directed relationship layout, selected-neighborhood highlighting/refocus, Fit/Zoom/Layout controls, resize-aware fitting, and graph/list selection sync. Graph memory nodes carry the full item payload so selecting an off-page graph memory still renders details. +- The memory list uses `limit`/`offset` pagination with a default page size of 4 records; the current page is reflected in the URL. + +## Integration Points +- Dashboard server code should import `memoryListCommand()` and `memorySummaryCommand()` or the lower-level handlers through `@ai-devkit/memory`. +- The command wrappers are the best fit when the dashboard has only a DB path and wants connection cleanup handled by the memory package. +- In this worktree, `npm install --ignore-scripts` was needed to create local workspace links so `@ai-devkit/memory-dashboard` tests resolve the updated local `@ai-devkit/memory` package. `npm rebuild better-sqlite3` was then required because scripts were skipped during install. + +## Error Handling +- Invalid list inputs throw the existing `ValidationError`. +- Malformed stored tag JSON maps to an empty tag array rather than breaking dashboard reads. + +## Performance Considerations +- List responses are bounded by default and capped at `200`. +- Summary currently reads rows once to count tags and recency buckets. This is acceptable for MVP, but high-volume memory databases may need SQL-side JSON expansion or cached facets later. + +## Security Notes +- New APIs are read-only from the dashboard perspective. +- Existing database initialization may create a missing memory DB file before reads, matching the current memory package behavior. + +## Phase 7 Implementation Check +- Requirements alignment: the package lives under `packages/memory-dashboard`, declares `@ai-devkit/memory-dashboard`, registers `memory-dashboard`, resolves the memory DB path through runtime/standalone config, launches a localhost dashboard, and remains read-only. +- Design alignment: server-side memory access stays in `@ai-devkit/memory`; the dashboard server exposes `/api/memory`, `/api/summary`, and `/api/graph`; the static Tailwind/Cytoscape UI implements search, scope/tag filters, grouping, 4-row pagination, detail, graph fit/zoom/layout, and selected-neighborhood refocus. +- Verified design deviation: graph memory nodes include an optional `item` payload beyond the original graph shape so selecting an off-page graph memory can render full details without changing the visible list page. The design doc now records this payload. +- Residual implementation concerns: none blocking. Remaining items are release-readiness decisions: publishing cadence, whether browser smoke should become an automated gate, and graph scalability beyond the 250-item default graph cap. + +## Phase 9 Review Check +- Final review found and fixed one release-alignment issue: `--open` now attempts to open the printed dashboard URL through a platform browser launcher and warns only when that launch fails. +- API and contract review found no blocking issues: memory dashboard endpoints remain read-only, bind through the requested host/port, use the configured memory DB path, and keep list and graph responses bounded by default. +- Security review found no direct HTML rendering of memory content; the client inserts memory fields with DOM text APIs. +- Remaining non-blocking release checks are manual: installed plugin command help, real default memory DB launch, empty/missing DB browser smoke, and keyboard focus review. + +## Manual Smoke Workflow +- Build the package with `cd packages/memory-dashboard && npm run build`. +- For local development without plugin installation, run `cd packages/memory-dashboard && npm run dev:standalone`. +- To use a fixture DB, run `cd packages/memory-dashboard && npm run dev:standalone -- --db-path /tmp/memory.db --port 3000`. +- Launch through a plugin runtime or package-local helper with the runtime memory DB path, defaulting to `127.0.0.1` and an available port. +- Confirm `npm run dev:standalone -- --help` exposes `--host`, `--port`, `--db-path`, and `--open`. +- Confirm `memory-dashboard --help` exposes `--host`, `--port`, and `--open` after plugin installation. +- Open the printed local URL and verify summary cards, scope/tag facets, grouped records, detail view, and Cytoscape graph render. +- Click memory rows and graph controls to verify selection highlighting, detail sync, Fit, Zoom In, Zoom Out, and Layout behavior. +- Use Next/Previous pagination to verify list pages advance without list scrolling and preserve page state in the URL. +- Search for a known memory title, select scope and tag filters, switch group modes between scope/tag/recency, and confirm the URL query string tracks state. +- Search for a no-match term to confirm the empty list, neutral detail prompt, and empty graph state. +- Repeat at desktop and narrow mobile widths and check the browser console for errors. + +## Verification Evidence +- Red step for list API: `cd packages/memory && npx vitest run tests/integration/list.test.ts --config vitest.config.ts` failed because `memoryListCommand is not a function`. +- Green step for list API: same command exited `0`, `1 passed`, `2 passed`. +- Red step for summary API: `cd packages/memory && npx vitest run tests/integration/summary.test.ts --config vitest.config.ts` failed because `memorySummaryCommand is not a function`. +- Green step for summary API: same command exited `0`, `1 passed`, `2 passed`. +- Package typecheck: `cd packages/memory && npm run typecheck` exited `0`. +- Package tests: `cd packages/memory && npm test` exited `0`, `10 passed`, `109 passed`. +- CLI memory regression: `cd packages/cli && npx vitest run src/__tests__/commands/memory.test.ts --config vitest.config.ts` exited `0`, `1 passed`, `10 passed`. +- Plugin package manifest red step: `cd packages/cli && npx vitest run src/__tests__/services/plugin/memory-dashboard-package.test.ts --config vitest.config.ts` failed because `packages/memory-dashboard/package.json` did not exist. +- Plugin package manifest green step: same command exited `0`, `1 passed`, `1 passed`. +- Command red step: `cd packages/memory-dashboard && npx vitest run tests/command.test.ts --config vitest.config.ts` failed because `createMemoryDashboardAction is not a function`. +- Command green step: same command exited `0`, `1 passed`, `2 passed`. +- Server red step: `cd packages/memory-dashboard && npx vitest run tests/server.test.ts --config vitest.config.ts` failed because the server was still a stub that did not bind a socket. +- Server green step: same command exited `0`, `1 passed`, `3 passed`. +- Memory dashboard package tests: `cd packages/memory-dashboard && npm test` exited `0`, `2 passed`, `5 passed`. +- Memory dashboard typecheck: `cd packages/memory-dashboard && npm run typecheck` exited `0`. +- Memory dashboard build: `cd packages/memory-dashboard && npm run build` exited `0`, `Successfully compiled: 3 files with swc`. +- Dashboard shell red step: `cd packages/memory-dashboard && npx vitest run tests/server.test.ts --config vitest.config.ts` failed because placeholder HTML did not include required controls. +- Dashboard shell green step: same command exited `0`, `1 passed`, `3 passed`. +- Favicon red step: `cd packages/memory-dashboard && npx vitest run tests/server.test.ts --config vitest.config.ts` failed because `/favicon.ico` returned `404`. +- Favicon green step: same command exited `0`, `1 passed`, `4 passed`. +- Client graph-label red step: `cd packages/memory-dashboard && npx vitest run tests/server.test.ts --config vitest.config.ts` failed because the client asset lacked `truncateGraphLabel()`. +- Client graph-label green step: same command exited `0`, `1 passed`, `4 passed`. +- Browser smoke: launched a built dashboard package against a seeded temporary memory database at `http://127.0.0.1:41739`. +- Browser smoke initial load: Playwright verified summary cards, scope/tag facets, grouped memory rows, detail view, SVG graph nodes/edges, and zero console errors. +- Browser smoke search: entering `alpha` updated the URL to `?query=alpha`, narrowed the list to `Dashboard smoke alpha memory`, updated detail, and reduced graph nodes. +- Browser smoke scope filter: selecting `project:ai-devkit` updated the URL with `scope=project%3Aai-devkit` and kept only matching records. +- Browser smoke tag/grouping: selecting tag `graph` and group `tag` updated the URL to `?tag=graph&group=tag`, rendered `dashboard` and `graph` sections, and showed the beta record in both tag groups. +- Browser smoke recency grouping: selecting group `recency` rendered the `today` section for the seeded record. +- Browser smoke empty state: searching `nomatch-memory-dashboard` rendered the no-results message, neutral detail prompt, and empty graph region without overlap. +- Browser smoke responsive layout: desktop `1440x1000` and mobile `390x844` screenshots showed stacked controls and non-overlapping graph labels; current console errors were `0` in both views. +- Final memory dashboard package tests: `cd packages/memory-dashboard && npm test` exited `0`, `2 passed`, `8 passed`. +- Final memory dashboard typecheck: `cd packages/memory-dashboard && npm run typecheck` exited `0`. +- Final memory dashboard build: `cd packages/memory-dashboard && npm run build` exited `0`, `Successfully compiled: 3 files with swc`. +- Final memory package tests: `cd packages/memory && npm test` exited `0`, `10 passed`, `109 passed`. +- Final CLI plugin manifest test: `cd packages/cli && npx vitest run src/__tests__/services/plugin/memory-dashboard-package.test.ts --config vitest.config.ts` exited `0`, `1 passed`, `1 passed`. +- Feature lint: `npx ai-devkit@latest lint --feature memory-dashboard` exited `0`, all checks passed. +- Command help and missing DB coverage is included in the final memory dashboard package test run. +- Standalone red step: `cd packages/memory-dashboard && npx vitest run tests/standalone.test.ts --config vitest.config.ts` failed because `../src/standalone` did not exist. +- Standalone green step: same command exited `0`, `1 passed`, `4 passed`. +- Standalone script help: `cd packages/memory-dashboard && npm run dev:standalone -- --help` exited `0`, built 4 files, and printed `--host`, `--port`, `--db-path`, and `--open`. +- Standalone launch smoke: `cd packages/memory-dashboard && npm run dev:standalone -- --db-path /tmp/ai-devkit-memory-dashboard-standalone-smoke.db --port 41740` built 4 files and printed `Memory dashboard: http://127.0.0.1:41740`; the smoke server was then stopped. +- Final memory dashboard package tests after standalone launcher: `cd packages/memory-dashboard && npm test` exited `0`, `3 passed`, `12 passed`. +- Final memory dashboard typecheck after standalone launcher: `cd packages/memory-dashboard && npm run typecheck` exited `0`. +- Final memory dashboard build after standalone launcher: `cd packages/memory-dashboard && npm run build` exited `0`, `Successfully compiled: 4 files with swc`. +- Cytoscape/Tailwind red step: `cd packages/memory-dashboard && npx vitest run tests/server.test.ts --config vitest.config.ts` failed because the old embedded SVG HTML lacked `graph-selected-title`, `graph-fit`, and Cytoscape client markers. +- Cytoscape/Tailwind green step: same command exited `0`, `1 passed`, `5 passed`. +- Final memory dashboard package tests after Cytoscape/Tailwind split: `cd packages/memory-dashboard && npm test` exited `0`, `3 passed`, `12 passed`. +- Final memory dashboard typecheck after Cytoscape/Tailwind split: `cd packages/memory-dashboard && npm run typecheck` exited `0`. +- Final memory dashboard build after Cytoscape/Tailwind split: `cd packages/memory-dashboard && npm run build` exited `0`, `Successfully compiled: 5 files with swc`; Tailwind v4.3.1 completed successfully. +- Browser smoke after Cytoscape/Tailwind split: launched `http://127.0.0.1:41741` with three seeded records, verified `#memory-graph` contained three Cytoscape canvases, graph panel measured about `606x620` on desktop, and current console errors/warnings were `0`. +- Browser smoke interaction after Cytoscape/Tailwind split: clicking `Tailwind split frontend memory` updated `#graph-selected-title`, detail title, and selected row state; Fit and Layout buttons ran with `0` console errors/warnings. +- Browser smoke mobile after Cytoscape/Tailwind split: at `390x844`, filters, list, graph controls, graph canvas, and detail stacked cleanly with `0` console errors/warnings. +- Superseded desktop bounded-list smoke: launched `http://127.0.0.1:41742` with 23 seeded records; Playwright verified graph canvas count `3`, graph panel about `606x664`, Zoom In/Zoom Out/Fit/Layout controls present, and `0` console errors/warnings before the records rail changed to all-entry internal scrolling. +- Superseded desktop bounded-list interaction smoke: clicking the old list navigation changed the URL and graph controls ran with `0` console errors/warnings before the records rail changed to all-entry internal scrolling. +- Desktop workbench/refocus smoke: launched `http://127.0.0.1:41744` with 23 seeded records; Playwright verified 4 visible rows, no list scrolling, pagination `1-4 of 23`, summary inline with the H1, graph memory count `23`, selecting an off-page graph memory rendered that memory detail, and current console errors/warnings were `0`. diff --git a/docs/ai/planning/2026-06-14-feature-memory-dashboard.md b/docs/ai/planning/2026-06-14-feature-memory-dashboard.md new file mode 100644 index 00000000..1df9fb9d --- /dev/null +++ b/docs/ai/planning/2026-06-14-feature-memory-dashboard.md @@ -0,0 +1,162 @@ +--- +phase: planning +title: Project Planning & Task Breakdown +description: Break down work into actionable tasks and estimate timeline +--- + +# Project Planning & Task Breakdown + +## Milestones +- [x] Milestone 1: Read-only memory data APIs +- [x] Milestone 2: Installable dashboard plugin command and local server +- [x] Milestone 3: Dashboard UI for browse, search, filters, grouping, detail, and graph +- [x] Milestone 4: Integration, browser smoke, and release readiness + +## Task Breakdown + +### Milestone 1: Read-Only Memory Data APIs +- [x] Task 1.1: Add list query types and exported result types to `@ai-devkit/memory`. + - Outcome: `ListKnowledgeInput`, `ListKnowledgeResult`, `KnowledgeSummaryResult`, and graph-ready item types are available to the dashboard package. + - Dependencies: Existing `KnowledgeItem`, `KnowledgeRow`, and command API exports. + - Validation: Typecheck and memory package tests compile against the new exported types. + - Related tests: Memory Read APIs list and empty database scenarios. +- [x] Task 1.2: Implement bounded `listKnowledge` read helper. + - Outcome: Server-side memory list API supports optional text query, scope filter, tag filters, limit, offset, and sort. + - Dependencies: Existing database connection, FTS query builder, row mapping, and schema. + - Validation: Unit tests cover query, scope, tags, limit/offset, sort, and empty results. + - Related tests: List returns item fields, query search, scope filter, tag filter, limit/offset. +- [x] Task 1.3: Implement `getKnowledgeSummary` read helper. + - Outcome: Summary API returns total item count plus scope, tag, and recency buckets. + - Dependencies: Task 1.1 and existing `knowledge` table shape. + - Validation: Unit tests cover scope counts, tag counts, recency counts, no-tag rows, and empty database. + - Related tests: Summary counts by scope, tag, recency bucket. +- [x] Task 1.4: Preserve existing memory command behavior. + - Outcome: Existing `memory search/store/update` CLI and tests remain unchanged from the user's perspective. + - Dependencies: Tasks 1.1-1.3. + - Validation: Existing memory package tests and CLI memory command tests pass. + - Related tests: Regression of adjacent features. + +### Milestone 2: Plugin Package and Local Server +- [x] Task 2.1: Scaffold `packages/memory-dashboard` as `@ai-devkit/memory-dashboard`. + - Outcome: Package has `package.json`, `project.json`, TypeScript config, build/test scripts, exports, files list, and an `aiDevkit.commands` manifest for `memory-dashboard`. + - Dependencies: Existing monorepo workspace and plugin manifest contract. + - Validation: Package builds and plugin manifest validation accepts the generated package after build. + - Related tests: Package manifest declares `memory-dashboard` with a JavaScript entrypoint. +- [x] Task 2.2: Implement plugin command registration. + - Outcome: `register(command, runtime)` wires `--host`, `--port`, and `--open`, resolves `runtime.getMemoryDbPath()`, starts the server, and prints the URL. + - Dependencies: Task 2.1 and existing `AiDevkitRuntime`. + - Validation: Commander unit tests verify command options, runtime memory path usage, and help output. + - Related tests: Plugin command option parsing and `memory-dashboard --help`. +- [x] Task 2.3: Implement local HTTP server and routing. + - Outcome: Server binds to `127.0.0.1` by default, serves static assets, and exposes `/api/memory`, `/api/summary`, and `/api/graph`. + - Dependencies: Tasks 1.2, 1.3, and 2.2. + - Validation: Server route unit tests cover success, invalid query parameters, shutdown, and JSON error responses. + - Related tests: Dashboard Server endpoint scenarios. +- [x] Task 2.4: Implement graph response builder. + - Outcome: API derives memory, tag, and scope nodes plus `has-tag` and `in-scope` edges from bounded filtered results. + - Dependencies: Task 2.3 and list API result shape. + - Validation: Unit tests cover no tags, high-cardinality tag limits, filtered graph results, and empty graph. + - Related tests: `GET /api/graph` and graph-bound performance scenarios. + +### Milestone 3: Dashboard UI +- [x] Task 3.1: Build static dashboard shell. + - Outcome: Framework-free HTML/CSS/TypeScript UI with toolbar, filter panel, memory list/detail area, summary panel, and graph panel. + - Dependencies: Task 2.3 static asset serving. + - Validation: Server tests verify shell markup and assets; Playwright smoke confirmed the desktop dashboard renders without text overlap. + - Related tests: Client initial load, empty state, desktop layout manual testing. +- [x] Task 3.2: Implement client data loading and URL state. + - Outcome: Client fetches summary, memory list, and graph data; search/filter/grouping state survives refresh through URL query parameters. + - Dependencies: Tasks 2.3 and 3.1. + - Validation: Browser smoke covered search, scope filter, tag filter, and URL query state updates. + - Related tests: Search input, scope filter, tag filter, integration API calls. +- [x] Task 3.3: Implement grouping and detail views. + - Outcome: User can group by scope, tag, or recency and select an item to inspect full details. + - Dependencies: Task 3.2. + - Validation: Browser smoke covers grouping headers and selected item details. + - Related tests: Grouping by scope/tag/recency and selecting memory item. +- [x] Task 3.4: Implement Cytoscape graph rendering and interactions. + - Outcome: Graph view renders non-empty item, tag, and scope nodes from API data, with natural force-directed layout, zoom/fit/layout controls, selection highlighting/refocus, and graph memory-node detail rendering. + - Dependencies: Task 2.4 and 3.2. + - Validation: Server tests assert graph payload shape and memory item payloads; Playwright smoke verified Cytoscape canvas rendering, graph controls, off-page graph memory selection, and empty graph state for empty data. + - Related tests: Graph renders nodes and edges from API data. + +### Milestone 4: Integration and Release Readiness +- [x] Task 4.1: Add deterministic fixture memory database setup. + - Outcome: Tests seed temporary memory databases with global/project/repo-style records and overlapping tags through `memoryStoreCommand`; a shared helper was not needed for the MVP test surface. + - Dependencies: Memory package database helpers and Task 1 APIs. + - Validation: Memory and dashboard integration tests create populated and empty databases deterministically, clean up SQLite sidecar files, and verify API behavior against seeded data. + - Related tests: Test Data requirements. +- [x] Task 4.2: Add server integration tests. + - Outcome: Integration tests start the dashboard server against fixture DBs and verify `/api/memory`, `/api/summary`, and `/api/graph`. + - Dependencies: Tasks 2.3, 2.4, and 4.1. + - Validation: Dashboard package integration test target passes. + - Related tests: Integration Tests section. +- [x] Task 4.3: Add browser smoke coverage. + - Outcome: Playwright or repo-available browser tooling validates launch, list rendering, search, tag filtering, grouping, and graph rendering. + - Dependencies: Tasks 3.1-3.4 and 4.1. + - Validation: Manual Playwright smoke passes locally against a seeded temporary DB; automated browser smoke can be added later if release gating needs it. + - Related tests: End-to-End Tests section. +- [x] Task 4.4: Run package and feature verification. + - Outcome: Relevant Nx build/test targets pass for memory, memory-dashboard, and affected CLI/plugin behavior. + - Dependencies: All implementation tasks. + - Validation: `npx ai-devkit@latest lint --feature memory-dashboard`, package builds, package tests, and relevant CLI tests pass. + - Related tests: Test Reporting & Coverage and Manual Testing. +- [x] Task 4.5: Document manual smoke workflow. + - Outcome: Implementation doc records how to run `ai-devkit memory-dashboard --help`, launch the dashboard, test empty/missing DB behavior, and inspect layout manually. + - Dependencies: Task 4.4. + - Validation: Manual checklist is executable and references real commands. + - Related tests: Manual Testing section. + +## Dependencies +- Task 1 APIs must land before server endpoints can avoid direct SQLite duplication. +- Package scaffold must land before command registration, server build output, and plugin manifest validation. +- Server endpoints must land before client data loading and graph rendering. +- Fixture helpers should be shared by memory API tests, server integration tests, and browser smoke tests. +- Browser smoke depends on a built or runnable dashboard package. + +## Timeline & Estimates +- Milestone 1: medium, because query/filter/summary behavior touches existing memory internals and regression tests. +- Milestone 2: medium, because plugin packaging and long-running local server behavior need careful command tests. +- Milestone 3: medium-large, because the dashboard has several interactive states and responsive layout requirements. +- Milestone 4: medium, because integration and browser smoke tests need deterministic fixture data and process cleanup. + +## Risks & Mitigation +| Risk | Impact | Mitigation | +|---|---|---| +| New memory list APIs regress existing search behavior | High | Keep existing search/store/update tests passing and isolate list helpers from command helpers. | +| Dashboard package duplicates memory database internals | Medium | Keep database reads in `@ai-devkit/memory` APIs and import those APIs from the server only. | +| Long-running server command hangs tests | Medium | Expose server creation/start helpers that tests can start and close explicitly. | +| Browser auto-open is brittle in agent/CI sessions | Low | Keep URL-only default and make `--open` opt-in. | +| Large memory databases make graph unusable | Medium | Bound list and graph endpoints; aggregate or cap graph nodes. | +| Static UI grows hard to maintain | Medium | Keep modules separated by API client, state, grouping, graph, and rendering. Revisit framework choice after MVP. | + +## Resources Needed +- Existing `@ai-devkit/memory` package APIs, database schema, and test helpers. +- Existing CLI plugin manifest/runtime contract. +- Nx package build/test patterns from existing packages. +- Browser smoke tooling available in the repository or added in a minimal package-local way. +- Manual local browser validation after implementation. + +## Coverage Matrix +| Testing scenario | Covered by tasks | +|---|---| +| Memory list fields/query/scope/tag/limit/offset | 1.1, 1.2, 4.4 | +| Summary by scope/tag/recency | 1.3, 4.2 | +| Empty database and missing database | 1.2, 1.3, 2.3, 3.1, 4.1, 4.2 | +| Plugin manifest and command options | 2.1, 2.2, 4.4 | +| Server routes and invalid parameters | 2.3, 4.2 | +| Graph data and graph rendering | 2.4, 3.4, 4.2, 4.3 | +| Search, filters, grouping, detail UI | 3.2, 3.3, 4.3 | +| Desktop layout and no text overlap | 3.1, 4.3, 4.5 | +| Existing memory CLI regression | 1.4, 4.4 | + +## Follow-Up Checks +- Confirm whether `packages/memory-dashboard` should be published with the same release cadence as other first-party packages. +- Revisit edit/delete/merge workflows only after the read-only dashboard has passing smoke coverage. +- Revisit React/Vite or another UI framework only if static UI maintenance cost becomes visible during implementation. + +## Phase 6 Planning Update +Implementation has completed the MVP task set: read-only memory APIs, plugin package/server, Tailwind static dashboard, Cytoscape graph, paginated records, graph-node detail rendering, package-local standalone dev script, integration tests, browser smoke, and lifecycle lint are in place. The original shared fixture-helper task was reconciled as deterministic inline fixture setup because the current tests can seed and clean temporary databases without introducing a separate abstraction. Current residual risks are release-readiness review items rather than open implementation tasks: confirm package publishing cadence, decide whether automated browser smoke should become a release gate, and review graph scalability beyond the 250-item default graph cap. + +## Phase 8 Testing Update +Testing reconciliation is complete for the current MVP scope. The dashboard package now has 22 automated tests across command, standalone, and server behavior, including coverage for plugin options, browser-open success/failure handling, invalid ports, standalone config edge cases, static assets, paginated APIs, missing databases, graph memory payloads, invalid routes, invalid sorts, and the 250-item graph default. Coverage passes the configured threshold with branch coverage at 84.68%. Remaining checks are release/manual items: installed plugin command help, real default memory DB launch, browser launch against an empty or missing DB, and keyboard focus review. diff --git a/docs/ai/requirements/2026-06-14-feature-memory-dashboard.md b/docs/ai/requirements/2026-06-14-feature-memory-dashboard.md new file mode 100644 index 00000000..f981454e --- /dev/null +++ b/docs/ai/requirements/2026-06-14-feature-memory-dashboard.md @@ -0,0 +1,121 @@ +--- +phase: requirements +title: Requirements & Problem Understanding +description: Clarify the problem space, gather requirements, and define success criteria +--- + +# Requirements & Problem Understanding + +## Problem Statement +AI DevKit memory is currently manageable through CLI commands such as `memory search`, `memory store`, and `memory update`. That works for precise command-line usage, but it is a poor fit for browsing, assessing, grouping, and understanding the shape of accumulated knowledge over time. + +Users need a lightweight local web dashboard that can be installed as an AI DevKit plugin package, launched from the AI DevKit CLI, and used to inspect memory records without manually querying SQLite or repeatedly crafting CLI searches. + +## Goals & Objectives +### Primary Goals +- Create a new package under `packages/` named `@ai-devkit/memory-dashboard`. +- Ship it as an installable AI DevKit plugin using the existing `package.json` `aiDevkit.commands` contract. +- Add a plugin command named `memory-dashboard` that launches a local web application. +- Provide a minimum read-only dashboard for browsing and assessing memory records. +- Reuse the configured AI DevKit memory database path via the plugin runtime `getMemoryDbPath()`. +- Support search, filtering, grouping, and a basic graph view in the first usable version. + +### Secondary Goals +- Keep the first release local-only and dependency-light enough to fit the monorepo's existing Node/Nx package style. +- Provide a concrete first-party plugin package that exercises the plugin foundation with a real user-facing tool. +- Keep the dashboard useful against both an existing populated memory database and a new empty database. + +### Non-Goals +- Hosted, multi-user, or remote dashboard deployment. +- Authentication beyond binding to localhost for the local development server. +- Editing, deleting, merging, or bulk importing memories in the first release. +- Replacing the existing memory CLI or MCP server. +- Full semantic clustering, embeddings, or AI-generated summaries. +- Project-local plugin installation behavior beyond the existing global plugin system. + +## User Stories & Use Cases +- As an AI DevKit user, I want to run `ai-devkit memory-dashboard` so that I can open a local UI for memory without leaving the AI DevKit toolchain. +- As an AI DevKit user, I want to search memory by text so that I can find relevant records without remembering exact CLI flags. +- As an AI DevKit user, I want to filter by scope and tags so that I can assess project-specific and global knowledge separately. +- As an AI DevKit user, I want to group memories by scope, tag, and recency so that I can spot stale, overloaded, or underused areas. +- As an AI DevKit user, I want to inspect a memory's title, content, tags, scope, and timestamps so that I can judge whether it is still useful. +- As an AI DevKit user, I want a graph view of memory relationships so that I can see connections between scopes, tags, and memory items. +- As a plugin developer, I want this package to follow the existing plugin manifest and runtime contract so that it is a concrete first-party plugin example. + +### Minimum Workflows +1. **Install Plugin** + - User installs the package with `ai-devkit plugin add @ai-devkit/memory-dashboard` once the package is published or otherwise available to npm. + - AI DevKit validates the package manifest and registers the `memory-dashboard` command. +2. **Launch Dashboard** + - User runs `ai-devkit memory-dashboard`. + - The command resolves the configured memory database path from the AI DevKit runtime. + - The command starts a localhost-only HTTP server and prints the dashboard URL. + - If `--open` is provided, the command attempts to open the browser when the host environment supports it. +3. **Browse and Assess Memory** + - Dashboard loads memory summary statistics and a paginated or bounded list. + - User searches by text, filters by tags/scope, and opens an item detail view. + - User groups results by scope, tag, or recency. +4. **View Memory Graph** + - Dashboard shows a basic graph connecting memory items to tag and scope nodes. + - Selecting a node filters or highlights related memory items. + +### Edge Cases +- Memory database path is configured but the file does not exist yet. +- Memory database exists but contains no records. +- Memory database contains many records with high-cardinality tags. +- Memory records have no tags. +- Search query has no matches. +- Configured memory database path is invalid or not readable. +- Requested dashboard port is already in use. +- Browser auto-open is unavailable in the current environment. + +## Success Criteria +### Acceptance Criteria +- `packages/memory-dashboard` exists with package name `@ai-devkit/memory-dashboard`. +- The package declares an AI DevKit plugin manifest with a `memory-dashboard` JavaScript entrypoint. +- `ai-devkit memory-dashboard --help` is available after plugin registration. +- The command can launch a localhost dashboard using the memory DB path returned by `runtime.getMemoryDbPath()`. +- The dashboard lists memory items with title, scope, tags, created date, and updated date. +- The dashboard supports text search using the existing memory search behavior or equivalent SQLite FTS query behavior. +- The dashboard supports filtering by scope and tag. +- The dashboard supports grouping by scope, tag, and recency bucket. +- The dashboard renders a basic graph containing memory item, tag, and scope nodes. +- Empty databases and missing memory databases produce a useful empty state instead of a crash. +- The feature is covered by unit tests for data access/filtering/grouping and command registration behavior. +- The feature is covered by at least one integration or browser-level smoke test for dashboard rendering. + +### Performance Expectations +- The default memory list response is bounded and does not load the full database for large datasets. +- Search and filter operations are served by database queries or memory-package helpers, not by requiring a full client-side scan. +- The graph view remains usable by limiting or aggregating nodes when tag or memory counts are high. + +## Constraints & Assumptions +### Technical Constraints +- The dashboard package must live in `packages/`, matching the user's request and the monorepo workspace configuration. +- The plugin command name must not conflict with built-in commands; `memory-dashboard` satisfies the current plugin command naming rules. +- Plugin entrypoints must be built JavaScript files; the package may be authored in TypeScript but cannot require runtime TypeScript loading. +- The first release treats plugins as trusted local npm code, matching the existing plugin foundation. +- The memory package currently exposes store/search/update command helpers but does not expose a full list/stat/graph API. The dashboard feature may add read-only helper APIs to `@ai-devkit/memory` rather than reading SQLite from UI code directly. + +### Assumptions +- The first release is read-only to avoid accidental data loss while the dashboard's information architecture is validated. +- The graph can be derived from existing memory metadata: memory item -> tag and memory item -> scope edges. No new persisted graph schema is required for MVP. +- "Minimum requirement" means a useful local dashboard with core browse/search/filter/group/graph capabilities, not a full memory admin suite. + +## Questions & Open Items +### Resolved +- MVP scope is an installable local AI DevKit plugin package, not a built-in core CLI dashboard. +- MVP data access is read-only. +- MVP graph is derived from existing tags and scopes. +- The package lives under `packages/` and uses the existing plugin manifest contract. +- The dashboard launches from `ai-devkit memory-dashboard`. +- The command prints a URL by default and uses `--open` as an opt-in browser launch flag. + +### Alternatives Considered +- **Core CLI dashboard**: rejected because the user asked for an installable plugin and the plugin foundation exists. +- **Direct SQLite access only from the dashboard package**: rejected because it duplicates memory internals; add read-only memory APIs where needed. +- **Mutation-first memory manager**: rejected for MVP because browsing and assessment are the minimum useful baseline and mutation needs stronger safety UX. + +### Deferred +- Whether the web UI should use a static vanilla frontend or a bundled framework; the requirement is only that it remains local, installable, and maintainable in this monorepo. +- Editing, deleting, merging, and quality scoring memories after the read-only dashboard ships. diff --git a/docs/ai/testing/2026-06-14-feature-memory-dashboard.md b/docs/ai/testing/2026-06-14-feature-memory-dashboard.md new file mode 100644 index 00000000..1e0ff36a --- /dev/null +++ b/docs/ai/testing/2026-06-14-feature-memory-dashboard.md @@ -0,0 +1,122 @@ +--- +phase: testing +title: Testing Strategy +description: Define testing approach, test cases, and quality assurance +--- + +# Testing Strategy + +## Test Coverage Goals +- Unit test coverage target: 100% of new memory read helpers and dashboard server route logic. +- Integration coverage: plugin command registration, local server startup, API responses from a fixture memory database. +- Browser smoke coverage: dashboard renders the desktop workbench, filters, grouping, paginated records, detail panel, and graph from seeded data. +- Manual coverage: accessibility, desktop layout, graph controls, and command help output. + +## Unit Tests + +### Memory Read APIs +- [x] List returns memory items with id, title, content, tags, scope, createdAt, and updatedAt. +- [x] List supports query search without breaking existing search ranking behavior. +- [x] List filters by scope. +- [x] List filters by one or more tags. +- [x] List enforces limit and offset bounds. +- [x] Summary counts items by scope. +- [x] Summary counts items by tag. +- [x] Summary counts items by recency bucket. +- [x] Empty database returns empty item, facet, and graph-ready results. + +### Dashboard Server +- [x] Server binds to `127.0.0.1` by default. +- [x] `GET /` serves dashboard HTML. +- [x] `GET /api/memory` returns filtered list results. +- [x] `GET /api/summary` returns scope, tag, and recency counts. +- [x] `GET /api/graph` returns memory, tag, and scope nodes with edges. +- [x] Invalid query parameters return a clear error response. +- [x] Server shutdown closes resources. +- [x] Missing memory database path returns empty memory, summary, and graph payloads. + +### Plugin Command +- [x] Package manifest declares `memory-dashboard` with a JavaScript entrypoint. +- [x] `register(command, runtime)` wires options and action without conflicting with built-ins. +- [x] Command uses `runtime.getMemoryDbPath()` rather than hard-coding `~/.ai-devkit/memory.db`. +- [x] `--host`, `--port`, and `--open` are parsed correctly. +- [x] `memory-dashboard --help` includes host, port, and open options. +- [x] Package-local `npm run dev:standalone -- --help` launches the standalone entrypoint without requiring plugin installation. + +### Client UI +- [x] Initial load renders summary stats and memory rows. +- [x] Search input updates the memory list. +- [x] Scope filter updates the memory list. +- [x] Tag filter updates the memory list. +- [x] Grouping by scope, tag, and recency produces stable sections. +- [x] Selecting a memory item shows full details. +- [x] Graph renders nodes and edges from API data through Cytoscape canvas output. +- [x] Graph Fit, Zoom In, Zoom Out, and Layout controls run without console errors or warnings. +- [x] Selecting a memory row synchronizes the graph header, detail panel, selected row state, and selected-neighborhood graph refocus. +- [x] Selecting a graph memory node renders that memory detail, including when the node is not on the current list page. +- [x] List pagination limits visible rows to 4 and persists page state in the URL. +- [x] Empty state is visible and does not overlap controls. + +## Integration Tests +- [x] Start the dashboard server with a temporary seeded SQLite memory database. +- [x] Fetch `/api/memory` and verify seeded records are returned. +- [x] Fetch `/api/summary` and verify scope/tag counts. +- [x] Fetch `/api/graph` and verify item/tag/scope relationships. +- [x] Register the plugin command with a Commander instance and verify `memory-dashboard --help` output. + +## End-to-End Tests +- [x] Launch dashboard against a fixture database. +- [x] Load the dashboard in Playwright. +- [x] Search for a seeded memory title and verify the list narrows. +- [x] Filter by tag and verify only matching seeded records remain. +- [x] Switch grouping mode and verify group headers render. +- [x] Verify graph canvas contains non-empty rendered nodes. +- [x] Verify desktop Cytoscape graph layout fits the graph viewport and supports zoom controls. +- [x] Verify paginated list navigation advances records without requiring list scrolling. +- [x] Verify desktop first fold contains search/filter controls, records rail, graph, and selected memory detail. + +## Test Data +- Fixture memory database with at least: + - one `global` memory + - one `project:` memory + - one `repo:` memory + - overlapping tags across multiple records + - records with different created/updated timestamps for recency grouping +- Empty database fixture. +- Invalid query parameter cases for API error behavior. + +## Test Reporting & Coverage +- [x] Run package-level tests for `@ai-devkit/memory`. +- [x] Run package-level tests for `@ai-devkit/memory-dashboard`. +- [x] Run `npx ai-devkit@latest lint --feature memory-dashboard` before leaving lifecycle phases. +- [x] Run the relevant package build/test targets once implementation exists. +- [x] Run package build after Tailwind/Cytoscape changes and verify generated `dist/ui` assets. + +## Manual Testing +- [ ] Run `ai-devkit memory-dashboard --help`. +- [x] Run `npm run dev:standalone -- --help` from `packages/memory-dashboard`. +- [ ] Run dashboard with the default memory DB path. +- [ ] Run dashboard with an empty or missing memory DB path. +- [x] Verify the dashboard is usable in the desktop viewport targeted by this plugin. +- [x] Verify interactive graph Fit/Zoom/Layout controls and list-to-graph selection refocus. +- [ ] Verify keyboard focus reaches search, filters, memory rows, and graph controls. +- [x] Verify no text overlaps in the dashboard layout. + +## Performance Testing +- [ ] Seed at least 1,000 memory items and confirm the initial list remains responsive. +- [ ] Confirm API default limits prevent loading the full database into the browser. +- [x] Confirm graph rendering remains bounded by the 250-item default graph limit. + +## Phase 8 Test Execution +- `npx ai-devkit@latest lint --feature memory-dashboard` validated the feature docs and worktree state before test reconciliation. +- `packages/memory-dashboard/tests/server.test.ts` now covers the split HTML/Tailwind/JS assets, paginated API responses, missing database handling, favicon behavior, graph node payloads for selected memory detail, and the 250-item graph default. +- `packages/memory-dashboard/tests/command.test.ts` covers plugin command registration, host/port/open options, browser-open success/failure handling, runtime memory DB path resolution, and help output. +- `packages/memory-dashboard/tests/standalone.test.ts` covers package-local standalone path resolution, `--db-path`, and option parsing. +- `packages/memory/tests/integration/list.test.ts` and `packages/memory/tests/integration/summary.test.ts` cover the read-only memory APIs used by the dashboard. +- Current automated dashboard package result: 3 test files, 22 tests. +- Current dashboard coverage result: statements 90.47%, branches 84.68%, functions 83.72%, lines 90.47%; coverage thresholds pass. +- Remaining manual checks before release: run the installed plugin command help, run against the default real memory DB path, run against an empty/missing DB from the browser, and verify keyboard focus through controls and rows. + +## Bug Tracking +- Track implementation issues in the feature planning document. +- Any regression in existing `memory search/store/update` behavior blocks release. diff --git a/package-lock.json b/package-lock.json index 4748c813..1266d86a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai-devkit", - "version": "0.41.1", + "version": "0.41.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-devkit", - "version": "0.41.1", + "version": "0.41.2", "license": "MIT", "workspaces": [ "apps/*", @@ -34,6 +34,10 @@ "resolved": "packages/memory", "link": true }, + "node_modules/@ai-devkit/memory-dashboard": { + "resolved": "packages/memory-dashboard", + "link": true + }, "node_modules/@ai-devkit/pi-session-tracker": { "resolved": "packages/pi-session-tracker", "link": true @@ -4651,10 +4655,46 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -4665,13 +4705,17 @@ "android" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -4682,13 +4726,17 @@ "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], @@ -4699,13 +4747,17 @@ "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], @@ -4716,198 +4768,254 @@ "freebsd" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ - "arm64" + "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ - "s390x" + "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ - "wasm32" + "x64" ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, + "os": [ + "win32" + ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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==", + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "engines": { + "node": ">=0.10" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { + "node_modules/@rolldown/binding-darwin-arm64": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -4915,16 +5023,16 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { + "node_modules/@rolldown/binding-darwin-x64": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -4932,20 +5040,256 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@sec-ant/readable-stream": { + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "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.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "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, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", @@ -5254,29 +5598,317 @@ "@swc/counter": "^0.1.3" } }, - "node_modules/@telegraf/types": { - "version": "7.1.0", - "license": "MIT" - }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "node_modules/@tailwindcss/cli": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.1.tgz", + "integrity": "sha512-ZWPy20rF+TBfTImxDMG3Wr75Y3RpaPlo9lc+oJbInlMyjT+XPkTVKVIL5RZ7JirXuIahcfHoLNFRmDorKi+JQQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" + "@parcel/watcher": "2.5.1", + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "enhanced-resolve": "5.21.6", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.3.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "bin": { + "tailwindcss": "dist/index.mjs" } }, - "node_modules/@tokenizer/token": { + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "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.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@telegraf/types": { + "version": "7.1.0", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", @@ -5409,50 +6041,317 @@ "dev": true, "license": "MIT" }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "license": "ISC" - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", - "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", - "dev": true, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", "license": "MIT", "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.8", - "ast-v8-to-istanbul": "^1.0.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.2", - "obug": "^2.1.1", - "std-env": "^4.0.0-rc.1", - "tinyrainbow": "^3.1.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@vitest/browser": "4.1.8", - "vitest": "4.1.8" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", - "dev": true, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" + }, + "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/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" + }, + "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/@typescript-eslint/scope-manager": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "license": "MIT", + "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/@typescript-eslint/type-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "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/@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==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "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==", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" + }, + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "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==", + "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", + "license": "ISC" + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", "@vitest/spy": "4.1.8", "@vitest/utils": "4.1.8", "chai": "^6.2.2", @@ -6946,6 +7845,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/debug": { "version": "4.4.3", "license": "MIT", @@ -7143,6 +8051,20 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/enquirer": { "version": "2.3.6", "dev": true, @@ -8988,6 +9910,16 @@ "node": ">=8" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "6.1.3", "license": "MIT", @@ -9520,6 +10452,20 @@ "dev": true, "license": "MIT" }, + "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": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.54.0", "license": "MIT", @@ -9652,6 +10598,13 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-fetch": { "version": "2.7.0", "license": "MIT", @@ -11438,6 +12391,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "license": "MIT", @@ -11658,6 +12632,18 @@ "tree-kill": "cli.js" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-node": { "version": "10.9.2", "dev": true, @@ -12409,2016 +13395,333 @@ "node": ">=20.20.0" } }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", - "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", - "dev": true, + "packages/channel-connector": { + "name": "@ai-devkit/channel-connector", + "version": "0.9.0", "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/type-utils": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "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" + "marked": "^15.0.12", + "telegraf": "^4.16.3", + "uuid": "14.0.0" }, - "peerDependencies": { + "devDependencies": { + "@swc/cli": "^0.8.1", + "@swc/core": "^1.10.0", + "@types/node": "^20.11.5", + "@typescript-eslint/eslint-plugin": "^8.60.1", "@typescript-eslint/parser": "^8.60.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "@vitest/coverage-v8": "^4.1.8", + "eslint": "^8.56.0", + "typescript": "^5.5.0", + "vitest": "^4.1.8" + }, + "engines": { + "node": ">=20.20.0" } }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", - "dev": true, + "packages/channel-connector/node_modules/marked": { + "version": "15.0.12", "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" + "bin": { + "marked": "bin/marked.js" }, "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": ">= 18" } }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, + "packages/cli": { + "name": "ai-devkit", + "version": "0.41.2", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@ai-devkit/agent-manager": "0.19.1", + "@ai-devkit/channel-connector": "0.9.0", + "@ai-devkit/memory": "0.14.0", + "@inquirer/prompts": "^8.5.2", + "chalk": "^5.6.0", + "commander": "^11.1.0", "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" + "fs-extra": "^11.2.0", + "gray-matter": "^4.0.3", + "ink": "^7.0.4", + "ink-text-input": "^6.0.0", + "ora": "^9.0.0", + "react": "^19.2.0", + "smol-toml": "^1.6.1", + "uuid": "14.0.0", + "yaml": "^2.3.4", + "zod": "^3.25.76" }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "bin": { + "ai-devkit": "dist/cli.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "devDependencies": { + "@swc/cli": "^0.8.1", + "@swc/core": "^1.10.0", + "@types/debug": "^4.1.13", + "@types/fs-extra": "^11.0.4", + "@types/node": "^20.11.5", + "@types/react": "^19.2.0", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.60.1", + "@vitest/coverage-v8": "^4.1.8", + "eslint": "^8.56.0", + "ts-node": "^10.9.2", + "typescript": "^5.5.0", + "vitest": "^4.1.8" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=20.20.0" } }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, + "packages/cli/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, + "packages/cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", - "dev": true, + "packages/cli/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" + "restore-cursor": "^5.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "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" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, + "packages/cli/node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, - "license": "MIT", - "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" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/eslint-plugin/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": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, - "license": "MIT", - "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" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/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": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", - "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "packages/agent-manager/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "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" - } - }, - "packages/agent-manager/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" - } - }, - "packages/agent-manager/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "packages/agent-manager/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" - } - }, - "packages/agent-manager/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": { - "node": ">= 4" - } - }, - "packages/agent-manager/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" - } - }, - "packages/channel-connector": { - "name": "@ai-devkit/channel-connector", - "version": "0.9.0", - "license": "MIT", - "dependencies": { - "marked": "^15.0.12", - "telegraf": "^4.16.3", - "uuid": "14.0.0" - }, - "devDependencies": { - "@swc/cli": "^0.8.1", - "@swc/core": "^1.10.0", - "@types/node": "^20.11.5", - "@typescript-eslint/eslint-plugin": "^8.60.1", - "@typescript-eslint/parser": "^8.60.1", - "@vitest/coverage-v8": "^4.1.8", - "eslint": "^8.56.0", - "typescript": "^5.5.0", - "vitest": "^4.1.8" - }, - "engines": { - "node": ">=20.20.0" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", - "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/type-utils": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "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-eslint/parser": "^8.60.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, - "license": "MIT", - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" - }, - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, - "license": "MIT", - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/eslint-plugin/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": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, - "license": "MIT", - "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" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/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": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", - "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "packages/channel-connector/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "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" - } - }, - "packages/channel-connector/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" - } - }, - "packages/channel-connector/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "packages/channel-connector/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" - } - }, - "packages/channel-connector/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": { - "node": ">= 4" - } - }, - "packages/channel-connector/node_modules/marked": { - "version": "15.0.12", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "packages/channel-connector/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" - } - }, - "packages/cli": { - "name": "ai-devkit", - "version": "0.41.2", - "license": "MIT", - "dependencies": { - "@ai-devkit/agent-manager": "0.19.1", - "@ai-devkit/channel-connector": "0.9.0", - "@ai-devkit/memory": "0.14.0", - "@inquirer/prompts": "^8.5.2", - "chalk": "^5.6.0", - "commander": "^11.1.0", - "debug": "^4.4.3", - "fs-extra": "^11.2.0", - "gray-matter": "^4.0.3", - "ink": "^7.0.4", - "ink-text-input": "^6.0.0", - "ora": "^9.0.0", - "react": "^19.2.0", - "smol-toml": "^1.6.1", - "uuid": "14.0.0", - "yaml": "^2.3.4", - "zod": "^3.25.76" - }, - "bin": { - "ai-devkit": "dist/cli.js" - }, - "devDependencies": { - "@swc/cli": "^0.8.1", - "@swc/core": "^1.10.0", - "@types/debug": "^4.1.13", - "@types/fs-extra": "^11.0.4", - "@types/node": "^20.11.5", - "@types/react": "^19.2.0", - "@typescript-eslint/eslint-plugin": "^8.60.1", - "@typescript-eslint/parser": "^8.60.1", - "@vitest/coverage-v8": "^4.1.8", - "eslint": "^8.56.0", - "ts-node": "^10.9.2", - "typescript": "^5.5.0", - "vitest": "^4.1.8" - }, - "engines": { - "node": ">=20.20.0" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", - "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/type-utils": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "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-eslint/parser": "^8.60.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, - "license": "MIT", - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" - }, - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, - "license": "MIT", - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/eslint-plugin/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": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "packages/cli/node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "dev": true, - "license": "MIT", - "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" - } - }, - "packages/cli/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/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": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "packages/cli/node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", - "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "packages/cli/node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "packages/cli/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "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" - } - }, - "packages/cli/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "packages/cli/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" - } - }, - "packages/cli/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "packages/cli/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "packages/cli/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/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" - } - }, - "packages/cli/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": { - "node": ">= 4" - } - }, - "packages/cli/node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/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" - } - }, - "packages/cli/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/node_modules/ora": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", - "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.2", - "string-width": "^8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/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==", - "license": "ISC", "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "packages/cli/node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/cli/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==", + "packages/cli/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "packages/cli/node_modules/zod": { - "version": "3.25.76", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "packages/memory": { - "name": "@ai-devkit/memory", - "version": "0.14.0", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "@typescript-eslint/eslint-plugin": "8.60.1", - "@typescript-eslint/parser": "8.60.1", - "better-sqlite3": "^12.6.2", - "uuid": "^14.0.0" - }, - "bin": { - "ai-devkit-memory": "dist/index.js" - }, - "devDependencies": { - "@swc/cli": "^0.8.1", - "@swc/core": "^1.10.0", - "@types/better-sqlite3": "^7.6.11", - "@types/node": "^20.14.0", - "@types/uuid": "^10.0.0", - "@vitest/coverage-v8": "^4.1.8", - "chokidar": "^3.6.0", - "ts-node": "^10.9.2", - "typescript": "^5.5.0", - "vitest": "^4.1.8" - }, - "engines": { - "node": ">=20.20.0" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", - "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/type-utils": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "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-eslint/parser": "^8.60.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "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" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "license": "MIT", - "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" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" - }, - "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" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "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" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" - }, - "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" - } - }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", - "license": "MIT", - "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" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/memory/node_modules/@typescript-eslint/eslint-plugin/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==", + "packages/cli/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "license": "MIT", "engines": { - "node": ">=18.12" + "node": ">=18" }, - "peerDependencies": { - "typescript": ">=4.8.4" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/memory/node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "packages/cli/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "debug": "^4.4.3" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "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" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/memory/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "packages/cli/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/memory/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "packages/cli/node_modules/ora": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", + "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", - "debug": "^4.4.3" + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=20" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/memory/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "packages/cli/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/memory/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/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==", - "license": "MIT", + "packages/cli/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==", + "license": "ISC", "engines": { - "node": ">=18.12" + "node": ">=14" }, - "peerDependencies": { - "typescript": ">=4.8.4" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "packages/memory/node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", - "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "packages/cli/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=20" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/memory/node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "packages/cli/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==", "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "packages/memory/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "packages/cli/node_modules/zod": { + "version": "3.25.76", "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.60.1", - "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" + "url": "https://github.com/sponsors/colinhacks" } }, - "packages/memory/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==", + "packages/memory": { + "name": "@ai-devkit/memory", + "version": "0.14.0", "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "better-sqlite3": "^12.6.2", + "uuid": "^14.0.0" + }, + "bin": { + "ai-devkit-memory": "dist/index.js" + }, + "devDependencies": { + "@swc/cli": "^0.8.1", + "@swc/core": "^1.10.0", + "@types/better-sqlite3": "^7.6.11", + "@types/node": "^20.14.0", + "@types/uuid": "^10.0.0", + "@vitest/coverage-v8": "^4.1.8", + "chokidar": "^3.6.0", + "ts-node": "^10.9.2", + "typescript": "^5.5.0", + "vitest": "^4.1.8" + }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=20.20.0" } }, - "packages/memory/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "packages/memory-dashboard": { + "name": "@ai-devkit/memory-dashboard", + "version": "0.1.0", "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "@ai-devkit/memory": "0.14.0", + "commander": "^11.1.0", + "cytoscape": "^3.34.0" + }, + "devDependencies": { + "@swc/cli": "^0.8.1", + "@swc/core": "^1.10.0", + "@tailwindcss/cli": "^4.3.1", + "@types/node": "^20.14.0", + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@vitest/coverage-v8": "^4.1.8", + "eslint": "^8.56.0", + "tailwindcss": "^4.3.1", + "typescript": "^5.5.0", + "vitest": "^4.1.8" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=20.20.0" } }, "packages/memory/node_modules/chokidar": { @@ -14446,18 +13749,6 @@ "fsevents": "~2.3.2" } }, - "packages/memory/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==", - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "packages/memory/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -14471,30 +13762,6 @@ "node": ">= 6" } }, - "packages/memory/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "packages/memory/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "packages/memory/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/packages/cli/src/__tests__/services/plugin/memory-dashboard-package.test.ts b/packages/cli/src/__tests__/services/plugin/memory-dashboard-package.test.ts new file mode 100644 index 00000000..6b9b7597 --- /dev/null +++ b/packages/cli/src/__tests__/services/plugin/memory-dashboard-package.test.ts @@ -0,0 +1,22 @@ +import fs from 'fs-extra'; +import path from 'path'; + +describe('memory dashboard plugin package', () => { + it('declares the memory-dashboard plugin command manifest', async () => { + const packageJsonPath = path.resolve( + process.cwd(), + '../../packages/memory-dashboard/package.json' + ); + const packageJson = await fs.readJson(packageJsonPath) as unknown; + const aiDevkit = (packageJson as { aiDevkit?: { commands?: unknown } }).aiDevkit; + + expect((packageJson as { name?: string }).name).toBe('@ai-devkit/memory-dashboard'); + expect(aiDevkit?.commands).toEqual([ + { + name: 'memory-dashboard', + description: 'Launch the local AI DevKit memory dashboard', + entry: './dist/command.js', + }, + ]); + }); +}); diff --git a/packages/memory-dashboard/.eslintrc.json b/packages/memory-dashboard/.eslintrc.json new file mode 100644 index 00000000..998c72fb --- /dev/null +++ b/packages/memory-dashboard/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "plugins": ["@typescript-eslint"], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "env": { + "node": true, + "browser": true, + "es6": true + }, + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { "caughtErrorsIgnorePattern": "^ignore" } + ] + } +} diff --git a/packages/memory-dashboard/.swcrc b/packages/memory-dashboard/.swcrc new file mode 100644 index 00000000..4970347a --- /dev/null +++ b/packages/memory-dashboard/.swcrc @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": false, + "dynamicImport": true + }, + "target": "es2022", + "loose": false, + "externalHelpers": false, + "keepClassNames": true + }, + "module": { + "type": "es6", + "strict": true, + "noInterop": false + }, + "sourceMaps": true, + "minify": false +} diff --git a/packages/memory-dashboard/README.md b/packages/memory-dashboard/README.md new file mode 100644 index 00000000..540460a8 --- /dev/null +++ b/packages/memory-dashboard/README.md @@ -0,0 +1,23 @@ +# @ai-devkit/memory-dashboard + +Local web dashboard for browsing and assessing AI DevKit memory. + +```bash +ai-devkit memory-dashboard +``` + +The MVP is implemented as an AI DevKit plugin package with a `memory-dashboard` command. + +The UI is split across `src/ui/dashboard.html`, `src/ui/tailwind.css`, and `src/ui/app.js`. The build emits Tailwind CSS and copies the UI assets into `dist/ui`. The graph view uses Cytoscape.js with memory, tag, and scope nodes plus Fit/Layout controls. + +For local development without installing the plugin into AI DevKit: + +```bash +npm run dev:standalone +``` + +The standalone script reads the same default/configured memory database path as the plugin runtime. Use `--db-path` to point it at a fixture database: + +```bash +npm run dev:standalone -- --db-path /tmp/memory.db --port 3000 +``` diff --git a/packages/memory-dashboard/package.json b/packages/memory-dashboard/package.json new file mode 100644 index 00000000..12b37c97 --- /dev/null +++ b/packages/memory-dashboard/package.json @@ -0,0 +1,77 @@ +{ + "name": "@ai-devkit/memory-dashboard", + "version": "0.1.0", + "type": "module", + "description": "Local web dashboard for browsing and assessing AI DevKit memory", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./command": { + "types": "./dist/command.d.ts", + "import": "./dist/command.js" + } + }, + "aiDevkit": { + "commands": [ + { + "name": "memory-dashboard", + "description": "Launch the local AI DevKit memory dashboard", + "entry": "./dist/command.js" + } + ] + }, + "scripts": { + "build": "swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly && mkdir -p dist/ui && tailwindcss -i src/ui/tailwind.css -o dist/ui/styles.css --minify && cp src/ui/dashboard.html dist/ui/dashboard.html && cp src/ui/app.js dist/ui/app.js", + "dev": "swc src -d dist --strip-leading-paths --watch", + "dev:standalone": "npm run build && node dist/standalone.js", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "lint": "eslint src --ext .ts", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "keywords": [ + "ai", + "memory", + "dashboard", + "plugin", + "ai-devkit" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/codeaholicguy/ai-devkit.git", + "directory": "packages/memory-dashboard" + }, + "dependencies": { + "@ai-devkit/memory": "0.14.0", + "commander": "^11.1.0", + "cytoscape": "^3.34.0" + }, + "devDependencies": { + "@swc/cli": "^0.8.1", + "@swc/core": "^1.10.0", + "@tailwindcss/cli": "^4.3.1", + "@types/node": "^20.14.0", + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@vitest/coverage-v8": "^4.1.8", + "eslint": "^8.56.0", + "tailwindcss": "^4.3.1", + "typescript": "^5.5.0", + "vitest": "^4.1.8" + }, + "engines": { + "node": ">=20.20.0" + }, + "files": [ + "dist", + "README.md" + ] +} diff --git a/packages/memory-dashboard/project.json b/packages/memory-dashboard/project.json new file mode 100644 index 00000000..4b7338b0 --- /dev/null +++ b/packages/memory-dashboard/project.json @@ -0,0 +1,29 @@ +{ + "name": "memory-dashboard", + "root": "packages/memory-dashboard", + "sourceRoot": "packages/memory-dashboard/src", + "projectType": "library", + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "command": "npm run build", + "cwd": "packages/memory-dashboard" + } + }, + "test": { + "executor": "nx:run-commands", + "options": { + "command": "npm run test", + "cwd": "packages/memory-dashboard" + } + }, + "lint": { + "executor": "nx:run-commands", + "options": { + "command": "npm run lint", + "cwd": "packages/memory-dashboard" + } + } + } +} diff --git a/packages/memory-dashboard/src/command.ts b/packages/memory-dashboard/src/command.ts new file mode 100644 index 00000000..30b04ca7 --- /dev/null +++ b/packages/memory-dashboard/src/command.ts @@ -0,0 +1,106 @@ +import type { Command } from 'commander'; +import { spawn } from 'child_process'; +import { startDashboardServer } from './server.js'; + +interface AiDevkitRuntime { + getMemoryDbPath(): Promise; + logger: { + info(message: string): void; + warn?(message: string): void; + }; +} + +export interface MemoryDashboardOptions { + host?: string; + port?: string; + open?: boolean; +} + +export interface DashboardServerOptions { + dbPath: string; + host: string; + port: number; +} + +export interface DashboardServerHandle { + url: string; + close(): void | Promise; +} + +interface MemoryDashboardActionDeps { + startServer?: (options: DashboardServerOptions) => Promise; + openUrl?: (url: string) => Promise; +} + +export function register( + command: Command, + runtime: AiDevkitRuntime, + deps: MemoryDashboardActionDeps = {} +): void { + command + .description('Launch the local AI DevKit memory dashboard') + .option('--host ', 'Host interface to bind', '127.0.0.1') + .option('--port ', 'Port to bind, or 0 for a random free port', '0') + .option('--open', 'Open the dashboard URL in the browser') + .action(createMemoryDashboardAction(runtime, deps)); +} + +export function createMemoryDashboardAction( + runtime: AiDevkitRuntime, + deps: MemoryDashboardActionDeps = {} +): (options: MemoryDashboardOptions) => Promise { + const startServer = deps.startServer ?? startDashboardServer; + const openUrl = deps.openUrl ?? openBrowser; + + return async (options: MemoryDashboardOptions): Promise => { + const dbPath = await runtime.getMemoryDbPath(); + const host = options.host ?? '127.0.0.1'; + const port = parsePort(options.port ?? '0'); + const server = await startServer({ dbPath, host, port }); + + runtime.logger.info(`Memory dashboard: ${server.url}`); + + if (options.open) { + try { + await openUrl(server.url); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + runtime.logger.warn?.(`Unable to open browser automatically: ${message}. Open the printed URL manually.`); + } + } + }; +} + +function parsePort(rawPort: string): number { + const port = Number.parseInt(rawPort, 10); + + if (!Number.isInteger(port) || port < 0 || port > 65_535) { + throw new Error('Port must be an integer between 0 and 65535.'); + } + + return port; +} + +async function openBrowser(url: string): Promise { + const command = process.platform === 'darwin' + ? 'open' + : process.platform === 'win32' + ? 'cmd' + : 'xdg-open'; + const args = process.platform === 'win32' + ? ['/c', 'start', '', url] + : [url]; + + await new Promise((resolve, reject) => { + const child = spawn(command, args, { + detached: true, + stdio: 'ignore', + }); + + child.once('error', reject); + child.once('spawn', () => { + child.unref(); + resolve(); + }); + }); +} diff --git a/packages/memory-dashboard/src/index.ts b/packages/memory-dashboard/src/index.ts new file mode 100644 index 00000000..61111fb3 --- /dev/null +++ b/packages/memory-dashboard/src/index.ts @@ -0,0 +1 @@ +export { register } from './command.js'; diff --git a/packages/memory-dashboard/src/server.ts b/packages/memory-dashboard/src/server.ts new file mode 100644 index 00000000..41ac438d --- /dev/null +++ b/packages/memory-dashboard/src/server.ts @@ -0,0 +1,289 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'http'; +import { readFileSync } from 'fs'; +import { createRequire } from 'module'; +import { dirname, join } from 'path'; +import { URL } from 'url'; +import { memoryListCommand, memorySummaryCommand, type KnowledgeItem } from '@ai-devkit/memory'; +import type { DashboardServerHandle, DashboardServerOptions } from './command.js'; + +const DEFAULT_LIMIT = 50; +const DEFAULT_GRAPH_LIMIT = 250; +const require = createRequire(import.meta.url); +const cytoscapeAssetPath = join(dirname(require.resolve('cytoscape')), 'cytoscape.esm.min.mjs'); + +interface GraphNode { + id: string; + label: string; + type: 'memory' | 'tag' | 'scope'; + count?: number; + item?: KnowledgeItem; +} + +interface GraphEdge { + source: string; + target: string; + type: 'has-tag' | 'in-scope'; +} + +export async function startDashboardServer(options: DashboardServerOptions): Promise { + const server = createServer((request, response) => { + void handleRequest(request, response, options); + }); + + await listen(server, options.host, options.port); + + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : options.port; + + return { + url: `http://${options.host}:${port}`, + async close(): Promise { + await closeServer(server); + }, + }; +} + +async function handleRequest( + request: IncomingMessage, + response: ServerResponse, + options: DashboardServerOptions +): Promise { + const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? '127.0.0.1'}`); + + try { + if (request.method !== 'GET') { + writeJson(response, 405, { error: 'Method not allowed' }); + return; + } + + if (requestUrl.pathname === '/') { + writeHtml(response, readUiAsset('dashboard.html')); + return; + } + + if (requestUrl.pathname === '/favicon.ico') { + response.writeHead(204, { 'cache-control': 'no-store' }); + response.end(); + return; + } + + if (requestUrl.pathname === '/assets/styles.css') { + writeCss(response, readUiAsset('styles.css', 'tailwind.css')); + return; + } + + if (requestUrl.pathname === '/assets/app.js') { + writeJavaScript(response, readUiAsset('app.js')); + return; + } + + if (requestUrl.pathname === '/assets/vendor/cytoscape.esm.min.mjs') { + writeJavaScript(response, readFileSync(cytoscapeAssetPath, 'utf8')); + return; + } + + if (requestUrl.pathname === '/api/memory') { + writeJson(response, 200, memoryListCommand({ + dbPath: options.dbPath, + query: readOptionalString(requestUrl, 'query'), + scope: readOptionalString(requestUrl, 'scope'), + tags: readTags(requestUrl), + limit: readOptionalInteger(requestUrl, 'limit', DEFAULT_LIMIT), + offset: readOptionalInteger(requestUrl, 'offset', 0), + sort: readOptionalSort(requestUrl), + })); + return; + } + + if (requestUrl.pathname === '/api/summary') { + writeJson(response, 200, memorySummaryCommand({ dbPath: options.dbPath })); + return; + } + + if (requestUrl.pathname === '/api/graph') { + const list = memoryListCommand({ + dbPath: options.dbPath, + query: readOptionalString(requestUrl, 'query'), + scope: readOptionalString(requestUrl, 'scope'), + tags: readTags(requestUrl), + limit: readOptionalInteger(requestUrl, 'maxItems', DEFAULT_GRAPH_LIMIT), + offset: 0, + sort: readOptionalSort(requestUrl), + }); + writeJson(response, 200, buildMemoryGraph(list.items)); + return; + } + + writeJson(response, 404, { error: 'Not found' }); + } catch (error) { + writeJson(response, 400, { + error: 'Invalid request', + message: error instanceof Error ? error.message : String(error), + }); + } +} + +function buildMemoryGraph(items: KnowledgeItem[]): { nodes: GraphNode[]; edges: GraphEdge[] } { + const memoryNodes: GraphNode[] = []; + const tagCounts = new Map(); + const scopeCounts = new Map(); + const edges: GraphEdge[] = []; + + for (const item of items) { + const memoryNodeId = `memory:${item.id}`; + memoryNodes.push({ + id: memoryNodeId, + type: 'memory', + label: item.title, + item, + }); + + scopeCounts.set(item.scope, (scopeCounts.get(item.scope) ?? 0) + 1); + edges.push({ + source: memoryNodeId, + target: `scope:${item.scope}`, + type: 'in-scope', + }); + + for (const tag of item.tags) { + tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1); + edges.push({ + source: memoryNodeId, + target: `tag:${tag}`, + type: 'has-tag', + }); + } + } + + const tagNodes = Array.from(tagCounts.entries()) + .sort(([leftTag, leftCount], [rightTag, rightCount]) => rightCount - leftCount || leftTag.localeCompare(rightTag)) + .map(([tag, count]) => ({ + id: `tag:${tag}`, + type: 'tag' as const, + label: tag, + count, + })); + + const scopeNodes = Array.from(scopeCounts.entries()) + .sort(([leftScope], [rightScope]) => leftScope.localeCompare(rightScope)) + .map(([scope, count]) => ({ + id: `scope:${scope}`, + type: 'scope' as const, + label: scope, + count, + })); + + return { + nodes: [...memoryNodes, ...tagNodes, ...scopeNodes], + edges, + }; +} + +function readUiAsset(fileName: string, fallbackFileName?: string): string { + try { + return readFileSync(new URL(`./ui/${fileName}`, import.meta.url), 'utf8'); + } catch (error) { + if (fallbackFileName && error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { + return readFileSync(new URL(`./ui/${fallbackFileName}`, import.meta.url), 'utf8'); + } + + throw error; + } +} + +function readOptionalString(url: URL, key: string): string | undefined { + const value = url.searchParams.get(key); + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function readTags(url: URL): string | undefined { + const tags = url.searchParams + .getAll('tag') + .map(tag => tag.trim()) + .filter(Boolean); + + return tags.length > 0 ? tags.join(',') : undefined; +} + +function readOptionalInteger(url: URL, key: string, fallback: number): number { + const value = url.searchParams.get(key); + if (value === null || value.trim() === '') { + return fallback; + } + + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || String(parsed) !== value) { + throw new Error(`${key} must be an integer.`); + } + + return parsed; +} + +function readOptionalSort(url: URL): 'updated-desc' | 'created-desc' | 'title-asc' | undefined { + const sort = readOptionalString(url, 'sort'); + if (sort === undefined) { + return undefined; + } + + if (sort === 'updated-desc' || sort === 'created-desc' || sort === 'title-asc') { + return sort; + } + + throw new Error('sort must be updated-desc, created-desc, or title-asc.'); +} + +function writeHtml(response: ServerResponse, html: string): void { + response.writeHead(200, { + 'content-type': 'text/html; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(html); +} + +function writeCss(response: ServerResponse, css: string): void { + response.writeHead(200, { + 'content-type': 'text/css; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(css); +} + +function writeJavaScript(response: ServerResponse, javaScript: string): void { + response.writeHead(200, { + 'content-type': 'text/javascript; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(javaScript); +} + +function writeJson(response: ServerResponse, status: number, body: unknown): void { + response.writeHead(status, { + 'content-type': 'application/json; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(JSON.stringify(body)); +} + +async function listen(server: Server, host: string, port: number): Promise { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(port, host, () => { + server.off('error', reject); + resolve(); + }); + }); +} + +async function closeServer(server: Server): Promise { + await new Promise((resolve, reject) => { + server.close(error => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }); +} diff --git a/packages/memory-dashboard/src/standalone.ts b/packages/memory-dashboard/src/standalone.ts new file mode 100644 index 00000000..90a915e3 --- /dev/null +++ b/packages/memory-dashboard/src/standalone.ts @@ -0,0 +1,108 @@ +import { readFile } from 'fs/promises'; +import { homedir } from 'os'; +import { dirname, isAbsolute, join, resolve } from 'path'; +import { Command } from 'commander'; +import { createMemoryDashboardAction } from './command.js'; + +interface StandaloneRuntimeOptions { + homeDir?: string; + dbPathOverride?: string; +} + +interface AiDevkitConfig { + memory?: { + path?: unknown; + }; +} + +export interface StandaloneDashboardOptions { + host?: string; + port?: string; + open?: boolean; + dbPath?: string; +} + +export function createStandaloneMemoryRuntime(options: StandaloneRuntimeOptions = {}) { + const homeDir = options.homeDir ?? homedir(); + const configPath = join(homeDir, '.ai-devkit', '.ai-devkit.json'); + const defaultDbPath = join(homeDir, '.ai-devkit', 'memory.db'); + + return { + async getMemoryDbPath(): Promise { + if (options.dbPathOverride && options.dbPathOverride.trim().length > 0) { + return resolve(options.dbPathOverride.trim()); + } + + const config = await readAiDevkitConfig(configPath); + const configuredPath = config.memory?.path; + + if (typeof configuredPath !== 'string' || configuredPath.trim().length === 0) { + return defaultDbPath; + } + + const trimmedPath = configuredPath.trim(); + return isAbsolute(trimmedPath) + ? trimmedPath + : resolve(dirname(configPath), trimmedPath); + }, + logger: { + info(message: string): void { + console.log(message); + }, + warn(message: string): void { + console.warn(message); + }, + }, + }; +} + +export function parseStandaloneOptions(argv: string[]): StandaloneDashboardOptions { + const command = createStandaloneCommand(); + command.exitOverride(); + command.configureOutput({ + writeOut: () => undefined, + writeErr: () => undefined, + }); + command.parse(['node', 'memory-dashboard-standalone', ...argv]); + return command.opts(); +} + +export function createStandaloneCommand(): Command { + return new Command('memory-dashboard-standalone') + .description('Launch the AI DevKit memory dashboard without installing it as a plugin') + .option('--host ', 'Host interface to bind', '127.0.0.1') + .option('--port ', 'Port to bind, or 0 for a random free port', '0') + .option('--db-path ', 'Memory database path override') + .option('--open', 'Open the dashboard URL in the browser'); +} + +export async function runStandalone(argv: string[] = process.argv.slice(2)): Promise { + const command = createStandaloneCommand(); + command.parse(argv, { from: 'user' }); + const options = command.opts(); + const runtime = createStandaloneMemoryRuntime({ dbPathOverride: options.dbPath }); + await createMemoryDashboardAction(runtime)({ + host: options.host, + port: options.port, + open: options.open, + }); +} + +async function readAiDevkitConfig(configPath: string): Promise { + try { + return JSON.parse(await readFile(configPath, 'utf8')) as AiDevkitConfig; + } catch (error) { + if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { + return {}; + } + + throw error; + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + runStandalone().catch(error => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/packages/memory-dashboard/src/ui/app.js b/packages/memory-dashboard/src/ui/app.js new file mode 100644 index 00000000..7cee6c54 --- /dev/null +++ b/packages/memory-dashboard/src/ui/app.js @@ -0,0 +1,594 @@ +import cytoscape from '/assets/vendor/cytoscape.esm.min.mjs'; + +const elements = { + search: document.getElementById('memory-search'), + scope: document.getElementById('scope-filter'), + tag: document.getElementById('tag-filter'), + group: document.getElementById('group-mode'), + summary: document.getElementById('summary-panel'), + list: document.getElementById('memory-list'), + pagination: document.getElementById('memory-pagination'), + detail: document.getElementById('memory-detail'), + graph: document.getElementById('memory-graph'), + graphSelectedTitle: document.getElementById('graph-selected-title'), + graphFit: document.getElementById('graph-fit'), + graphLayout: document.getElementById('graph-layout'), + graphZoomIn: document.getElementById('graph-zoom-in'), + graphZoomOut: document.getElementById('graph-zoom-out'), +}; + +const PAGE_SIZE = 4; +const dateFormatter = new Intl.DateTimeFormat(undefined, { + dateStyle: 'medium', +}); +let state = readStateFromUrl(); +let currentItems = []; +let currentTotal = 0; +let selectedId = null; +let selectedDetailItem = null; +let graphInstance = null; +let latestGraphElements = []; + +function readStateFromUrl() { + const params = new URLSearchParams(window.location.search); + return { + query: params.get('query') || '', + scope: params.get('scope') || '', + tag: params.get('tag') || '', + group: params.get('group') || 'scope', + page: Math.max(1, Number.parseInt(params.get('page') || '1', 10) || 1), + }; +} + +function applyStateToControls() { + if (elements.search) elements.search.value = state.query; + if (elements.scope) elements.scope.value = state.scope; + if (elements.tag) elements.tag.value = state.tag; + if (elements.group) elements.group.value = state.group; +} + +function updateUrlState() { + const params = new URLSearchParams(); + if (state.query) params.set('query', state.query); + if (state.scope) params.set('scope', state.scope); + if (state.tag) params.set('tag', state.tag); + if (state.group && state.group !== 'scope') params.set('group', state.group); + if (state.page && state.page > 1) params.set('page', String(state.page)); + const query = params.toString(); + history.replaceState(null, '', query ? `?${query}` : window.location.pathname); +} + +function buildApiParams() { + const params = new URLSearchParams(); + if (state.query) params.set('query', state.query); + if (state.scope) params.set('scope', state.scope); + if (state.tag) params.append('tag', state.tag); + params.set('limit', String(PAGE_SIZE)); + params.set('offset', String((state.page - 1) * PAGE_SIZE)); + return params; +} + +function buildApiSuffix(params) { + const query = params.toString(); + return query ? `?${query}` : ''; +} + +async function loadDashboard() { + updateUrlState(); + const params = buildApiParams(); + const { summary, memory, graph } = await fetchDashboardData(params); + + currentItems = memory.items || []; + currentTotal = memory.total || currentItems.length; + syncSelectedMemory(); + + renderFacets(summary); + renderSummary(summary); + renderMemoryList(currentItems); + renderPagination(); + renderDetail(selectedDetailItem); + renderMemoryGraph(graph); +} + +async function fetchDashboardData(params) { + const suffix = buildApiSuffix(params); + const [summaryResponse, memoryResponse, graphResponse] = await Promise.all([ + fetch('/api/summary'), + fetch(`/api/memory${suffix}`), + fetch(`/api/graph${suffix}`), + ]); + + const [summary, memory, graph] = await Promise.all([ + summaryResponse.json(), + memoryResponse.json(), + graphResponse.json(), + ]); + + return { summary, memory, graph }; +} + +function findCurrentItem(id) { + return currentItems.find(item => item.id === id) || null; +} + +function resolveSelectedItem(fallbackItem = null) { + return selectedId ? findCurrentItem(selectedId) || fallbackItem : fallbackItem; +} + +function syncSelectedMemory() { + const currentSelection = resolveSelectedItem(selectedDetailItem); + if (currentSelection) { + selectedId = currentSelection.id; + selectedDetailItem = currentSelection; + return; + } + + selectedId = currentItems[0]?.id || null; + selectedDetailItem = currentItems[0] || null; +} + +function renderFacets(summary) { + replaceOptions(elements.scope, 'All scopes', (summary.scopes || []).map(item => item.scope)); + replaceOptions(elements.tag, 'All tags', (summary.tags || []).map(item => item.tag)); + applyStateToControls(); +} + +function replaceOptions(select, emptyLabel, values) { + if (!select) return; + const selected = select.value; + select.textContent = ''; + const empty = document.createElement('option'); + empty.value = ''; + empty.textContent = emptyLabel; + select.append(empty); + for (const value of values) { + const option = document.createElement('option'); + option.value = value; + option.textContent = value; + select.append(option); + } + select.value = values.includes(selected) ? selected : ''; +} + +function renderSummary(summary) { + if (!elements.summary) return; + elements.summary.textContent = ''; + const grid = document.createElement('div'); + grid.className = 'summary-grid'; + grid.append(createStat('Total', summary.totalItems || 0)); + grid.append(createStat('Scopes', (summary.scopes || []).length)); + grid.append(createStat('Tags', (summary.tags || []).length)); + elements.summary.append(grid); +} + +function createStat(label, value) { + const stat = document.createElement('div'); + stat.className = 'summary-stat'; + const strong = document.createElement('strong'); + strong.className = 'block text-lg font-bold text-slate-950'; + strong.textContent = String(value); + const span = document.createElement('div'); + span.className = 'muted'; + span.textContent = label; + stat.append(strong, span); + return stat; +} + +function renderMemoryList(items) { + if (!elements.list) return; + elements.list.textContent = ''; + if (items.length === 0) { + elements.list.textContent = 'No memory records match the current filters.'; + renderDetail(null); + return; + } + + const groups = groupItems(items, state.group); + for (const [groupName, groupItemsForName] of groups) { + const heading = document.createElement('h2'); + heading.className = 'memory-group-heading'; + heading.textContent = groupName; + elements.list.append(heading); + + for (const item of groupItemsForName) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'memory-row'; + button.setAttribute('aria-selected', String(item.id === selectedId)); + button.setAttribute('aria-label', `Inspect ${item.title}`); + button.addEventListener('click', () => selectMemory(item.id, item)); + + const title = document.createElement('span'); + title.className = 'memory-title'; + title.textContent = item.title; + const meta = document.createElement('span'); + meta.className = 'muted'; + meta.textContent = `${item.scope} • ${formatDate(item.updatedAt)}`; + const tags = document.createElement('span'); + tags.className = 'tag-list'; + tags.textContent = (item.tags || []).join(', '); + button.append(title, meta, tags); + elements.list.append(button); + } + } +} + +function renderPagination() { + if (!elements.pagination) return; + elements.pagination.textContent = ''; + const totalPages = Math.max(1, Math.ceil(currentTotal / PAGE_SIZE)); + const page = Math.min(state.page, totalPages); + state.page = page; + + const label = document.createElement('div'); + label.className = 'text-xs font-semibold text-slate-700'; + const start = currentTotal === 0 ? 0 : (page - 1) * PAGE_SIZE + 1; + const end = Math.min(page * PAGE_SIZE, currentTotal); + label.textContent = `${start}-${end} of ${currentTotal}`; + + const controls = document.createElement('div'); + controls.className = 'flex items-center gap-2'; + const previous = createPagerButton('Previous', page <= 1, () => { + state.page = Math.max(1, state.page - 1); + void loadDashboard(); + }); + const current = document.createElement('span'); + current.className = 'text-xs font-semibold text-slate-500'; + current.textContent = `Page ${page} / ${totalPages}`; + const next = createPagerButton('Next', page >= totalPages, () => { + state.page = Math.min(totalPages, state.page + 1); + void loadDashboard(); + }); + controls.append(previous, current, next); + elements.pagination.append(label, controls); +} + +function createPagerButton(label, disabled, onClick) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'pager-button'; + button.textContent = label; + button.disabled = disabled; + button.setAttribute('aria-label', `${label} memory page`); + button.addEventListener('click', onClick); + return button; +} + +function groupItems(items, mode) { + const groups = new Map(); + for (const item of items) { + const keys = mode === 'tag' + ? (item.tags.length ? item.tags : ['untagged']) + : [mode === 'recency' ? recencyBucket(item.updatedAt) : item.scope]; + for (const key of keys) { + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(item); + } + } + return Array.from(groups.entries()).sort(([left], [right]) => left.localeCompare(right)); +} + +function selectMemory(id, fallbackItem) { + selectedId = id; + selectedDetailItem = resolveSelectedItem(fallbackItem || null); + renderDetail(selectedDetailItem); + renderMemoryList(currentItems); + highlightGraphSelection(); +} + +function renderDetail(item) { + if (!elements.detail) return; + elements.detail.textContent = ''; + if (!item) { + elements.detail.textContent = 'Select a memory item to inspect details.'; + updateGraphSelectedTitle(null); + return; + } + const eyebrow = document.createElement('p'); + eyebrow.className = 'text-xs font-bold uppercase text-slate-500'; + eyebrow.textContent = 'Selected Memory'; + const title = document.createElement('h2'); + title.className = 'detail-title mt-2'; + title.textContent = item.title; + const meta = document.createElement('p'); + meta.className = 'muted mt-3'; + meta.textContent = `${item.scope} • Updated ${formatDate(item.updatedAt)}`; + const content = document.createElement('p'); + content.className = 'mt-4 text-sm leading-6 text-slate-800'; + content.textContent = item.content; + const tags = document.createElement('p'); + tags.className = 'tag-list mt-4'; + tags.textContent = (item.tags || []).join(', '); + elements.detail.append(eyebrow, title, meta, content, tags); + updateGraphSelectedTitle(item.title); +} + +function renderMemoryGraph(graph) { + if (!elements.graph) return; + latestGraphElements = toCytoscapeElements(graph); + + if (!graphInstance) { + graphInstance = cytoscape({ + container: elements.graph, + elements: [], + minZoom: 0.08, + maxZoom: 4, + style: graphStyle(), + layout: { name: 'preset' }, + }); + graphInstance.on('tap', 'node', event => handleGraphNodeTap(event.target)); + graphInstance.on('mouseover', 'node', event => event.target.addClass('hovered')); + graphInstance.on('mouseout', 'node', event => event.target.removeClass('hovered')); + graphInstance.add(latestGraphElements); + elements.graph.__memoryDashboardGraph = graphInstance; + runGraphLayout(); + } else { + graphInstance.elements().remove(); + graphInstance.add(latestGraphElements); + elements.graph.__memoryDashboardGraph = graphInstance; + runGraphLayout(); + } + + highlightGraphSelection(); +} + +function toCytoscapeElements(graph) { + const nodes = (graph.nodes || []).map(node => ({ + data: { + id: node.id, + label: node.label, + type: node.type, + count: node.count || 1, + item: node.item || null, + memoryId: node.type === 'memory' ? node.id.replace(/^memory:/, '') : '', + }, + classes: node.type, + })); + const edges = (graph.edges || []).map((edge, index) => ({ + data: { + id: `edge:${index}:${edge.source}:${edge.target}`, + source: edge.source, + target: edge.target, + type: edge.type, + }, + classes: edge.type, + })); + return [...nodes, ...edges]; +} + +function graphStyle() { + return [ + { + selector: 'node', + style: { + label: 'data(label)', + 'font-size': 11, + 'font-weight': 700, + color: '#0f172a', + 'text-max-width': 104, + 'text-wrap': 'ellipsis', + 'text-valign': 'bottom', + 'text-margin-y': 8, + width: 'mapData(count, 1, 8, 28, 52)', + height: 'mapData(count, 1, 8, 28, 52)', + 'border-width': 2, + 'border-color': '#ffffff', + }, + }, + { + selector: 'node.memory', + style: { + shape: 'round-rectangle', + width: 44, + height: 30, + 'background-color': '#2563eb', + color: '#1e3a8a', + }, + }, + { + selector: 'node.tag', + style: { + shape: 'ellipse', + 'background-color': '#14b8a6', + color: '#0f766e', + }, + }, + { + selector: 'node.scope', + style: { + shape: 'diamond', + 'background-color': '#f59e0b', + color: '#92400e', + }, + }, + { + selector: 'edge', + style: { + width: 1.5, + 'curve-style': 'bezier', + 'line-color': '#94a3b8', + 'target-arrow-shape': 'triangle', + 'target-arrow-color': '#94a3b8', + opacity: 0.72, + }, + }, + { + selector: '.selected', + style: { + 'border-color': '#1d4ed8', + 'border-width': 4, + 'background-blacken': -0.12, + 'z-index': 20, + }, + }, + { + selector: '.connected', + style: { + opacity: 1, + 'line-color': '#2563eb', + 'target-arrow-color': '#2563eb', + 'z-index': 10, + }, + }, + { + selector: '.dimmed', + style: { + opacity: 0.28, + }, + }, + { + selector: '.hovered', + style: { + 'border-color': '#0f172a', + }, + }, + ]; +} + +function graphLayout() { + return { + name: 'cose', + animate: false, + fit: true, + padding: 56, + avoidOverlap: true, + nodeRepulsion: 12000, + idealEdgeLength: 96, + edgeElasticity: 80, + nestingFactor: 1.05, + numIter: 1400, + }; +} + +function runGraphLayout() { + if (!graphInstance) return; + const layout = graphInstance.layout(graphLayout()); + layout.one('layoutstop', () => { + fitGraphTo(); + }); + layout.run(); +} + +function fitGraphTo(elementsToFit) { + if (!graphInstance) return; + graphInstance.resize(); + graphInstance.fit(elementsToFit, 56); +} + +function zoomGraphBy(factor) { + if (!graphInstance || !elements.graph) return; + const nextZoom = Math.max( + graphInstance.minZoom(), + Math.min(graphInstance.maxZoom(), graphInstance.zoom() * factor) + ); + graphInstance.zoom({ + level: nextZoom, + renderedPosition: { + x: elements.graph.clientWidth / 2, + y: elements.graph.clientHeight / 2, + }, + }); +} + +function handleGraphNodeTap(node) { + const type = node.data('type'); + if (type === 'memory') { + selectMemory(node.data('memoryId'), node.data('item')); + return; + } + + updateGraphSelectedTitle(`${type}: ${node.data('label')}`); + graphInstance.elements().removeClass('selected connected dimmed'); + node.addClass('selected'); + graphInstance.elements().difference(node.closedNeighborhood()).addClass('dimmed'); + node.connectedEdges().addClass('connected'); + fitGraphTo(node.closedNeighborhood()); +} + +function highlightGraphSelection() { + if (!graphInstance) return; + graphInstance.elements().removeClass('selected connected dimmed'); + if (!selectedId) return; + + const node = graphInstance.getElementById(`memory:${selectedId}`); + if (!node || node.empty()) return; + + node.addClass('selected'); + node.connectedEdges().addClass('connected'); + node.neighborhood('node').addClass('connected'); + graphInstance.elements().difference(node.closedNeighborhood()).addClass('dimmed'); + fitGraphTo(node.closedNeighborhood()); +} + +function updateGraphSelectedTitle(value) { + if (elements.graphSelectedTitle) { + elements.graphSelectedTitle.textContent = value || 'Select a graph node or memory row'; + } +} + +function recencyBucket(value) { + const age = Date.now() - Date.parse(value); + const day = 24 * 60 * 60 * 1000; + if (Number.isNaN(age)) return 'older'; + if (age <= day) return 'today'; + if (age <= 7 * day) return 'week'; + if (age <= 30 * day) return 'month'; + return 'older'; +} + +function formatDate(value) { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? value : dateFormatter.format(date); +} + +function bindControls() { + const onChange = () => { + state = { + query: elements.search ? elements.search.value.trim() : '', + scope: elements.scope ? elements.scope.value : '', + tag: elements.tag ? elements.tag.value : '', + group: elements.group ? elements.group.value : 'scope', + page: 1, + }; + void loadDashboard(); + }; + elements.search && elements.search.addEventListener('input', onChange); + elements.scope && elements.scope.addEventListener('change', onChange); + elements.tag && elements.tag.addEventListener('change', onChange); + elements.group && elements.group.addEventListener('change', onChange); + elements.graphFit && elements.graphFit.addEventListener('click', () => fitGraphTo()); + elements.graphLayout && elements.graphLayout.addEventListener('click', runGraphLayout); + elements.graphZoomIn && elements.graphZoomIn.addEventListener('click', () => zoomGraphBy(1.25)); + elements.graphZoomOut && elements.graphZoomOut.addEventListener('click', () => zoomGraphBy(0.8)); + elements.graph && elements.graph.addEventListener('keydown', handleGraphKeyDown); + window.addEventListener('resize', () => fitGraphTo()); +} + +function handleGraphKeyDown(event) { + if (event.key === '+' || event.key === '=') { + event.preventDefault(); + zoomGraphBy(1.25); + return; + } + + if (event.key === '-' || event.key === '_') { + event.preventDefault(); + zoomGraphBy(0.8); + return; + } + + if (event.key === '0' || event.key.toLowerCase() === 'f') { + event.preventDefault(); + fitGraphTo(); + return; + } + + if (event.key.toLowerCase() === 'l') { + event.preventDefault(); + runGraphLayout(); + } +} + +applyStateToControls(); +bindControls(); +void loadDashboard(); diff --git a/packages/memory-dashboard/src/ui/dashboard.html b/packages/memory-dashboard/src/ui/dashboard.html new file mode 100644 index 00000000..16cb3bc1 --- /dev/null +++ b/packages/memory-dashboard/src/ui/dashboard.html @@ -0,0 +1,85 @@ + + + + + + AI DevKit Memory + + + + +
+
+
+
+
+

AI DevKit Memory

+
+
+
+ +
+ + + + +
+
+
+ +
+
+
+
+

Records

+
+
+
+
+
+ +
+
+
+
+

Relationships

+

Select a graph node or memory row

+
+
+ + + + +
+
+
+
+
+
+
+
+
+
+ + + diff --git a/packages/memory-dashboard/src/ui/tailwind.css b/packages/memory-dashboard/src/ui/tailwind.css new file mode 100644 index 00000000..ce5415ea --- /dev/null +++ b/packages/memory-dashboard/src/ui/tailwind.css @@ -0,0 +1,209 @@ +@import "tailwindcss"; + +@source "./dashboard.html"; +@source "./app.js"; + +@layer components { + .skip-link { + background: rgb(15 23 42); + border-radius: 0.375rem; + color: white; + font-size: 0.875rem; + font-weight: 700; + left: 1rem; + padding: 0.5rem 0.75rem; + position: fixed; + top: 1rem; + transform: translateY(-150%); + transition: transform 150ms ease; + z-index: 50; + } + + .skip-link:focus-visible { + outline: 2px solid rgb(37 99 235); + outline-offset: 2px; + transform: translateY(0); + } + + .graph-canvas { + height: calc(100vh - 19rem); + min-height: 520px; + touch-action: manipulation; + width: 100%; + } + + .graph-canvas:focus-visible { + outline: 2px solid rgb(37 99 235); + outline-offset: -2px; + } + + .graph-toolbar-button { + border-radius: 0.375rem; + border: 1px solid rgb(203 213 225); + color: rgb(51 65 85); + font-size: 0.75rem; + font-weight: 700; + padding: 0.5rem 0.75rem; + transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease, box-shadow 150ms ease; + } + + .graph-toolbar-button:hover { + background: rgb(241 245 249); + } + + .graph-toolbar-button:focus-visible, + .pager-button:focus-visible, + .memory-row:focus-visible { + border-color: rgb(37 99 235); + outline: none; + box-shadow: 0 0 0 3px rgb(191 219 254); + } + + .memory-row { + width: 100%; + border-radius: 0.375rem; + border: 1px solid rgb(226 232 240); + background: rgb(248 250 252); + padding: 0.625rem 0.75rem; + text-align: left; + touch-action: manipulation; + transition: border-color 150ms ease, background-color 150ms ease, box-shadow 150ms ease; + } + + .memory-row:hover, + .memory-row[aria-selected="true"] { + border-color: rgb(37 99 235); + background: rgb(239 246 255); + box-shadow: 0 0 0 3px rgb(219 234 254); + } + + .pager-button { + border-radius: 0.375rem; + border: 1px solid rgb(203 213 225); + color: rgb(51 65 85); + font-size: 0.75rem; + font-weight: 700; + padding: 0.5rem 0.75rem; + touch-action: manipulation; + transition: background-color 150ms ease, color 150ms ease; + } + + .pager-button:hover:not(:disabled) { + background: rgb(241 245 249); + } + + .pager-button:disabled { + color: rgb(148 163 184); + cursor: not-allowed; + } + + .summary-grid { + display: grid; + gap: 0.5rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .header-summary-row { + grid-template-columns: minmax(18rem, 1fr) minmax(24rem, 34rem); + } + + .summary-strip { + min-width: 0; + } + + .summary-stat { + border-radius: 0.375rem; + border: 1px solid rgb(226 232 240); + background: rgb(248 250 252); + padding: 0.625rem 0.75rem; + font-variant-numeric: tabular-nums; + } + + .dashboard-grid { + min-height: calc(100vh - 11rem); + } + + .dashboard-controls-grid { + grid-template-columns: minmax(28rem, 1fr) minmax(13rem, 16rem) minmax(13rem, 16rem) minmax(10rem, 12rem); + } + + .dashboard-controls-grid label { + min-width: 0; + } + + .dashboard-controls-grid select, + .dashboard-controls-grid input { + min-width: 0; + width: 100%; + } + + .memory-list { + align-content: start; + display: grid; + gap: 0.5rem; + min-height: 0; + overflow: hidden; + padding: 0.75rem; + } + + .memory-pagination { + align-items: center; + border-top: 1px solid rgb(226 232 240); + display: flex; + justify-content: space-between; + padding: 0.75rem; + } + + .graph-workspace { + min-height: calc(100vh - 19rem); + } + + .memory-detail-panel { + border-left: 1px solid rgb(226 232 240); + height: calc(100vh - 19rem); + min-height: 520px; + overflow: auto; + padding: 1rem; + } + + .memory-group-heading { + padding: 0.125rem 0.25rem 0; + font-size: 0.75rem; + font-weight: 700; + color: rgb(71 85 105); + text-transform: uppercase; + text-wrap: balance; + } + + .memory-title { + display: block; + font-weight: 700; + color: rgb(15 23 42); + } + + .muted, + .tag-list { + color: rgb(71 85 105); + display: block; + font-size: 0.75rem; + font-weight: 500; + } + + .detail-title { + font-size: 1.25rem; + font-weight: 750; + line-height: 1.2; + color: rgb(15 23 42); + overflow-wrap: anywhere; + text-wrap: balance; + } +} + +@media (prefers-reduced-motion: reduce) { + .skip-link, + .graph-toolbar-button, + .memory-row, + .pager-button { + transition-duration: 0ms; + } +} diff --git a/packages/memory-dashboard/tests/command.test.ts b/packages/memory-dashboard/tests/command.test.ts new file mode 100644 index 00000000..415258d0 --- /dev/null +++ b/packages/memory-dashboard/tests/command.test.ts @@ -0,0 +1,163 @@ +import { Command } from 'commander'; +import { createMemoryDashboardAction, register } from '../src/command'; + +describe('memory dashboard command', () => { + it('uses runtime memory path and parsed server options', async () => { + const startServer = vi.fn().mockResolvedValue({ + url: 'http://127.0.0.2:4567', + close: vi.fn(), + }); + const runtime = { + getMemoryDbPath: vi.fn().mockResolvedValue('/tmp/memory.db'), + logger: { + info: vi.fn(), + warn: vi.fn(), + }, + }; + + const action = createMemoryDashboardAction(runtime, { startServer }); + + await action({ + host: '127.0.0.2', + port: '4567', + open: false, + }); + + expect(runtime.getMemoryDbPath).toHaveBeenCalledOnce(); + expect(startServer).toHaveBeenCalledWith({ + dbPath: '/tmp/memory.db', + host: '127.0.0.2', + port: 4567, + }); + expect(runtime.logger.info).toHaveBeenCalledWith('Memory dashboard: http://127.0.0.2:4567'); + }); + + it('opens the dashboard URL when browser open is requested', async () => { + const openUrl = vi.fn().mockResolvedValue(undefined); + const runtime = { + getMemoryDbPath: vi.fn().mockResolvedValue('/tmp/memory.db'), + logger: { + info: vi.fn(), + warn: vi.fn(), + }, + }; + + const action = createMemoryDashboardAction(runtime, { + startServer: vi.fn().mockResolvedValue({ + url: 'http://127.0.0.1:3000', + close: vi.fn(), + }), + openUrl, + }); + + await action({ open: true }); + + expect(openUrl).toHaveBeenCalledWith('http://127.0.0.1:3000'); + expect(runtime.logger.warn).not.toHaveBeenCalled(); + }); + + it('warns when browser open fails', async () => { + const runtime = { + getMemoryDbPath: vi.fn().mockResolvedValue('/tmp/memory.db'), + logger: { + info: vi.fn(), + warn: vi.fn(), + }, + }; + + const action = createMemoryDashboardAction(runtime, { + startServer: vi.fn().mockResolvedValue({ + url: 'http://127.0.0.1:3000', + close: vi.fn(), + }), + openUrl: vi.fn().mockRejectedValue(new Error('missing opener')), + }); + + await action({ open: true }); + + expect(runtime.logger.warn).toHaveBeenCalledWith( + 'Unable to open browser automatically: missing opener. Open the printed URL manually.' + ); + }); + + it('rejects invalid ports before starting the server', async () => { + const startServer = vi.fn(); + const runtime = { + getMemoryDbPath: vi.fn().mockResolvedValue('/tmp/memory.db'), + logger: { + info: vi.fn(), + warn: vi.fn(), + }, + }; + + const action = createMemoryDashboardAction(runtime, { startServer }); + + await expect(action({ port: '65536' })).rejects.toThrow('Port must be an integer between 0 and 65535.'); + expect(startServer).not.toHaveBeenCalled(); + }); + + it('supports runtimes without a warn logger', async () => { + const runtime = { + getMemoryDbPath: vi.fn().mockResolvedValue('/tmp/memory.db'), + logger: { + info: vi.fn(), + }, + }; + + const action = createMemoryDashboardAction(runtime, { + startServer: vi.fn().mockResolvedValue({ + url: 'http://127.0.0.1:3000', + close: vi.fn(), + }), + openUrl: vi.fn().mockRejectedValue(new Error('missing opener')), + }); + + await expect(action({ open: true })).resolves.toBeUndefined(); + }); + + + it('registers host, port, and open options on the command', async () => { + const command = new Command('memory-dashboard'); + const runtime = { + getMemoryDbPath: vi.fn().mockResolvedValue('/tmp/memory.db'), + logger: { + info: vi.fn(), + warn: vi.fn(), + }, + }; + + register(command, runtime, { + startServer: vi.fn().mockResolvedValue({ + url: 'http://127.0.0.1:3000', + close: vi.fn(), + }), + }); + + await command.parseAsync(['node', 'memory-dashboard', '--host', '127.0.0.1', '--port', '3000', '--open'], { + from: 'user', + }); + + expect(runtime.getMemoryDbPath).toHaveBeenCalledOnce(); + }); + + it('includes dashboard options in help output', () => { + const command = new Command('memory-dashboard'); + const runtime = { + getMemoryDbPath: vi.fn().mockResolvedValue('/tmp/memory.db'), + logger: { + info: vi.fn(), + warn: vi.fn(), + }, + }; + + register(command, runtime, { + startServer: vi.fn(), + }); + + const help = command.helpInformation(); + + expect(help).toContain('--host '); + expect(help).toContain('--port '); + expect(help).toContain('--open'); + }); +}); diff --git a/packages/memory-dashboard/tests/server.test.ts b/packages/memory-dashboard/tests/server.test.ts new file mode 100644 index 00000000..88a4df42 --- /dev/null +++ b/packages/memory-dashboard/tests/server.test.ts @@ -0,0 +1,299 @@ +import { join } from 'path'; +import { tmpdir } from 'os'; +import { rmSync } from 'fs'; +import { memoryStoreCommand } from '@ai-devkit/memory'; +import { startDashboardServer } from '../src/server'; + +describe('dashboard server', () => { + const testDbPath = join(tmpdir(), `test-dashboard-server-${Date.now()}-${Math.random().toString(36)}.db`); + + afterEach(() => { + rmSync(testDbPath, { force: true }); + rmSync(`${testDbPath}-wal`, { force: true }); + rmSync(`${testDbPath}-shm`, { force: true }); + }); + + function seedDashboardRecords(): void { + memoryStoreCommand({ + dbPath: testDbPath, + title: 'Dashboard server route memory', + content: 'The memory dashboard server should expose JSON APIs backed by the configured memory database.', + tags: 'dashboard,server', + scope: 'project:ai-devkit', + }); + + for (let index = 0; index < 55; index += 1) { + memoryStoreCommand({ + dbPath: testDbPath, + title: `Dashboard complete list memory ${index}`, + content: `The dashboard memory list should return a bounded page while preserving the total matching count. Fixture row ${index}.`, + tags: 'dashboard,server', + scope: 'project:ai-devkit', + }); + } + } + + it('serves dashboard HTML and static assets', async () => { + const server = await startDashboardServer({ + dbPath: testDbPath, + host: '127.0.0.1', + port: 0, + }); + + try { + const html = await fetch(`${server.url}/`); + expect(html.status).toBe(200); + const htmlText = await html.text(); + expect(htmlText).toContain('AI DevKit Memory'); + expect(htmlText).toContain('id="memory-search"'); + expect(htmlText).toContain('id="scope-filter"'); + expect(htmlText).toContain('id="tag-filter"'); + expect(htmlText).toContain('id="memory-list"'); + expect(htmlText).toContain('id="memory-pagination"'); + expect(htmlText).toContain('id="memory-detail"'); + expect(htmlText).toContain('id="memory-graph"'); + expect(htmlText).toContain('id="graph-selected-title"'); + expect(htmlText).toContain('id="graph-zoom-out"'); + expect(htmlText).toContain('id="graph-zoom-in"'); + expect(htmlText).toContain('id="graph-fit"'); + expect(htmlText).toContain('id="graph-layout"'); + expect(htmlText).toContain('/assets/app.js'); + expect(htmlText).toContain('/assets/styles.css'); + + const css = await fetch(`${server.url}/assets/styles.css`); + expect(css.status).toBe(200); + const cssSource = await css.text(); + expect(cssSource).toContain('tailwindcss'); + expect(cssSource).toContain('.graph-canvas'); + expect(cssSource).toContain('.memory-pagination'); + + const app = await fetch(`${server.url}/assets/app.js`); + expect(app.status).toBe(200); + const appSource = await app.text(); + expect(appSource).toContain('/api/memory'); + expect(appSource).toContain('/api/summary'); + expect(appSource).toContain('/api/graph'); + expect(appSource).toContain('cytoscape'); + expect(appSource).toContain('URLSearchParams'); + expect(appSource).toContain('history.replaceState'); + expect(appSource).toContain('renderMemoryList'); + expect(appSource).toContain('renderPagination'); + expect(appSource).toContain('renderMemoryGraph'); + expect(appSource).toContain('zoomGraphBy'); + expect(appSource).toContain('fitGraphTo'); + expect(appSource).toContain('graph-selected-title'); + + const vendor = await fetch(`${server.url}/assets/vendor/cytoscape.esm.min.mjs`); + expect(vendor.status).toBe(200); + await expect(vendor.text()).resolves.toContain('cytoscape'); + } finally { + await server.close(); + } + }); + + it('returns paginated memory and summary APIs', async () => { + seedDashboardRecords(); + + const server = await startDashboardServer({ + dbPath: testDbPath, + host: '127.0.0.1', + port: 0, + }); + + try { + const memory = await fetch(`${server.url}/api/memory?query=dashboard&scope=project:ai-devkit&tag=server`); + const memoryBody = await memory.json(); + expect(memory.status).toBe(200); + expect(memoryBody.total).toBe(56); + expect(memoryBody.items).toHaveLength(50); + expect(memoryBody.items[0]).toEqual(expect.objectContaining({ + tags: ['dashboard', 'server'], + scope: 'project:ai-devkit', + })); + + const summary = await fetch(`${server.url}/api/summary`); + expect(summary.status).toBe(200); + await expect(summary.json()).resolves.toMatchObject({ + totalItems: 56, + scopes: [{ scope: 'project:ai-devkit', count: 56 }], + }); + + const sorted = await fetch(`${server.url}/api/memory?sort=title-asc&limit=1`); + const sortedBody = await sorted.json(); + expect(sorted.status).toBe(200); + expect(sortedBody.items[0].title).toBe('Dashboard complete list memory 0'); + } finally { + await server.close(); + } + }); + + it('returns JSON errors for unsupported methods and routes', async () => { + const server = await startDashboardServer({ + dbPath: testDbPath, + host: '127.0.0.1', + port: 0, + }); + + try { + const method = await fetch(`${server.url}/api/memory`, { method: 'POST' }); + expect(method.status).toBe(405); + await expect(method.json()).resolves.toMatchObject({ + error: 'Method not allowed', + }); + + const missing = await fetch(`${server.url}/not-found`); + expect(missing.status).toBe(404); + await expect(missing.json()).resolves.toMatchObject({ + error: 'Not found', + }); + } finally { + await server.close(); + } + }); + + it('returns JSON errors for invalid query parameters', async () => { + const server = await startDashboardServer({ + dbPath: testDbPath, + host: '127.0.0.1', + port: 0, + }); + + try { + const response = await fetch(`${server.url}/api/memory?limit=invalid`); + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + error: 'Invalid request', + }); + + const sortResponse = await fetch(`${server.url}/api/graph?sort=unknown`); + expect(sortResponse.status).toBe(400); + await expect(sortResponse.json()).resolves.toMatchObject({ + error: 'Invalid request', + message: 'sort must be updated-desc, created-desc, or title-asc.', + }); + } finally { + await server.close(); + } + }); + + it('returns empty memory, summary, and graph data for a missing database path', async () => { + const server = await startDashboardServer({ + dbPath: testDbPath, + host: '127.0.0.1', + port: 0, + }); + + try { + const memory = await fetch(`${server.url}/api/memory`); + expect(memory.status).toBe(200); + await expect(memory.json()).resolves.toMatchObject({ + total: 0, + items: [], + }); + + const summary = await fetch(`${server.url}/api/summary`); + expect(summary.status).toBe(200); + await expect(summary.json()).resolves.toMatchObject({ + totalItems: 0, + scopes: [], + tags: [], + }); + + const graph = await fetch(`${server.url}/api/graph`); + expect(graph.status).toBe(200); + await expect(graph.json()).resolves.toMatchObject({ + nodes: [], + edges: [], + }); + } finally { + await server.close(); + } + }); + + it('handles favicon requests without a console-visible 404', async () => { + const server = await startDashboardServer({ + dbPath: testDbPath, + host: '127.0.0.1', + port: 0, + }); + + try { + const response = await fetch(`${server.url}/favicon.ico`); + expect(response.status).toBe(204); + } finally { + await server.close(); + } + }); + + it('returns graph nodes and edges from filtered memory records', async () => { + memoryStoreCommand({ + dbPath: testDbPath, + title: 'Graph dashboard relationship memory', + content: 'Graph data should connect memory records to their tags and scopes for the dashboard.', + tags: 'graph,dashboard', + scope: 'project:ai-devkit', + }); + + const server = await startDashboardServer({ + dbPath: testDbPath, + host: '127.0.0.1', + port: 0, + }); + + try { + const response = await fetch(`${server.url}/api/graph?query=graph&tag=dashboard`); + expect(response.status).toBe(200); + const graph = await response.json(); + + expect(graph.nodes).toEqual(expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^memory:/), + type: 'memory', + label: 'Graph dashboard relationship memory', + item: expect.objectContaining({ + title: 'Graph dashboard relationship memory', + content: 'Graph data should connect memory records to their tags and scopes for the dashboard.', + }), + }), + { id: 'tag:dashboard', type: 'tag', label: 'dashboard', count: 1 }, + { id: 'tag:graph', type: 'tag', label: 'graph', count: 1 }, + { id: 'scope:project:ai-devkit', type: 'scope', label: 'project:ai-devkit', count: 1 }, + ])); + expect(graph.edges).toEqual(expect.arrayContaining([ + expect.objectContaining({ type: 'has-tag', target: 'tag:dashboard' }), + expect.objectContaining({ type: 'in-scope', target: 'scope:project:ai-devkit' }), + ])); + } finally { + await server.close(); + } + }); + + it('uses a 250 item default graph limit', async () => { + for (let index = 0; index < 120; index += 1) { + memoryStoreCommand({ + dbPath: testDbPath, + title: `Graph limit dashboard memory ${index}`, + content: `Graph default limit should include more than one hundred records. Fixture row ${index}.`, + tags: 'graph-limit', + scope: 'project:ai-devkit', + }); + } + + const server = await startDashboardServer({ + dbPath: testDbPath, + host: '127.0.0.1', + port: 0, + }); + + try { + const response = await fetch(`${server.url}/api/graph?tag=graph-limit`); + expect(response.status).toBe(200); + const graph = await response.json(); + const memoryNodes = graph.nodes.filter((node: { type: string }) => node.type === 'memory'); + + expect(memoryNodes).toHaveLength(120); + } finally { + await server.close(); + } + }); +}); diff --git a/packages/memory-dashboard/tests/standalone.test.ts b/packages/memory-dashboard/tests/standalone.test.ts new file mode 100644 index 00000000..7b4b460e --- /dev/null +++ b/packages/memory-dashboard/tests/standalone.test.ts @@ -0,0 +1,92 @@ +import { join } from 'path'; +import { tmpdir } from 'os'; +import { mkdirSync, rmSync, writeFileSync } from 'fs'; +import { createStandaloneMemoryRuntime, parseStandaloneOptions } from '../src/standalone'; + +describe('standalone memory dashboard launcher', () => { + const homeDir = join(tmpdir(), `memory-dashboard-home-${Date.now()}-${Math.random().toString(36)}`); + + afterEach(() => { + rmSync(homeDir, { recursive: true, force: true }); + }); + + it('resolves the same default memory database path as the plugin runtime', async () => { + const runtime = createStandaloneMemoryRuntime({ homeDir }); + + await expect(runtime.getMemoryDbPath()).resolves.toBe(join(homeDir, '.ai-devkit', 'memory.db')); + }); + + it('resolves configured relative memory paths from the AI DevKit config directory', async () => { + mkdirSync(join(homeDir, '.ai-devkit'), { recursive: true }); + writeFileSync(join(homeDir, '.ai-devkit', '.ai-devkit.json'), JSON.stringify({ + memory: { + path: 'custom-memory.db', + }, + })); + + const runtime = createStandaloneMemoryRuntime({ homeDir }); + + await expect(runtime.getMemoryDbPath()).resolves.toBe(join(homeDir, '.ai-devkit', 'custom-memory.db')); + }); + + it('resolves configured absolute memory paths unchanged', async () => { + mkdirSync(join(homeDir, '.ai-devkit'), { recursive: true }); + writeFileSync(join(homeDir, '.ai-devkit', '.ai-devkit.json'), JSON.stringify({ + memory: { + path: '/tmp/absolute-memory.db', + }, + })); + + const runtime = createStandaloneMemoryRuntime({ homeDir }); + + await expect(runtime.getMemoryDbPath()).resolves.toBe('/tmp/absolute-memory.db'); + }); + + it('ignores blank configured memory paths', async () => { + mkdirSync(join(homeDir, '.ai-devkit'), { recursive: true }); + writeFileSync(join(homeDir, '.ai-devkit', '.ai-devkit.json'), JSON.stringify({ + memory: { + path: ' ', + }, + })); + + const runtime = createStandaloneMemoryRuntime({ homeDir }); + + await expect(runtime.getMemoryDbPath()).resolves.toBe(join(homeDir, '.ai-devkit', 'memory.db')); + }); + + it('surfaces invalid AI DevKit config JSON', async () => { + mkdirSync(join(homeDir, '.ai-devkit'), { recursive: true }); + writeFileSync(join(homeDir, '.ai-devkit', '.ai-devkit.json'), '{'); + + const runtime = createStandaloneMemoryRuntime({ homeDir }); + + await expect(runtime.getMemoryDbPath()).rejects.toThrow(); + }); + + it('lets standalone launches override the memory database path', async () => { + const runtime = createStandaloneMemoryRuntime({ + homeDir, + dbPathOverride: '/tmp/standalone-memory.db', + }); + + await expect(runtime.getMemoryDbPath()).resolves.toBe('/tmp/standalone-memory.db'); + }); + + it('parses standalone dashboard options', () => { + expect(parseStandaloneOptions([ + '--host', + '127.0.0.2', + '--port', + '4567', + '--db-path', + '/tmp/memory.db', + '--open', + ])).toEqual({ + host: '127.0.0.2', + port: '4567', + dbPath: '/tmp/memory.db', + open: true, + }); + }); +}); diff --git a/packages/memory-dashboard/tsconfig.json b/packages/memory-dashboard/tsconfig.json new file mode 100644 index 00000000..d0258071 --- /dev/null +++ b/packages/memory-dashboard/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022", + "DOM" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "sourceMap": true, + "resolveJsonModule": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "isolatedModules": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "tests" + ] +} diff --git a/packages/memory-dashboard/vitest.config.ts b/packages/memory-dashboard/vitest.config.ts new file mode 100644 index 00000000..fe330c0c --- /dev/null +++ b/packages/memory-dashboard/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['src/**/*.ts'], + exclude: ['src/index.ts', 'src/**/*.d.ts'], + thresholds: { + branches: 75, + functions: 75, + lines: 75, + statements: 75, + }, + }, + }, +}); diff --git a/packages/memory/src/api.ts b/packages/memory/src/api.ts index c12428d9..5bd54121 100644 --- a/packages/memory/src/api.ts +++ b/packages/memory/src/api.ts @@ -1,11 +1,13 @@ import { storeKnowledge } from './handlers/store.js'; import { searchKnowledge } from './handlers/search.js'; import { updateKnowledge } from './handlers/update.js'; +import { listKnowledge } from './handlers/list.js'; +import { getKnowledgeSummary } from './handlers/summary.js'; import { closeDatabase, getDatabase } from './database/index.js'; -import type { StoreKnowledgeInput, SearchKnowledgeInput, StoreKnowledgeResult, SearchKnowledgeResult, UpdateKnowledgeInput, UpdateKnowledgeResult } from './types/index.js'; +import type { StoreKnowledgeInput, SearchKnowledgeInput, StoreKnowledgeResult, SearchKnowledgeResult, UpdateKnowledgeInput, UpdateKnowledgeResult, ListKnowledgeInput, ListKnowledgeResult, ListKnowledgeSort, KnowledgeSummaryResult, KnowledgeItem } from './types/index.js'; -export { storeKnowledge, searchKnowledge, updateKnowledge }; -export type { StoreKnowledgeInput, SearchKnowledgeInput, StoreKnowledgeResult, SearchKnowledgeResult, UpdateKnowledgeInput, UpdateKnowledgeResult }; +export { storeKnowledge, searchKnowledge, updateKnowledge, listKnowledge, getKnowledgeSummary }; +export type { StoreKnowledgeInput, SearchKnowledgeInput, StoreKnowledgeResult, SearchKnowledgeResult, UpdateKnowledgeInput, UpdateKnowledgeResult, ListKnowledgeInput, ListKnowledgeResult, ListKnowledgeSort, KnowledgeSummaryResult, KnowledgeItem }; // CLI command handlers for integration with main ai-devkit CLI export interface MemoryStoreOptions { @@ -33,6 +35,20 @@ export interface MemorySearchOptions { dbPath?: string; } +export interface MemoryListOptions { + query?: string; + tags?: string; + scope?: string; + limit?: number; + offset?: number; + sort?: ListKnowledgeSort; + dbPath?: string; +} + +export interface MemorySummaryOptions { + dbPath?: string; +} + export function memoryStoreCommand(options: MemoryStoreOptions): StoreKnowledgeResult { try { getDatabase({ dbPath: options.dbPath }); @@ -81,3 +97,30 @@ export function memorySearchCommand(options: MemorySearchOptions): SearchKnowled closeDatabase(); } } + +export function memoryListCommand(options: MemoryListOptions = {}): ListKnowledgeResult { + try { + getDatabase({ dbPath: options.dbPath }); + const input: ListKnowledgeInput = { + query: options.query, + tags: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined, + scope: options.scope, + limit: options.limit, + offset: options.offset, + sort: options.sort, + }; + + return listKnowledge(input); + } finally { + closeDatabase(); + } +} + +export function memorySummaryCommand(options: MemorySummaryOptions = {}): KnowledgeSummaryResult { + try { + getDatabase({ dbPath: options.dbPath }); + return getKnowledgeSummary(); + } finally { + closeDatabase(); + } +} diff --git a/packages/memory/src/handlers/list.ts b/packages/memory/src/handlers/list.ts new file mode 100644 index 00000000..3cffdb18 --- /dev/null +++ b/packages/memory/src/handlers/list.ts @@ -0,0 +1,160 @@ +import { getDatabase } from '../database/index.js'; +import { buildFtsQuery } from '../services/search.js'; +import { normalizeScope, normalizeTags } from '../services/normalizer.js'; +import { ValidationError } from '../utils/errors.js'; +import type { KnowledgeItem, KnowledgeRow, ListKnowledgeInput, ListKnowledgeResult, ListKnowledgeSort } from '../types/index.js'; + +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 200; +const MAX_OFFSET = 100_000; +const MAX_QUERY_LENGTH = 500; + +interface ListQuery { + where: string[]; + params: unknown[]; +} + +export function listKnowledge(input: ListKnowledgeInput = {}): ListKnowledgeResult { + validateListInput(input); + + const db = getDatabase(); + const limit = clampLimit(input.limit); + const offset = clampOffset(input.offset); + const query = buildListQuery(input); + const orderBy = getOrderBy(input.sort); + const whereSql = query.where.length > 0 ? `WHERE ${query.where.join(' AND ')}` : ''; + + const totalRow = db.queryOne<{ total: number }>( + `SELECT COUNT(*) as total FROM knowledge k ${whereSql}`, + query.params + ); + + const rows = db.query( + `SELECT + k.id, + k.title, + k.content, + k.tags, + k.scope, + k.normalized_title, + k.content_hash, + k.created_at, + k.updated_at + FROM knowledge k + ${whereSql} + ${orderBy} + LIMIT ? OFFSET ?`, + [...query.params, limit, offset] + ); + + return { + items: rows.map(mapKnowledgeRow), + total: totalRow?.total ?? 0, + }; +} + +function validateListInput(input: ListKnowledgeInput): void { + const errors: string[] = []; + + if (input.query !== undefined && typeof input.query !== 'string') { + errors.push('Query must be a string'); + } + + if (typeof input.query === 'string' && input.query.length > MAX_QUERY_LENGTH) { + errors.push(`Query must be at most ${MAX_QUERY_LENGTH} characters`); + } + + if (input.limit !== undefined && (!Number.isInteger(input.limit) || input.limit < 1)) { + errors.push('Limit must be a positive integer'); + } + + if (input.offset !== undefined && (!Number.isInteger(input.offset) || input.offset < 0)) { + errors.push('Offset must be a non-negative integer'); + } + + if (input.sort !== undefined && !isSupportedSort(input.sort)) { + errors.push('Sort must be one of updated-desc, created-desc, or title-asc'); + } + + if (errors.length > 0) { + throw new ValidationError(errors.join('; '), { errors }); + } +} + +function buildListQuery(input: ListKnowledgeInput): ListQuery { + const where: string[] = []; + const params: unknown[] = []; + const query = input.query?.trim(); + + if (query) { + const ftsQuery = buildFtsQuery(query); + if (ftsQuery) { + where.push(`k.rowid IN ( + SELECT rowid FROM knowledge_fts WHERE knowledge_fts MATCH ? + )`); + params.push(ftsQuery); + } + } + + if (input.scope) { + where.push('k.scope = ?'); + params.push(normalizeScope(input.scope)); + } + + const tags = normalizeTags(input.tags ?? []); + for (const tag of tags) { + where.push('k.tags LIKE ?'); + params.push(`%"${tag}"%`); + } + + return { where, params }; +} + +function mapKnowledgeRow(row: KnowledgeRow): KnowledgeItem { + return { + id: row.id, + title: row.title, + content: row.content, + tags: parseTags(row.tags), + scope: row.scope, + normalizedTitle: row.normalized_title, + contentHash: row.content_hash, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +} + +function parseTags(rawTags: string): string[] { + try { + const tags = JSON.parse(rawTags) as unknown; + return Array.isArray(tags) + ? tags.filter((tag): tag is string => typeof tag === 'string') + : []; + } catch { + return []; + } +} + +function clampLimit(limit: number | undefined): number { + return Math.min(Math.max(limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT); +} + +function clampOffset(offset: number | undefined): number { + return Math.min(Math.max(offset ?? 0, 0), MAX_OFFSET); +} + +function getOrderBy(sort: ListKnowledgeSort | undefined): string { + switch (sort) { + case 'created-desc': + return 'ORDER BY k.created_at DESC, k.title ASC'; + case 'title-asc': + return 'ORDER BY k.title ASC, k.created_at DESC'; + case 'updated-desc': + case undefined: + return 'ORDER BY k.updated_at DESC, k.title ASC'; + } +} + +function isSupportedSort(sort: string): sort is ListKnowledgeSort { + return sort === 'updated-desc' || sort === 'created-desc' || sort === 'title-asc'; +} diff --git a/packages/memory/src/handlers/summary.ts b/packages/memory/src/handlers/summary.ts new file mode 100644 index 00000000..dc52a38b --- /dev/null +++ b/packages/memory/src/handlers/summary.ts @@ -0,0 +1,94 @@ +import { getDatabase } from '../database/index.js'; +import type { KnowledgeRow, KnowledgeSummaryResult } from '../types/index.js'; + +type RecencyBucket = 'today' | 'week' | 'month' | 'older'; + +const RECENCY_BUCKETS: RecencyBucket[] = ['today', 'week', 'month', 'older']; + +export function getKnowledgeSummary(): KnowledgeSummaryResult { + const db = getDatabase(); + const totalRow = db.queryOne<{ total: number }>('SELECT COUNT(*) as total FROM knowledge'); + const scopes = db.query<{ scope: string; count: number }>( + `SELECT scope, COUNT(*) as count + FROM knowledge + GROUP BY scope + ORDER BY scope ASC` + ); + const rows = db.query( + `SELECT id, title, content, tags, scope, normalized_title, content_hash, created_at, updated_at + FROM knowledge` + ); + + return { + totalItems: totalRow?.total ?? 0, + scopes, + tags: buildTagCounts(rows), + recency: buildRecencyCounts(rows), + }; +} + +function buildTagCounts(rows: KnowledgeRow[]): Array<{ tag: string; count: number }> { + const counts = new Map(); + + for (const row of rows) { + for (const tag of parseTags(row.tags)) { + counts.set(tag, (counts.get(tag) ?? 0) + 1); + } + } + + return Array.from(counts.entries()) + .map(([tag, count]) => ({ tag, count })) + .sort((left, right) => right.count - left.count || left.tag.localeCompare(right.tag)); +} + +function buildRecencyCounts(rows: KnowledgeRow[]): Array<{ bucket: string; count: number }> { + const counts = new Map( + RECENCY_BUCKETS.map(bucket => [bucket, 0]) + ); + const now = Date.now(); + + for (const row of rows) { + const bucket = getRecencyBucket(row.updated_at, now); + counts.set(bucket, (counts.get(bucket) ?? 0) + 1); + } + + return RECENCY_BUCKETS.map(bucket => ({ + bucket, + count: counts.get(bucket) ?? 0, + })); +} + +function getRecencyBucket(updatedAt: string, now: number): RecencyBucket { + const timestamp = Date.parse(updatedAt); + if (Number.isNaN(timestamp)) { + return 'older'; + } + + const ageMs = now - timestamp; + const dayMs = 24 * 60 * 60 * 1000; + + if (ageMs <= dayMs) { + return 'today'; + } + + if (ageMs <= 7 * dayMs) { + return 'week'; + } + + if (ageMs <= 30 * dayMs) { + return 'month'; + } + + return 'older'; +} + +function parseTags(rawTags: string): string[] { + try { + const tags = JSON.parse(rawTags) as unknown; + return Array.isArray(tags) + ? tags.filter((tag): tag is string => typeof tag === 'string') + : []; + } catch { + return []; + } +} diff --git a/packages/memory/src/types/index.ts b/packages/memory/src/types/index.ts index 9ef73543..b990350b 100644 --- a/packages/memory/src/types/index.ts +++ b/packages/memory/src/types/index.ts @@ -61,6 +61,29 @@ export interface SearchKnowledgeResult { query: string; } +export type ListKnowledgeSort = 'updated-desc' | 'created-desc' | 'title-asc'; + +export interface ListKnowledgeInput { + query?: string; + tags?: string[]; + scope?: string; + limit?: number; + offset?: number; + sort?: ListKnowledgeSort; +} + +export interface ListKnowledgeResult { + items: KnowledgeItem[]; + total: number; +} + +export interface KnowledgeSummaryResult { + totalItems: number; + scopes: Array<{ scope: string; count: number }>; + tags: Array<{ tag: string; count: number }>; + recency: Array<{ bucket: string; count: number }>; +} + export interface KnowledgeRow { id: string; title: string; @@ -76,4 +99,4 @@ export interface KnowledgeRow { export interface MetaRow { key: string; value: string; -} \ No newline at end of file +} diff --git a/packages/memory/tests/integration/list.test.ts b/packages/memory/tests/integration/list.test.ts new file mode 100644 index 00000000..550f2bc7 --- /dev/null +++ b/packages/memory/tests/integration/list.test.ts @@ -0,0 +1,98 @@ +import { join } from 'path'; +import { tmpdir } from 'os'; +import { rmSync } from 'fs'; +import { + memoryListCommand, + memoryStoreCommand, + type ListKnowledgeResult, +} from '../../src/api'; +import { closeDatabase } from '../../src/database'; + +describe('list knowledge command', () => { + const testDbPath = join(tmpdir(), `test-list-${Date.now()}-${Math.random().toString(36)}.db`); + + beforeEach(() => { + closeDatabase(); + rmSync(testDbPath, { force: true }); + rmSync(`${testDbPath}-wal`, { force: true }); + rmSync(`${testDbPath}-shm`, { force: true }); + }); + + afterAll(() => { + closeDatabase(); + rmSync(testDbPath, { force: true }); + rmSync(`${testDbPath}-wal`, { force: true }); + rmSync(`${testDbPath}-shm`, { force: true }); + }); + + function seedKnowledge(): void { + memoryStoreCommand({ + dbPath: testDbPath, + title: 'Always use Response DTOs for API endpoints', + content: 'When building REST APIs, always use Response DTOs instead of returning domain entities directly for stable contracts.', + tags: 'api,backend,dto', + scope: 'global', + }); + memoryStoreCommand({ + dbPath: testDbPath, + title: 'Project memory dashboard graph strategy', + content: 'The memory dashboard graph should connect memory items to tag and scope nodes for lightweight local inspection.', + tags: 'dashboard,graph', + scope: 'project:ai-devkit', + }); + memoryStoreCommand({ + dbPath: testDbPath, + title: 'Repository plugin command convention', + content: 'Plugin command entrypoints should export register and use built JavaScript files for runtime loading.', + tags: 'plugin,command', + scope: 'repo:ai-devkit', + }); + } + + it('returns bounded memory items with normalized fields', () => { + seedKnowledge(); + + const result: ListKnowledgeResult = memoryListCommand({ + dbPath: testDbPath, + limit: 2, + }); + + expect(result.total).toBe(3); + expect(result.items).toHaveLength(2); + expect(result.items[0]).toEqual(expect.objectContaining({ + id: expect.any(String), + title: expect.any(String), + content: expect.any(String), + tags: expect.any(Array), + scope: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), + })); + }); + + it('returns all matching memory items when no limit is provided', () => { + seedKnowledge(); + + const result = memoryListCommand({ + dbPath: testDbPath, + }); + + expect(result.total).toBe(3); + expect(result.items).toHaveLength(3); + }); + + it('filters by query, scope, and tags', () => { + seedKnowledge(); + + const result = memoryListCommand({ + dbPath: testDbPath, + query: 'dashboard graph', + scope: 'project:ai-devkit', + tags: 'dashboard', + }); + + expect(result.total).toBe(1); + expect(result.items).toHaveLength(1); + expect(result.items[0]?.title).toBe('Project memory dashboard graph strategy'); + }); +}); diff --git a/packages/memory/tests/integration/summary.test.ts b/packages/memory/tests/integration/summary.test.ts new file mode 100644 index 00000000..9c5f6dfc --- /dev/null +++ b/packages/memory/tests/integration/summary.test.ts @@ -0,0 +1,74 @@ +import { join } from 'path'; +import { tmpdir } from 'os'; +import { rmSync } from 'fs'; +import { + memoryStoreCommand, + memorySummaryCommand, + type KnowledgeSummaryResult, +} from '../../src/api'; +import { closeDatabase } from '../../src/database'; + +describe('knowledge summary command', () => { + const testDbPath = join(tmpdir(), `test-summary-${Date.now()}-${Math.random().toString(36)}.db`); + + beforeEach(() => { + closeDatabase(); + rmSync(testDbPath, { force: true }); + rmSync(`${testDbPath}-wal`, { force: true }); + rmSync(`${testDbPath}-shm`, { force: true }); + }); + + afterAll(() => { + closeDatabase(); + rmSync(testDbPath, { force: true }); + rmSync(`${testDbPath}-wal`, { force: true }); + rmSync(`${testDbPath}-shm`, { force: true }); + }); + + it('returns empty counts for a new database', () => { + const result: KnowledgeSummaryResult = memorySummaryCommand({ dbPath: testDbPath }); + + expect(result).toEqual({ + totalItems: 0, + scopes: [], + tags: [], + recency: [ + { bucket: 'today', count: 0 }, + { bucket: 'week', count: 0 }, + { bucket: 'month', count: 0 }, + { bucket: 'older', count: 0 }, + ], + }); + }); + + it('counts scopes, tags, and recency buckets', () => { + memoryStoreCommand({ + dbPath: testDbPath, + title: 'Global dashboard memory convention', + content: 'The memory dashboard should expose a local read-only view for inspecting global memory records safely.', + tags: 'dashboard,read', + scope: 'global', + }); + memoryStoreCommand({ + dbPath: testDbPath, + title: 'Project plugin memory dashboard plan', + content: 'The dashboard plugin should reuse the plugin runtime and configured memory database path.', + tags: 'dashboard,plugin', + scope: 'project:ai-devkit', + }); + + const result = memorySummaryCommand({ dbPath: testDbPath }); + + expect(result.totalItems).toBe(2); + expect(result.scopes).toEqual([ + { scope: 'global', count: 1 }, + { scope: 'project:ai-devkit', count: 1 }, + ]); + expect(result.tags).toEqual([ + { tag: 'dashboard', count: 2 }, + { tag: 'plugin', count: 1 }, + { tag: 'read', count: 1 }, + ]); + expect(result.recency.find(bucket => bucket.bucket === 'today')?.count).toBe(2); + }); +}); From 393799c6b8d9f38914831d7d9e4801a63997cf1d Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Mon, 15 Jun 2026 14:33:56 +0200 Subject: [PATCH 2/2] Update readme --- packages/memory-dashboard/README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/memory-dashboard/README.md b/packages/memory-dashboard/README.md index 540460a8..ae872912 100644 --- a/packages/memory-dashboard/README.md +++ b/packages/memory-dashboard/README.md @@ -1,23 +1,32 @@ # @ai-devkit/memory-dashboard -Local web dashboard for browsing and assessing AI DevKit memory. +A local web dashboard for browsing AI DevKit memory. + +## Usage + +After installing the plugin, start the dashboard with: ```bash ai-devkit memory-dashboard ``` -The MVP is implemented as an AI DevKit plugin package with a `memory-dashboard` command. +The command prints a local URL. Open it in your browser to search, filter, group, and inspect memory records. -The UI is split across `src/ui/dashboard.html`, `src/ui/tailwind.css`, and `src/ui/app.js`. The build emits Tailwind CSS and copies the UI assets into `dist/ui`. The graph view uses Cytoscape.js with memory, tag, and scope nodes plus Fit/Layout controls. - -For local development without installing the plugin into AI DevKit: +## Development ```bash npm run dev:standalone ``` -The standalone script reads the same default/configured memory database path as the plugin runtime. Use `--db-path` to point it at a fixture database: +Use a custom database path when testing with fixture data: ```bash npm run dev:standalone -- --db-path /tmp/memory.db --port 3000 ``` + +## Build + +```bash +npm run build +npm test +```