← back to Dw Boardroom V2
initial scaffold (gitify-all 2026-05-06)
82c61294cec09d803d787f6438e3dbf04ed537cf · 2026-05-06 10:25:15 -0700 · Steve Abrams
Files touched
A .gitignoreA boardroom.db-shmA boardroom.db-walA ecosystem.config.cjsA frontend/index.htmlA frontend/package-lock.jsonA frontend/package.jsonA frontend/serve.cjsA frontend/src/App.tsxA frontend/src/api.tsA frontend/src/components/AgendaManager.tsxA frontend/src/components/AgentRoster.tsxA frontend/src/components/LiveFeed.tsxA frontend/src/components/MeetingControls.tsxA frontend/src/components/MeetingRoom.tsxA frontend/src/components/PhaseBar.tsxA frontend/src/hooks/useApi.tsA frontend/src/hooks/useWebSocket.tsA frontend/src/main.tsxA frontend/src/types.tsA frontend/tsconfig.jsonA frontend/vite.config.tsA package-lock.jsonA package.jsonA src/agents/president.tsA src/agents/registry.tsA src/agents/voices.tsA src/api/middleware/auth.tsA src/api/routes/agenda.tsA src/api/routes/agents.tsA src/api/routes/health.tsA src/api/routes/meetings.tsA src/api/routes/voice.tsA src/api/server.tsA src/db/migrate.tsA src/db/schema.sqlA src/db/sqlite.tsA src/engine/agentDialogue.tsA src/engine/governanceClient.tsA src/engine/meetingEngine.tsA src/types.tsA src/ws/broadcast.tsA tsconfig.json
Diff
commit 82c61294cec09d803d787f6438e3dbf04ed537cf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 10:25:15 2026 -0700
initial scaffold (gitify-all 2026-05-06)
---
.gitignore | 16 +
boardroom.db-shm | Bin 0 -> 32768 bytes
boardroom.db-wal | Bin 0 -> 3563832 bytes
ecosystem.config.cjs | 29 +
frontend/index.html | 42 +
frontend/package-lock.json | 1846 +++++++++++++++++++++++++++
frontend/package.json | 22 +
frontend/serve.cjs | 43 +
frontend/src/App.tsx | 108 ++
frontend/src/api.ts | 47 +
frontend/src/components/AgendaManager.tsx | 132 ++
frontend/src/components/AgentRoster.tsx | 125 ++
frontend/src/components/LiveFeed.tsx | 77 ++
frontend/src/components/MeetingControls.tsx | 74 ++
frontend/src/components/MeetingRoom.tsx | 86 ++
frontend/src/components/PhaseBar.tsx | 68 +
frontend/src/hooks/useApi.ts | 20 +
frontend/src/hooks/useWebSocket.ts | 54 +
frontend/src/main.tsx | 9 +
frontend/src/types.ts | 62 +
frontend/tsconfig.json | 21 +
frontend/vite.config.ts | 22 +
package-lock.json | 1626 +++++++++++++++++++++++
package.json | 31 +
src/agents/president.ts | 58 +
src/agents/registry.ts | 148 +++
src/agents/voices.ts | 79 ++
src/api/middleware/auth.ts | 25 +
src/api/routes/agenda.ts | 65 +
src/api/routes/agents.ts | 67 +
src/api/routes/health.ts | 45 +
src/api/routes/meetings.ts | 121 ++
src/api/routes/voice.ts | 51 +
src/api/server.ts | 79 ++
src/db/migrate.ts | 37 +
src/db/schema.sql | 58 +
src/db/sqlite.ts | 45 +
src/engine/agentDialogue.ts | 153 +++
src/engine/governanceClient.ts | 128 ++
src/engine/meetingEngine.ts | 376 ++++++
src/types.ts | 182 +++
src/ws/broadcast.ts | 62 +
tsconfig.json | 19 +
43 files changed, 6358 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bc9cc73
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+node_modules/
+dist/
+.env
+*.db
+*.sqlite
+frontend/node_modules/
+frontend/dist/
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+build/
+.next/
+*.bak
diff --git a/boardroom.db-shm b/boardroom.db-shm
new file mode 100644
index 0000000..129f4b4
Binary files /dev/null and b/boardroom.db-shm differ
diff --git a/boardroom.db-wal b/boardroom.db-wal
new file mode 100644
index 0000000..07f7d0f
Binary files /dev/null and b/boardroom.db-wal differ
diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
new file mode 100644
index 0000000..3825241
--- /dev/null
+++ b/ecosystem.config.cjs
@@ -0,0 +1,29 @@
+module.exports = {
+ apps: [
+ {
+ name: 'dw-boardroom-api',
+ script: 'dist/api/server.js',
+ cwd: '/root/Projects/dw-boardroom-v2',
+ env: {
+ PORT: 4040,
+ NODE_ENV: 'production',
+ GOVERNANCE_URL: 'http://127.0.0.1:4020',
+ GEMINI_KEY: 'REDACTED_GEMINI_KEY',
+ AUTH_USER: 'admin',
+ AUTH_PASS: 'REDACTED_PASSWORD',
+ FRONTEND_URL: 'http://45.61.58.125:4050',
+ },
+ node_args: '--max-old-space-size=128',
+ max_memory_restart: '150M',
+ },
+ {
+ name: 'dw-boardroom-ui',
+ script: './serve.cjs',
+ cwd: '/root/Projects/dw-boardroom-v2/frontend',
+ env: {
+ NODE_ENV: 'production',
+ },
+ max_memory_restart: '100M',
+ }
+ ]
+};
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..2268be5
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>DW Boardroom V2</title>
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
+ <style>
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+ body {
+ font-family: 'Inter', -apple-system, system-ui, sans-serif;
+ background: #0a0a12;
+ color: #e8eaf0;
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+ }
+ #root { min-height: 100vh; }
+ ::-webkit-scrollbar { width: 6px; }
+ ::-webkit-scrollbar-track { background: transparent; }
+ ::-webkit-scrollbar-thumb { background: #252540; border-radius: 3px; }
+ ::-webkit-scrollbar-thumb:hover { background: #8b5cf6; }
+ input[type="range"] {
+ -webkit-appearance: none;
+ height: 4px;
+ background: #252540;
+ border-radius: 2px;
+ outline: none;
+ }
+ input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 14px; height: 14px;
+ border-radius: 50%;
+ background: #8b5cf6;
+ cursor: pointer;
+ }
+ </style>
+</head>
+<body>
+ <div id="root"></div>
+ <script type="module" src="/src/main.tsx"></script>
+</body>
+</html>
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..a87c0b3
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1846 @@
+{
+ "name": "dw-boardroom-v2-frontend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dw-boardroom-v2-frontend",
+ "version": "1.0.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.3"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
+ "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001770",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
+ "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.286",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+ "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..27fd7b7
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "dw-boardroom-v2-frontend",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.3"
+ }
+}
diff --git a/frontend/serve.cjs b/frontend/serve.cjs
new file mode 100644
index 0000000..55e44b1
--- /dev/null
+++ b/frontend/serve.cjs
@@ -0,0 +1,43 @@
+// Simple static server with Basic Auth for Boardroom V2 UI
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = 4050;
+const USER = 'admin';
+const PASS = 'REDACTED_PASSWORD';
+const DIST = path.join(__dirname, 'dist');
+
+const MIME = {
+ '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
+ '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
+ '.svg': 'image/svg+xml', '.woff2': 'font/woff2', '.woff': 'font/woff',
+};
+
+const server = http.createServer((req, res) => {
+ const auth = req.headers.authorization;
+ if (!auth || !auth.startsWith('Basic ')) {
+ res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Boardroom V2"' });
+ return res.end('Unauthorized');
+ }
+ const [u, p] = Buffer.from(auth.slice(6), 'base64').toString().split(':');
+ if (u !== USER || p !== PASS) {
+ res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Boardroom V2"' });
+ return res.end('Unauthorized');
+ }
+
+ let filePath = path.join(DIST, req.url === '/' ? 'index.html' : req.url);
+ if (!fs.existsSync(filePath)) filePath = path.join(DIST, 'index.html');
+ const ext = path.extname(filePath);
+ const mime = MIME[ext] || 'application/octet-stream';
+ try {
+ const content = fs.readFileSync(filePath);
+ res.writeHead(200, { 'Content-Type': mime });
+ res.end(content);
+ } catch {
+ res.writeHead(404);
+ res.end('Not found');
+ }
+});
+
+server.listen(PORT, '0.0.0.0', () => console.log(`[Boardroom V2 UI] Serving on port ${PORT} with auth`));
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..d349b1c
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,108 @@
+import { useState, useEffect } from 'react';
+import { api } from './api';
+import { useWebSocket } from './hooks/useWebSocket';
+import MeetingRoom from './components/MeetingRoom';
+import AgendaManager from './components/AgendaManager';
+import AgentRoster from './components/AgentRoster';
+
+const TABS = [
+ { id: 'meeting', label: 'Meeting Room', icon: '\uD83C\uDFDB' },
+ { id: 'agenda', label: 'Agenda', icon: '\uD83D\uDCCB' },
+ { id: 'agents', label: 'Agents', icon: '\uD83E\uDD16' },
+];
+
+export default function App() {
+ const [tab, setTab] = useState('meeting');
+ const [status, setStatus] = useState<any>(null);
+ const { events, connected } = useWebSocket();
+
+ useEffect(() => {
+ api.getStatus().then(setStatus).catch(() => {});
+ const interval = setInterval(() => {
+ api.getStatus().then(setStatus).catch(() => {});
+ }, 15000);
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+ <div style={{ minHeight: '100vh', background: '#0a0a12' }}>
+ {/* Header */}
+ <header style={styles.header}>
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
+ <span style={{ fontSize: 20 }}>{'\uD83C\uDFDB'}</span>
+ <h1 style={{ fontSize: 16, fontWeight: 800, margin: 0, letterSpacing: '-0.3px' }}>DW Boardroom V2</h1>
+ </div>
+ <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
+ <span style={{ ...styles.badge, background: connected ? '#22c55e22' : '#ef444422', color: connected ? '#22c55e' : '#ef4444' }}>
+ {connected ? 'WS Live' : 'WS Off'}
+ </span>
+ {status?.activeMeeting && (
+ <span style={{ ...styles.badge, background: '#8b5cf622', color: '#8b5cf6' }}>
+ {status.activeMeeting.type} — {status.activeMeeting.phase}
+ </span>
+ )}
+ <span style={{ ...styles.badge, background: '#3b82f622', color: '#3b82f6' }}>
+ {status?.agents || 0} agents
+ </span>
+ <span style={{
+ ...styles.badge,
+ background: status?.governance?.available ? '#22c55e22' : '#ef444422',
+ color: status?.governance?.available ? '#22c55e' : '#ef4444'
+ }}>
+ Gov {status?.governance?.available ? 'OK' : 'OFF'}
+ </span>
+ </div>
+ </header>
+
+ {/* Tabs */}
+ <nav style={styles.nav}>
+ {TABS.map(t => (
+ <button
+ key={t.id}
+ onClick={() => setTab(t.id)}
+ style={{
+ ...styles.tab,
+ ...(tab === t.id ? styles.tabActive : {}),
+ }}
+ >
+ <span style={{ fontSize: 14 }}>{t.icon}</span>
+ {t.label}
+ </button>
+ ))}
+ </nav>
+
+ {/* Content */}
+ <main style={styles.main}>
+ {tab === 'meeting' && <MeetingRoom events={events} />}
+ {tab === 'agenda' && <AgendaManager />}
+ {tab === 'agents' && <AgentRoster />}
+ </main>
+ </div>
+ );
+}
+
+const styles: Record<string, React.CSSProperties> = {
+ header: {
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
+ padding: '12px 24px', borderBottom: '1px solid #252540',
+ background: 'linear-gradient(180deg, #0f0f1e 0%, #0a0a12 100%)',
+ },
+ badge: {
+ padding: '4px 10px', borderRadius: 6, fontSize: 11, fontWeight: 700,
+ },
+ nav: {
+ display: 'flex', gap: 4, padding: '8px 24px', borderBottom: '1px solid #252540',
+ },
+ tab: {
+ display: 'flex', alignItems: 'center', gap: 6,
+ padding: '8px 16px', borderRadius: 8, border: 'none',
+ background: 'transparent', color: '#7a7f96', fontWeight: 600, fontSize: 12,
+ cursor: 'pointer', fontFamily: 'inherit', transition: 'all 0.2s',
+ },
+ tabActive: {
+ background: '#8b5cf622', color: '#8b5cf6',
+ },
+ main: {
+ padding: 24, maxWidth: 1440, margin: '0 auto',
+ },
+};
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
new file mode 100644
index 0000000..a528de5
--- /dev/null
+++ b/frontend/src/api.ts
@@ -0,0 +1,47 @@
+const BASE = 'http://45.61.58.125:4040';
+const AUTH = btoa('admin:REDACTED_PASSWORD');
+const headers: Record<string, string> = {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Basic ${AUTH}`,
+};
+
+async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
+ const res = await fetch(`${BASE}${path}`, { headers, ...options });
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`${res.status}: ${text}`);
+ }
+ return res.json();
+}
+
+export const api = {
+ // Meetings
+ getMeetings: () => apiFetch<any[]>('/api/meetings'),
+ getActiveMeeting: () => apiFetch<any>('/api/meetings/active'),
+ getMeeting: (id: string) => apiFetch<any>(`/api/meetings/${id}`),
+ startMeeting: (type: string) => apiFetch<any>(`/api/meetings/start/${type}`, { method: 'POST' }),
+ haltMeeting: (id: string) => apiFetch<any>(`/api/meetings/${id}/halt`, { method: 'POST' }),
+ advanceMeeting: (id: string) => apiFetch<any>(`/api/meetings/${id}/advance`, { method: 'POST' }),
+ addMessage: (id: string, msg: any) => apiFetch<any>(`/api/meetings/${id}/message`, { method: 'POST', body: JSON.stringify(msg) }),
+ getMessages: (id: string) => apiFetch<any[]>(`/api/meetings/${id}/messages`),
+
+ // Agents
+ getAgents: () => apiFetch<any>('/api/agents'),
+ getTeams: () => apiFetch<any>('/api/agents/teams'),
+ getAgent: (name: string) => apiFetch<any>(`/api/agents/${name}`),
+ speakAgent: (name: string, topic: string, phase?: string) =>
+ apiFetch<any>(`/api/agents/${name}/speak`, { method: 'POST', body: JSON.stringify({ topic, phase }) }),
+
+ // Agenda
+ getAgenda: (meetingId: string) => apiFetch<any[]>(`/api/agenda/${meetingId}`),
+ addAgendaItem: (meetingId: string, item: any) => apiFetch<any>(`/api/agenda/${meetingId}`, { method: 'POST', body: JSON.stringify(item) }),
+ updateAgendaItem: (meetingId: string, itemId: number, updates: any) => apiFetch<any>(`/api/agenda/${meetingId}/${itemId}`, { method: 'PUT', body: JSON.stringify(updates) }),
+ deleteAgendaItem: (meetingId: string, itemId: number) => apiFetch<any>(`/api/agenda/${meetingId}/${itemId}`, { method: 'DELETE' }),
+
+ // Voice
+ getVoiceConfig: () => apiFetch<any>('/api/voice/config'),
+ updateVoiceConfig: (cfg: any) => apiFetch<any>('/api/voice/config', { method: 'POST', body: JSON.stringify(cfg) }),
+
+ // Status
+ getStatus: () => apiFetch<any>('/api/status'),
+};
diff --git a/frontend/src/components/AgendaManager.tsx b/frontend/src/components/AgendaManager.tsx
new file mode 100644
index 0000000..22c9000
--- /dev/null
+++ b/frontend/src/components/AgendaManager.tsx
@@ -0,0 +1,132 @@
+import { useState, useEffect } from 'react';
+import { api } from '../api';
+import type { AgendaItem, Meeting } from '../types';
+
+export default function AgendaManager() {
+ const [meeting, setMeeting] = useState<Meeting | null>(null);
+ const [items, setItems] = useState<AgendaItem[]>([]);
+ const [topic, setTopic] = useState('');
+ const [recommendation, setRecommendation] = useState('');
+ const [decisionType, setDecisionType] = useState('B');
+ const [owner, setOwner] = useState('');
+
+ useEffect(() => {
+ api.getActiveMeeting().then(data => {
+ setMeeting(data.meeting);
+ if (data.meeting) {
+ api.getAgenda(data.meeting.id).then(setItems);
+ }
+ }).catch(() => {});
+ }, []);
+
+ function refresh() {
+ if (meeting) {
+ api.getAgenda(meeting.id).then(setItems);
+ }
+ }
+
+ async function addItem() {
+ if (!meeting || !topic.trim()) return;
+ try {
+ await api.addAgendaItem(meeting.id, { topic, recommendation, decisionType, owner: owner || undefined });
+ setTopic(''); setRecommendation(''); setOwner('');
+ refresh();
+ } catch (err: any) {
+ alert(err.message);
+ }
+ }
+
+ async function deleteItem(id: number) {
+ if (!meeting) return;
+ await api.deleteAgendaItem(meeting.id, id);
+ refresh();
+ }
+
+ if (!meeting) {
+ return (
+ <div style={styles.card}>
+ <div style={styles.empty}>No active meeting. Start a meeting first to manage its agenda.</div>
+ </div>
+ );
+ }
+
+ return (
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
+ {/* Add Item Form */}
+ <div style={styles.card}>
+ <h3 style={styles.title}>Add Agenda Item</h3>
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
+ <input style={styles.input} placeholder="Topic" value={topic} onChange={e => setTopic(e.target.value)} />
+ <input style={styles.input} placeholder="Recommendation" value={recommendation} onChange={e => setRecommendation(e.target.value)} />
+ <select style={styles.select} value={decisionType} onChange={e => setDecisionType(e.target.value)}>
+ <option value="A">Type A (Auto)</option>
+ <option value="B">Type B (Advisory)</option>
+ <option value="C">Type C (Human)</option>
+ </select>
+ <input style={{ ...styles.input, maxWidth: 120 }} placeholder="Owner" value={owner} onChange={e => setOwner(e.target.value)} />
+ <button style={styles.addBtn} onClick={addItem} disabled={items.length >= 5}>
+ {items.length >= 5 ? 'Max 5' : 'Add'}
+ </button>
+ </div>
+ <div style={{ fontSize: 11, color: '#7a7f96', marginTop: 6 }}>
+ {items.length}/5 items | Meeting: {meeting.meeting_type} ({meeting.id.slice(0, 8)})
+ </div>
+ </div>
+
+ {/* Items List */}
+ <div style={styles.card}>
+ <h3 style={styles.title}>Current Agenda</h3>
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
+ {items.length === 0 && <div style={styles.empty}>No agenda items yet.</div>}
+ {items.map((item, idx) => (
+ <div key={item.id} style={styles.item}>
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+ <span style={styles.position}>{idx + 1}</span>
+ <span style={{ fontWeight: 600, fontSize: 13 }}>{item.topic}</span>
+ </div>
+ <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
+ <span style={{
+ ...styles.typeBadge,
+ background: item.decision_type === 'A' ? '#22c55e22' : item.decision_type === 'C' ? '#ef444422' : '#3b82f622',
+ color: item.decision_type === 'A' ? '#22c55e' : item.decision_type === 'C' ? '#ef4444' : '#3b82f6',
+ }}>
+ Type {item.decision_type}
+ </span>
+ <span style={{
+ ...styles.statusBadge,
+ background: item.status === 'discussed' ? '#f59e0b22' : item.status === 'resolved' ? '#22c55e22' : '#7a7f9622',
+ color: item.status === 'discussed' ? '#f59e0b' : item.status === 'resolved' ? '#22c55e' : '#7a7f96',
+ }}>
+ {item.status}
+ </span>
+ <button style={styles.deleteBtn} onClick={() => deleteItem(item.id)}>x</button>
+ </div>
+ </div>
+ {item.recommendation && (
+ <div style={{ fontSize: 11, color: '#7a7f96', marginTop: 4 }}>Rec: {item.recommendation}</div>
+ )}
+ {item.owner && (
+ <div style={{ fontSize: 11, color: '#7a7f96', marginTop: 2 }}>Owner: {item.owner}</div>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+const styles: Record<string, React.CSSProperties> = {
+ card: { background: '#1a1a2e', borderRadius: 12, padding: 20, border: '1px solid #252540' },
+ title: { margin: '0 0 12px', fontSize: 15, fontWeight: 700 },
+ input: { padding: '8px 10px', borderRadius: 6, border: '1px solid #252540', background: '#0a0a12', color: '#e8eaf0', fontSize: 12, fontFamily: 'inherit', flex: 1, minWidth: 140 },
+ select: { padding: '8px', borderRadius: 6, border: '1px solid #252540', background: '#0a0a12', color: '#e8eaf0', fontSize: 12, fontFamily: 'inherit' },
+ addBtn: { padding: '8px 16px', borderRadius: 6, border: 'none', background: '#8b5cf6', color: '#fff', fontWeight: 600, fontSize: 12, cursor: 'pointer', fontFamily: 'inherit' },
+ empty: { color: '#7a7f96', fontSize: 13, textAlign: 'center' as const, padding: 30 },
+ item: { background: '#12121f', borderRadius: 8, padding: 12, border: '1px solid #252540' },
+ position: { width: 22, height: 22, borderRadius: '50%', background: '#8b5cf622', color: '#8b5cf6', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 700 },
+ typeBadge: { padding: '2px 8px', borderRadius: 4, fontSize: 10, fontWeight: 600 },
+ statusBadge: { padding: '2px 8px', borderRadius: 4, fontSize: 10, fontWeight: 600 },
+ deleteBtn: { width: 20, height: 20, borderRadius: 4, border: '1px solid #ef444466', background: 'transparent', color: '#ef4444', fontSize: 11, cursor: 'pointer', fontFamily: 'inherit' },
+};
diff --git a/frontend/src/components/AgentRoster.tsx b/frontend/src/components/AgentRoster.tsx
new file mode 100644
index 0000000..094deca
--- /dev/null
+++ b/frontend/src/components/AgentRoster.tsx
@@ -0,0 +1,125 @@
+import { useState } from 'react';
+import { api } from '../api';
+import { useApi } from '../hooks/useApi';
+import type { Agent, Executive } from '../types';
+
+export default function AgentRoster() {
+ const { data, loading } = useApi(() => api.getAgents(), []);
+ const [search, setSearch] = useState('');
+ const [speakingAgent, setSpeakingAgent] = useState<string | null>(null);
+ const [dialogue, setDialogue] = useState<string | null>(null);
+
+ if (loading || !data) return <div style={styles.card}><div style={styles.empty}>Loading agents...</div></div>;
+
+ const agents: Agent[] = data.agents || [];
+ const executives: Executive[] = data.executives || [];
+ const departments = [...new Set(agents.map(a => a.dept))].sort();
+
+ const filtered = search
+ ? agents.filter(a => a.name.toLowerCase().includes(search.toLowerCase()) || a.role.toLowerCase().includes(search.toLowerCase()) || a.id.toLowerCase().includes(search.toLowerCase()))
+ : agents;
+
+ async function speak(agentId: string) {
+ setSpeakingAgent(agentId);
+ setDialogue(null);
+ try {
+ const res = await api.speakAgent(agentId, 'Share your current status and priorities');
+ setDialogue(res.dialogue);
+ } catch {
+ setDialogue('Unable to generate dialogue.');
+ }
+ }
+
+ return (
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
+ {/* Search + Stats */}
+ <div style={styles.card}>
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
+ <h3 style={{ margin: 0, fontSize: 16, fontWeight: 700 }}>Agent Directory</h3>
+ <span style={{ fontSize: 12, color: '#7a7f96' }}>{agents.length} agents + {executives.length} executives</span>
+ </div>
+ <input
+ style={styles.search}
+ placeholder="Search agents by name, role, or ID..."
+ value={search}
+ onChange={e => setSearch(e.target.value)}
+ />
+ </div>
+
+ {/* Dialogue preview */}
+ {dialogue && (
+ <div style={{ ...styles.card, borderColor: '#8b5cf666' }}>
+ <div style={{ fontSize: 12, fontWeight: 700, color: '#8b5cf6', marginBottom: 6 }}>
+ {speakingAgent} says:
+ </div>
+ <div style={{ fontSize: 13, lineHeight: 1.5 }}>{dialogue}</div>
+ </div>
+ )}
+
+ {/* Executives */}
+ <div style={styles.card}>
+ <h4 style={{ margin: '0 0 12px', fontSize: 13, fontWeight: 700, color: '#8b5cf6' }}>Executive Team</h4>
+ <div style={styles.grid}>
+ {executives.map(exec => (
+ <div key={exec.id} style={{ ...styles.agentCard, borderColor: exec.color + '44' }}>
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+ <div style={{ ...styles.dot, background: exec.color }} />
+ <div>
+ <div style={{ fontWeight: 700, fontSize: 13 }}>{exec.name}</div>
+ <div style={{ fontSize: 10, color: '#7a7f96' }}>{exec.role}</div>
+ </div>
+ </div>
+ <div style={{ fontSize: 10, color: '#7a7f96', marginTop: 6 }}>
+ ID: {exec.id} | Port: {exec.port}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* Agents by Department */}
+ {departments.map(dept => {
+ const deptAgents = filtered.filter(a => a.dept === dept);
+ if (deptAgents.length === 0) return null;
+ return (
+ <div key={dept} style={styles.card}>
+ <h4 style={{ margin: '0 0 10px', fontSize: 13, fontWeight: 700, color: '#f59e0b' }}>{dept}</h4>
+ <div style={styles.grid}>
+ {deptAgents.map(agent => (
+ <div
+ key={agent.id}
+ style={{
+ ...styles.agentCard,
+ borderColor: agent.color + '44',
+ cursor: 'pointer',
+ }}
+ onClick={() => speak(agent.id)}
+ >
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+ <div style={{ ...styles.dot, background: agent.color }} />
+ <div>
+ <div style={{ fontWeight: 600, fontSize: 12 }}>{agent.name}</div>
+ <div style={{ fontSize: 10, color: '#7a7f96' }}>{agent.role}</div>
+ </div>
+ </div>
+ <div style={{ fontSize: 9, color: '#555', marginTop: 4 }}>
+ {agent.id} {agent.port ? `| :${agent.port}` : ''} {agent.hasApi ? '| API' : ''}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+
+const styles: Record<string, React.CSSProperties> = {
+ card: { background: '#1a1a2e', borderRadius: 12, padding: 20, border: '1px solid #252540' },
+ empty: { color: '#7a7f96', fontSize: 13, textAlign: 'center' as const, padding: 40 },
+ search: { width: '100%', padding: '10px 12px', borderRadius: 8, border: '1px solid #252540', background: '#0a0a12', color: '#e8eaf0', fontSize: 13, fontFamily: 'inherit' },
+ grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 8 },
+ agentCard: { background: '#12121f', borderRadius: 8, padding: 10, border: '1px solid #252540', transition: 'all 0.2s' },
+ dot: { width: 10, height: 10, borderRadius: '50%', flexShrink: 0 },
+};
diff --git a/frontend/src/components/LiveFeed.tsx b/frontend/src/components/LiveFeed.tsx
new file mode 100644
index 0000000..8b93010
--- /dev/null
+++ b/frontend/src/components/LiveFeed.tsx
@@ -0,0 +1,77 @@
+import { useEffect, useRef } from 'react';
+import type { MeetingMessage } from '../types';
+
+const TYPE_COLORS: Record<string, string> = {
+ dialogue: '#e8eaf0',
+ phase: '#8b5cf6',
+ system: '#7a7f96',
+ action: '#f59e0b',
+ decision: '#22c55e',
+};
+
+interface Props {
+ messages: MeetingMessage[];
+ meetingActive: boolean;
+}
+
+export default function LiveFeed({ messages, meetingActive }: Props) {
+ const feedRef = useRef<HTMLDivElement>(null);
+
+ useEffect(() => {
+ if (feedRef.current) {
+ feedRef.current.scrollTop = feedRef.current.scrollHeight;
+ }
+ }, [messages]);
+
+ return (
+ <div style={styles.card}>
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
+ <h3 style={{ margin: 0, fontSize: 14, fontWeight: 700 }}>Live Feed</h3>
+ <span style={{ fontSize: 11, color: '#7a7f96' }}>
+ {messages.length} message{messages.length !== 1 ? 's' : ''}
+ </span>
+ </div>
+
+ <div ref={feedRef} style={styles.feed}>
+ {messages.length === 0 && (
+ <div style={styles.empty}>
+ {meetingActive ? 'Waiting for dialogue...' : 'No active meeting. Start one above.'}
+ </div>
+ )}
+ {messages.map((msg) => (
+ <div key={msg.id} style={styles.message}>
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 2 }}>
+ <span style={{ fontWeight: 700, fontSize: 12, color: TYPE_COLORS[msg.message_type] || '#e8eaf0' }}>
+ {msg.agent_name}
+ </span>
+ {msg.message_type !== 'dialogue' && (
+ <span style={{
+ padding: '1px 6px', borderRadius: 3, fontSize: 9, fontWeight: 600,
+ background: (TYPE_COLORS[msg.message_type] || '#7a7f96') + '22',
+ color: TYPE_COLORS[msg.message_type] || '#7a7f96',
+ }}>
+ {msg.message_type}
+ </span>
+ )}
+ <span style={{ fontSize: 9, color: '#555', marginLeft: 'auto' }}>
+ {msg.phase}
+ </span>
+ </div>
+ <div style={{ fontSize: 12, color: '#b0b3c0', lineHeight: 1.5 }}>{msg.message}</div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+}
+
+const styles: Record<string, React.CSSProperties> = {
+ card: { background: '#1a1a2e', borderRadius: 12, padding: 16, border: '1px solid #252540' },
+ feed: {
+ maxHeight: 500, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 8,
+ },
+ message: {
+ padding: '8px 12px', background: '#12121f', borderRadius: 8, borderLeft: '3px solid #252540',
+ },
+ empty: { color: '#7a7f96', fontSize: 13, textAlign: 'center' as const, padding: 40 },
+};
diff --git a/frontend/src/components/MeetingControls.tsx b/frontend/src/components/MeetingControls.tsx
new file mode 100644
index 0000000..1a04a0a
--- /dev/null
+++ b/frontend/src/components/MeetingControls.tsx
@@ -0,0 +1,74 @@
+import type { Meeting } from '../types';
+
+const MEETING_TYPES = [
+ { type: 'strategic', label: 'Strategic', color: '#8b5cf6', icon: '\uD83C\uDFAF' },
+ { type: 'initiative', label: 'Initiative', color: '#3b82f6', icon: '\uD83D\uDE80' },
+ { type: 'ops', label: 'Operations', color: '#22c55e', icon: '\u2699\uFE0F' },
+ { type: 'emergency', label: 'Emergency', color: '#ef4444', icon: '\uD83D\uDEA8' },
+];
+
+interface Props {
+ activeMeeting: Meeting | null;
+ onStart: (type: string) => void;
+ onHalt: () => void;
+ onAdvance: () => void;
+}
+
+export default function MeetingControls({ activeMeeting, onStart, onHalt, onAdvance }: Props) {
+ const isActive = activeMeeting && activeMeeting.status === 'in_progress';
+
+ return (
+ <div style={styles.card}>
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+ <h3 style={{ margin: 0, fontSize: 14, fontWeight: 700 }}>Meeting Controls</h3>
+ {isActive && (
+ <div style={{ display: 'flex', gap: 6 }}>
+ <button style={styles.advanceBtn} onClick={onAdvance}>Advance Phase</button>
+ <button style={styles.haltBtn} onClick={onHalt}>Halt</button>
+ </div>
+ )}
+ </div>
+
+ {!isActive && (
+ <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
+ {MEETING_TYPES.map(mt => (
+ <button
+ key={mt.type}
+ style={{ ...styles.startBtn, borderColor: mt.color + '66', color: mt.color }}
+ onClick={() => onStart(mt.type)}
+ >
+ <span>{mt.icon}</span>
+ <span>{mt.label}</span>
+ </button>
+ ))}
+ </div>
+ )}
+
+ {isActive && (
+ <div style={{ marginTop: 8, fontSize: 11, color: '#7a7f96' }}>
+ {activeMeeting.meeting_type} meeting in progress — Phase: {activeMeeting.phase.replace(/_/g, ' ')}
+ </div>
+ )}
+ </div>
+ );
+}
+
+const styles: Record<string, React.CSSProperties> = {
+ card: { background: '#1a1a2e', borderRadius: 12, padding: 16, border: '1px solid #252540' },
+ startBtn: {
+ flex: 1, padding: '10px 12px', borderRadius: 8, border: '1px solid',
+ background: 'transparent', fontWeight: 600, fontSize: 12,
+ cursor: 'pointer', fontFamily: 'inherit', display: 'flex', alignItems: 'center',
+ justifyContent: 'center', gap: 6, transition: 'all 0.2s',
+ },
+ advanceBtn: {
+ padding: '6px 14px', borderRadius: 6, border: 'none',
+ background: '#8b5cf6', color: '#fff', fontWeight: 600, fontSize: 11,
+ cursor: 'pointer', fontFamily: 'inherit',
+ },
+ haltBtn: {
+ padding: '6px 14px', borderRadius: 6, border: 'none',
+ background: '#ef4444', color: '#fff', fontWeight: 600, fontSize: 11,
+ cursor: 'pointer', fontFamily: 'inherit',
+ },
+};
diff --git a/frontend/src/components/MeetingRoom.tsx b/frontend/src/components/MeetingRoom.tsx
new file mode 100644
index 0000000..f5fea96
--- /dev/null
+++ b/frontend/src/components/MeetingRoom.tsx
@@ -0,0 +1,86 @@
+import { useState, useEffect, useRef } from 'react';
+import { api } from '../api';
+import type { Meeting, MeetingMessage, WSEvent } from '../types';
+import PhaseBar from './PhaseBar';
+import MeetingControls from './MeetingControls';
+import LiveFeed from './LiveFeed';
+
+interface Props {
+ events: WSEvent[];
+}
+
+export default function MeetingRoom({ events }: Props) {
+ const [meeting, setMeeting] = useState<Meeting | null>(null);
+ const [messages, setMessages] = useState<MeetingMessage[]>([]);
+ const [loading, setLoading] = useState(true);
+
+ function fetchActive() {
+ api.getActiveMeeting().then(data => {
+ setMeeting(data.meeting);
+ setMessages(data.messages || []);
+ setLoading(false);
+ }).catch(() => setLoading(false));
+ }
+
+ useEffect(() => { fetchActive(); }, []);
+
+ // Listen for WS events
+ useEffect(() => {
+ const latest = events[events.length - 1];
+ if (!latest) return;
+
+ if (latest.type === 'meeting_started' || latest.type === 'meeting_completed' || latest.type === 'meeting_halted') {
+ fetchActive();
+ } else if (latest.type === 'meeting_phase' && meeting) {
+ setMeeting(prev => prev ? { ...prev, phase: latest.payload.phase } : null);
+ } else if (latest.type === 'meeting_message') {
+ setMessages(prev => [...prev, latest.payload]);
+ }
+ }, [events]);
+
+ async function handleStart(type: string) {
+ try {
+ await api.startMeeting(type);
+ fetchActive();
+ } catch (err: any) {
+ alert(err.message);
+ }
+ }
+
+ async function handleHalt() {
+ if (!meeting) return;
+ await api.haltMeeting(meeting.id);
+ fetchActive();
+ }
+
+ async function handleAdvance() {
+ if (!meeting) return;
+ await api.advanceMeeting(meeting.id);
+ fetchActive();
+ }
+
+ if (loading) return <div style={styles.card}><div style={styles.empty}>Loading...</div></div>;
+
+ return (
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
+ {/* Phase Bar */}
+ <PhaseBar currentPhase={meeting?.phase || null} status={meeting?.status || null} />
+
+ {/* Controls */}
+ <MeetingControls
+ activeMeeting={meeting}
+ onStart={handleStart}
+ onHalt={handleHalt}
+ onAdvance={handleAdvance}
+ />
+
+ {/* Live Feed */}
+ <LiveFeed messages={messages} meetingActive={!!meeting && meeting.status === 'in_progress'} />
+ </div>
+ );
+}
+
+const styles: Record<string, React.CSSProperties> = {
+ card: { background: '#1a1a2e', borderRadius: 12, padding: 20, border: '1px solid #252540' },
+ empty: { color: '#7a7f96', fontSize: 13, textAlign: 'center' as const, padding: 40 },
+};
diff --git a/frontend/src/components/PhaseBar.tsx b/frontend/src/components/PhaseBar.tsx
new file mode 100644
index 0000000..a933b51
--- /dev/null
+++ b/frontend/src/components/PhaseBar.tsx
@@ -0,0 +1,68 @@
+const PHASES = [
+ { id: 'caucus', label: 'Caucus' },
+ { id: 'call_to_order', label: 'Call to Order' },
+ { id: 'old_business', label: 'Old Business' },
+ { id: 'current_business', label: 'Current Business' },
+ { id: 'new_business', label: 'New Business' },
+ { id: 'breakout', label: 'Breakout' },
+ { id: 'minutes', label: 'Minutes' },
+ { id: 'adjournment', label: 'Adjournment' },
+];
+
+interface Props {
+ currentPhase: string | null;
+ status: string | null;
+}
+
+export default function PhaseBar({ currentPhase, status }: Props) {
+ const activeIdx = PHASES.findIndex(p => p.id === currentPhase);
+
+ return (
+ <div style={styles.bar}>
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
+ <span style={{ fontSize: 12, fontWeight: 700 }}>Meeting Phases</span>
+ {status && (
+ <span style={{
+ ...styles.statusBadge,
+ background: status === 'in_progress' ? '#22c55e22' : status === 'halted' ? '#ef444422' : '#7a7f9622',
+ color: status === 'in_progress' ? '#22c55e' : status === 'halted' ? '#ef4444' : '#7a7f96',
+ }}>
+ {status.replace('_', ' ')}
+ </span>
+ )}
+ </div>
+ <div style={styles.phases}>
+ {PHASES.map((p, i) => {
+ const isActive = i === activeIdx;
+ const isPast = i < activeIdx;
+ return (
+ <div
+ key={p.id}
+ style={{
+ ...styles.phase,
+ background: isActive ? '#8b5cf6' : isPast ? '#8b5cf644' : '#252540',
+ color: isActive ? '#fff' : isPast ? '#b0b3c0' : '#7a7f96',
+ fontWeight: isActive ? 700 : 400,
+ boxShadow: isActive ? '0 0 12px #8b5cf644' : 'none',
+ }}
+ >
+ <span style={{ fontSize: 8, opacity: 0.6 }}>{i + 1}</span>
+ <span style={{ fontSize: 10 }}>{p.label}</span>
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ );
+}
+
+const styles: Record<string, React.CSSProperties> = {
+ bar: { background: '#1a1a2e', borderRadius: 12, padding: 16, border: '1px solid #252540' },
+ phases: { display: 'flex', gap: 4 },
+ phase: {
+ flex: 1, padding: '8px 4px', borderRadius: 6, textAlign: 'center' as const,
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
+ transition: 'all 0.3s',
+ },
+ statusBadge: { padding: '3px 10px', borderRadius: 4, fontSize: 10, fontWeight: 600 },
+};
diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts
new file mode 100644
index 0000000..14b5193
--- /dev/null
+++ b/frontend/src/hooks/useApi.ts
@@ -0,0 +1,20 @@
+import { useState, useEffect, useCallback } from 'react';
+
+export function useApi<T>(fetcher: () => Promise<T>, deps: any[] = []) {
+ const [data, setData] = useState<T | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+
+ const refresh = useCallback(() => {
+ setLoading(true);
+ setError(null);
+ fetcher()
+ .then(setData)
+ .catch(e => setError(e.message))
+ .finally(() => setLoading(false));
+ }, deps);
+
+ useEffect(() => { refresh(); }, [refresh]);
+
+ return { data, loading, error, refresh };
+}
diff --git a/frontend/src/hooks/useWebSocket.ts b/frontend/src/hooks/useWebSocket.ts
new file mode 100644
index 0000000..c293c80
--- /dev/null
+++ b/frontend/src/hooks/useWebSocket.ts
@@ -0,0 +1,54 @@
+import { useState, useEffect, useRef, useCallback } from 'react';
+import type { WSEvent } from '../types';
+
+const WS_URL = `ws://${window.location.host}/ws`;
+const MAX_EVENTS = 200;
+
+export function useWebSocket() {
+ const [events, setEvents] = useState<WSEvent[]>([]);
+ const [connected, setConnected] = useState(false);
+ const wsRef = useRef<WebSocket | null>(null);
+ const reconnectTimer = useRef<number>();
+
+ const connect = useCallback(() => {
+ if (wsRef.current?.readyState === WebSocket.OPEN) return;
+
+ try {
+ const ws = new WebSocket(WS_URL);
+ wsRef.current = ws;
+
+ ws.onopen = () => {
+ setConnected(true);
+ if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
+ };
+
+ ws.onmessage = (evt) => {
+ try {
+ const event: WSEvent = JSON.parse(evt.data);
+ setEvents(prev => [...prev.slice(-MAX_EVENTS), event]);
+ } catch {}
+ };
+
+ ws.onclose = () => {
+ setConnected(false);
+ reconnectTimer.current = window.setTimeout(connect, 3000);
+ };
+
+ ws.onerror = () => {
+ ws.close();
+ };
+ } catch {}
+ }, []);
+
+ useEffect(() => {
+ connect();
+ return () => {
+ if (wsRef.current) wsRef.current.close();
+ if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
+ };
+ }, [connect]);
+
+ const clearEvents = useCallback(() => setEvents([]), []);
+
+ return { events, connected, clearEvents };
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..9707d82
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+ <React.StrictMode>
+ <App />
+ </React.StrictMode>
+);
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
new file mode 100644
index 0000000..39e6766
--- /dev/null
+++ b/frontend/src/types.ts
@@ -0,0 +1,62 @@
+export interface Meeting {
+ id: string;
+ meeting_type: string;
+ phase: string;
+ status: string;
+ started_at: string | null;
+ ended_at: string | null;
+ summary: string | null;
+ attendees: string;
+ action_items: string;
+ created_at: string;
+}
+
+export interface MeetingMessage {
+ id: number;
+ meeting_id: string;
+ agent_id: string;
+ agent_name: string;
+ message: string;
+ message_type: string;
+ phase: string | null;
+ created_at: string;
+}
+
+export interface AgendaItem {
+ id: number;
+ meeting_id: string;
+ topic: string;
+ recommendation: string | null;
+ decision_type: string;
+ owner: string | null;
+ position: number;
+ status: string;
+ created_at: string;
+}
+
+export interface Agent {
+ id: string;
+ name: string;
+ pm2: string;
+ port?: number;
+ color: string;
+ role: string;
+ dept: string;
+ hasApi: boolean;
+ subMgr?: string;
+}
+
+export interface Executive {
+ id: string;
+ name: string;
+ port: number;
+ color: string;
+ role: string;
+ avatar: string;
+}
+
+export interface WSEvent {
+ type: string;
+ payload: any;
+ timestamp: string;
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..75e2ff7
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..0abaae4
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 4050,
+ host: '0.0.0.0',
+ proxy: {
+ '/api': 'http://127.0.0.1:4040',
+ '/ws': { target: 'ws://127.0.0.1:4040', ws: true },
+ },
+ },
+ preview: {
+ port: 4050,
+ host: '0.0.0.0',
+ proxy: {
+ '/api': 'http://127.0.0.1:4040',
+ '/ws': { target: 'ws://127.0.0.1:4040', ws: true },
+ },
+ },
+});
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..c62abfe
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1626 @@
+{
+ "name": "dw-boardroom-v2",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dw-boardroom-v2",
+ "version": "1.0.0",
+ "dependencies": {
+ "better-sqlite3": "^11.7.0",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.1",
+ "node-fetch": "^2.7.0",
+ "uuid": "^11.0.3",
+ "ws": "^8.18.0"
+ },
+ "devDependencies": {
+ "@types/better-sqlite3": "^7.6.12",
+ "@types/cors": "^2.8.17",
+ "@types/express": "^5.0.0",
+ "@types/node": "^22.10.1",
+ "@types/node-fetch": "^2.6.12",
+ "@types/uuid": "^10.0.0",
+ "@types/ws": "^8.5.13",
+ "typescript": "^5.7.2"
+ }
+ },
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.13",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
+ "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.11",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
+ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/better-sqlite3": {
+ "version": "11.10.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
+ "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.87.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
+ "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9638a8e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "dw-boardroom-v2",
+ "version": "1.0.0",
+ "description": "DW Boardroom V2 — Meeting orchestration engine with 47 AI agents",
+ "main": "dist/api/server.js",
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/api/server.js",
+ "dev": "ts-node src/api/server.ts",
+ "migrate": "ts-node src/db/migrate.ts"
+ },
+ "dependencies": {
+ "better-sqlite3": "^11.7.0",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.7",
+ "express": "^4.21.1",
+ "node-fetch": "^2.7.0",
+ "uuid": "^11.0.3",
+ "ws": "^8.18.0"
+ },
+ "devDependencies": {
+ "@types/better-sqlite3": "^7.6.12",
+ "@types/cors": "^2.8.17",
+ "@types/express": "^5.0.0",
+ "@types/node": "^22.10.1",
+ "@types/node-fetch": "^2.6.12",
+ "@types/uuid": "^10.0.0",
+ "@types/ws": "^8.5.13",
+ "typescript": "^5.7.2"
+ }
+}
diff --git a/src/agents/president.ts b/src/agents/president.ts
new file mode 100644
index 0000000..4f95259
--- /dev/null
+++ b/src/agents/president.ts
@@ -0,0 +1,58 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — President / Chair Agent
+// Validates agenda, enforces rules, opens/closes
+// ═══════════════════════════════════════════════
+
+import type { AgendaItem, MeetingType } from '../types';
+
+const MAX_AGENDA_ITEMS = 5;
+
+export function validateAgenda(items: Partial<AgendaItem>[]): { valid: boolean; errors: string[] } {
+ const errors: string[] = [];
+
+ if (items.length > MAX_AGENDA_ITEMS) {
+ errors.push(`Maximum ${MAX_AGENDA_ITEMS} agenda items allowed (got ${items.length})`);
+ }
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (!item.topic || item.topic.trim().length === 0) {
+ errors.push(`Item ${i + 1}: Topic is required`);
+ }
+ if (item.decision_type && !['A', 'B', 'C'].includes(item.decision_type)) {
+ errors.push(`Item ${i + 1}: Invalid decision type (must be A, B, or C)`);
+ }
+ }
+
+ return { valid: errors.length === 0, errors };
+}
+
+export function generateOpeningStatement(meetingType: MeetingType, agendaCount: number): string {
+ const typeLabels: Record<MeetingType, string> = {
+ strategic: 'Strategic Planning Session',
+ initiative: 'Initiative Review',
+ ops: 'Operations Sync',
+ emergency: 'Emergency Session',
+ };
+
+ const label = typeLabels[meetingType] || 'General Meeting';
+ return `This ${label} is now called to order by President Lincoln. We have ${agendaCount} agenda item${agendaCount !== 1 ? 's' : ''} to address. Let us proceed with focus and efficiency.`;
+}
+
+export function generateClosingStatement(meetingType: MeetingType, decisionsCount: number): string {
+ return `This ${meetingType} session stands adjourned by order of President Lincoln. ${decisionsCount} decision${decisionsCount !== 1 ? 's' : ''} were addressed. Action items will be tracked through governance. You are all dismissed.`;
+}
+
+export function getPhaseChairMessage(phase: string): string {
+ const messages: Record<string, string> = {
+ caucus: 'Members, prepare your remarks. President Lincoln will convene us shortly.',
+ call_to_order: 'President Lincoln calls this meeting to order. Roll call proceeding.',
+ old_business: 'President Lincoln directs we review outstanding items from previous sessions.',
+ current_business: 'Moving to current business. Presenters, you have the floor. Address the President directly.',
+ new_business: 'The floor is now open for new business items. Present to the President.',
+ breakout: 'President Lincoln grants departments leave for brief breakout discussions.',
+ minutes: 'The note taker will now summarize our proceedings for the President.',
+ adjournment: 'With all business concluded, President Lincoln adjourns this session.',
+ };
+ return messages[phase] || 'Proceeding to the next phase.';
+}
diff --git a/src/agents/registry.ts b/src/agents/registry.ts
new file mode 100644
index 0000000..cfd880a
--- /dev/null
+++ b/src/agents/registry.ts
@@ -0,0 +1,148 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Agent Registry
+// All 47 agents + 5 executives ported from config.js
+// ═══════════════════════════════════════════════
+
+import type { Agent, Executive, DeptGroup } from '../types';
+
+// President Lincoln — the owner's podium persona
+// Sir Steven stands at the podium as Abraham Lincoln, the 16th President
+export const BOSS_PERSONA = {
+ name: 'President Lincoln',
+ title: 'Mr. President',
+ fullName: 'Abraham Lincoln',
+ shortRef: 'the President',
+ podiumNote: 'Sir Steven stands at the podium as Abraham Lincoln, the 16th President. Address him formally as "Mr. President" or "President Lincoln".',
+};
+
+export const TEAM_AGENTS: Agent[] = [
+ // Operations
+ { id: 'max', name: 'Ford', pm2: 'dw-master-hub', port: 9893, color: '#B91C1C', role: 'Fleet Commander', dept: 'Operations', hasApi: true },
+ { id: 'hawk', name: 'Sven', pm2: 'dw-dashboard-monitor', port: 9990, color: '#39FF14', role: 'System Monitor', dept: 'Operations', hasApi: true },
+ { id: 'clay', name: 'Seth', pm2: 'agent-cronjob', color: '#64748B', role: 'Scheduler', dept: 'Operations', hasApi: false },
+ { id: 'rex', name: 'Pierce', pm2: 'timeout-agent', color: '#2C3539', role: 'Process Guardian', dept: 'Operations', hasApi: false },
+ { id: 'steve', name: 'Inessa', pm2: 'server-agent', port: 9814, color: '#565672', role: 'Infrastructure', dept: 'Operations', hasApi: true },
+ { id: 'vince', name: 'Vince', pm2: 'web-uptime', port: 9813, color: '#008B8B', role: 'Site Monitor', dept: 'Operations', hasApi: true },
+ { id: 'link', name: 'Cora', pm2: 'dw-central', color: '#0077BE', role: 'Central Coordination', dept: 'Operations', hasApi: false },
+ { id: 'abby', name: 'Abby', pm2: 'abby-agenda', port: 9826, color: '#EC4899', role: 'Agenda Manager', dept: 'Operations', hasApi: false },
+ // Commerce
+ { id: 'shane', name: 'Shane', pm2: 'shopify-expert-agent', port: 9887, color: '#14B8A6', role: 'Shopify Expert', dept: 'Commerce', hasApi: true },
+ // Pipeline (Otto's team)
+ { id: 'clyde', name: 'Silas', pm2: 'sku-check-skill', color: '#84CC16', role: 'SKU Auditor', dept: 'Pipeline', hasApi: false, subMgr: 'otto' },
+ { id: 'dean', name: 'Cole', pm2: 'collections-agent', color: '#00A86B', role: 'Collections', dept: 'Pipeline', hasApi: false, subMgr: 'otto' },
+ { id: 'blake', name: 'Piper', pm2: 'product-scheduler', port: 9600, color: '#6366F1', role: 'Product Scheduler', dept: 'Pipeline', hasApi: true, subMgr: 'otto' },
+ { id: 'clark', name: 'Perry', pm2: 'update-agent', color: '#EA580C', role: 'Product Updates', dept: 'Pipeline', hasApi: false, subMgr: 'otto' },
+ // Finance
+ { id: 'grant', name: 'Finn', pm2: 'dw-accounting', port: 9882, color: '#004225', role: 'Finance', dept: 'Finance', hasApi: true },
+ { id: 'nash', name: 'Eli', pm2: 'dw-expenses-tracker', color: '#047857', role: 'Expenses', dept: 'Finance', hasApi: false },
+ { id: 'hugh', name: 'Phil', pm2: 'dw-purchasing-office', port: 9880, color: '#7C3AED', role: 'Procurement', dept: 'Finance', hasApi: true },
+ { id: 'annie', name: 'Penny', pm2: 'annie-arbitrage', port: 7202, color: '#E879F9', role: 'Pricing Arbitrage', dept: 'Finance', hasApi: true },
+ // Legal
+ { id: 'frank', name: 'Lance', pm2: 'dw-legal-compliance', port: 9878, color: '#374151', role: 'Legal Compliance', dept: 'Legal', hasApi: true },
+ // Content & Marketing
+ { id: 'bob', name: 'Craig', pm2: 'bob-the-blogger', color: '#0EA5E9', role: 'Content Creator', dept: 'Marketing', hasApi: false },
+ { id: 'connie', name: 'Connie', pm2: 'connie-competitive', color: '#EF4444', role: 'Competitive Intel', dept: 'Marketing', hasApi: false },
+ { id: 'sage', name: 'Tara', pm2: 'dw-trend-research', port: 9883, color: '#F59E0B', role: 'Trend Forecaster', dept: 'Marketing', hasApi: true },
+ { id: 'faye', name: 'Rita', pm2: 'furniture-scene-agent', color: '#EC4899', role: 'Room Staging', dept: 'Marketing', hasApi: false },
+ // Catalogs — Heritage (Hugo's team)
+ { id: 'ralph', name: 'Ralph', pm2: 'rl-catalog-9608', port: 9608, color: '#1E40AF', role: 'Ralph Lauren', dept: 'Catalogs-Heritage', hasApi: true, subMgr: 'hugo' },
+ { id: 'sasha', name: 'Sasha', pm2: 'schu-crawler', port: 9965, color: '#8B4513', role: 'Schumacher', dept: 'Catalogs-Heritage', hasApi: true, subMgr: 'hugo' },
+ { id: 'knox', name: 'Knox', pm2: 'krav-crawler', port: 9973, color: '#4B0082', role: 'Kravet', dept: 'Catalogs-Heritage', hasApi: true, subMgr: 'hugo' },
+ { id: 'cleo', name: 'Cleo', pm2: 'cole-crawler', port: 9974, color: '#2F4F4F', role: 'Cole & Son', dept: 'Catalogs-Heritage', hasApi: true, subMgr: 'hugo' },
+ { id: 'elise', name: 'Elise', pm2: 'elit-crawler', port: 9975, color: '#C71585', role: 'Elitis', dept: 'Catalogs-Heritage', hasApi: true, subMgr: 'hugo' },
+ { id: 'artie', name: 'Artie', pm2: 'arte-crawler', port: 9961, color: '#FF4500', role: 'Arte International', dept: 'Catalogs-Heritage', hasApi: true, subMgr: 'hugo' },
+ // Catalogs — Modern (Mack's team)
+ { id: 'kira', name: 'Kira', pm2: 'koro-crawler', port: 9962, color: '#20B2AA', role: 'Koroseal', dept: 'Catalogs-Modern', hasApi: true, subMgr: 'mack' },
+ { id: 'maya', name: 'Maya', pm2: 'maya-crawler', port: 9963, color: '#DA70D6', role: 'Maya Romanoff', dept: 'Catalogs-Modern', hasApi: true, subMgr: 'mack' },
+ { id: 'pj', name: 'PJ', pm2: 'pj-crawler', port: 9968, color: '#DAA520', role: 'Phillip Jeffries', dept: 'Catalogs-Modern', hasApi: true, subMgr: 'mack' },
+ { id: 'ines', name: 'Ines', pm2: 'inno-crawler', port: 9970, color: '#3CB371', role: 'Innovations', dept: 'Catalogs-Modern', hasApi: true, subMgr: 'mack' },
+ { id: 'beau', name: 'Beau', pm2: 'bspk-crawler', port: 9969, color: '#FF8C00', role: 'DW Bespoke', dept: 'Catalogs-Modern', hasApi: true, subMgr: 'mack' },
+ { id: 'tate', name: 'Tate', pm2: 'thibaut-agent', color: '#8FBC8F', role: 'Thibaut Catalog', dept: 'Catalogs-Modern', hasApi: false, subMgr: 'mack' },
+ // Catalogs — National (Ned's team)
+ { id: 'brock', name: 'Brock', pm2: 'brewster-york-catalog', color: '#1E3A5F', role: 'Brewster/York', dept: 'Catalogs-National', hasApi: false, subMgr: 'ned' },
+ { id: 'york', name: 'York', pm2: 'york-contract-catalog', color: '#4682B4', role: 'York Contract', dept: 'Catalogs-National', hasApi: false, subMgr: 'ned' },
+ { id: 'graham', name: 'Graham', pm2: 'graham-brown-catalog', color: '#DC2626', role: 'Graham & Brown', dept: 'Catalogs-National', hasApi: false, subMgr: 'ned' },
+ { id: 'jade', name: 'Grace', pm2: 'gb-agent', color: '#C9A96E', role: 'Glass Bead WP', dept: 'Catalogs-National', hasApi: false, subMgr: 'ned' },
+ { id: 'will', name: 'Will', pm2: 'william-watch-agent', color: '#7B2D8E', role: 'Watch Catalog', dept: 'Catalogs-National', hasApi: false, subMgr: 'ned' },
+ { id: 'scout', name: 'Cyrus', pm2: 'vendor-catalog-scraper', port: 9611, color: '#FF6B35', role: 'Catalog Scraper Engine', dept: 'Catalogs-National', hasApi: true, subMgr: 'ned' },
+ // Catalog Leadership
+ { id: 'atlas', name: 'Porter', pm2: 'product-agent', port: 9604, color: '#7B68EE', role: 'Product Master Viewer', dept: 'Catalogs', hasApi: true },
+ // Data & Communication
+ { id: 'chet', name: 'Chet', pm2: 'slack-dm-viewer', port: 7204, color: '#4A154B', role: 'Communications', dept: 'Data', hasApi: true },
+ { id: 'george', name: 'George', pm2: 'george-gmail', port: 9850, color: '#EA4335', role: 'Gmail Agent', dept: 'Data', hasApi: true },
+ { id: 'ken', name: 'Trent', pm2: 'ken', color: '#FF69B4', role: 'Trading Intelligence', dept: 'Data', hasApi: false },
+ { id: 'nate', name: 'Nate', pm2: 'commit-agent', color: '#059669', role: 'Note Taker & Records', dept: 'Data', hasApi: false, isNoteTaker: true },
+ { id: 'ross', name: 'Arlo', pm2: 'dw-agent-directory', port: 9891, color: '#8F8F8F', role: 'Agent Directory', dept: 'Data', hasApi: true },
+ // Imagery
+ { id: 'drew', name: 'Drew', pm2: 'dw-digital-samples', port: 9879, color: '#06B6D4', role: 'Digital Samples', dept: 'Imagery', hasApi: true },
+ { id: 'chase', name: 'Miles', pm2: 'no-image-agent', color: '#F87171', role: 'Missing Images', dept: 'Imagery', hasApi: false },
+ // Social Media
+ { id: 'iris', name: 'Iris', pm2: 'instagram-agent', port: 9870, color: '#E1306C', role: 'Instagram Agent', dept: 'Marketing', hasApi: true },
+];
+
+export const EXECUTIVES: Executive[] = [
+ { id: 'ceo', name: 'Curtis', port: 7120, color: '#5B21B6', role: 'Chief Executive', avatar: 'curtis' },
+ { id: 'cfo', name: 'Cliff', port: 7121, color: '#065F46', role: 'Chief Financial Officer', avatar: 'cliff' },
+ { id: 'coo', name: 'Conrad', port: 7122, color: '#CD7F32', role: 'Chief Operations Officer', avatar: 'conrad' },
+ { id: 'cto', name: 'Troy', port: 7124, color: '#9B111E', role: 'Chief Technology Officer', avatar: 'troy' },
+ { id: 'vp-ops', name: 'Vaughn', port: 7125, color: '#6D28D9', role: 'VP of Operations', avatar: 'vaughn' },
+];
+
+export const DEPARTMENT_GROUPS: Record<string, DeptGroup> = {
+ 'Operations': { emoji: '\u2699\uFE0F', color: '#B91C1C', leader: 'max', exec: 'coo', label: 'Ops Command' },
+ 'Commerce': { emoji: '\uD83D\uDED2', color: '#14B8A6', leader: 'shane', exec: 'cfo', label: 'Commerce' },
+ 'Finance': { emoji: '\uD83D\uDCB0', color: '#004225', leader: 'grant', exec: 'cfo', label: 'Finance' },
+ 'Legal': { emoji: '\u2696\uFE0F', color: '#374151', leader: 'frank', exec: 'ceo', label: 'Legal' },
+ 'Marketing': { emoji: '\uD83D\uDCE3', color: '#F59E0B', leader: 'sage', exec: 'vpops', label: 'Content & Staging' },
+ 'Catalogs': { emoji: '\uD83D\uDCE6', color: '#1E3A5F', leader: 'atlas', exec: 'vpops', label: 'Catalog Leadership' },
+ 'Catalogs-Heritage': { emoji: '\uD83C\uDFDB\uFE0F', color: '#8B4513', leader: 'ralph', exec: 'vpops', label: 'Heritage Vendors' },
+ 'Catalogs-Modern': { emoji: '\uD83D\uDE80', color: '#20B2AA', leader: 'kira', exec: 'vpops', label: 'Modern Vendors' },
+ 'Catalogs-National': { emoji: '\uD83C\uDDFA\uD83C\uDDF8', color: '#4682B4', leader: 'brock', exec: 'vpops', label: 'National Vendors' },
+ 'Pipeline': { emoji: '\u26A1', color: '#6366F1', leader: 'blake', exec: 'vpops', label: 'Ops Pipeline' },
+ 'Data': { emoji: '\uD83D\uDCCA', color: '#059669', leader: 'nate', exec: 'cto', label: 'Data & Intel' },
+ 'Imagery': { emoji: '\uD83D\uDDBC\uFE0F', color: '#06B6D4', leader: 'drew', exec: 'vpops', label: 'Imagery' },
+};
+
+// Cross-department questioners — agents who ask questions about other dept reports
+export const CROSS_DEPT_QUESTIONERS: Record<string, string[]> = {
+ 'Operations': ['shane', 'grant', 'frank'],
+ 'Commerce': ['hawk', 'grant', 'drew', 'frank'],
+ 'Finance': ['shane', 'hugh', 'max'],
+ 'Legal': ['shane', 'bob', 'max'],
+ 'Marketing': ['shane', 'dean', 'sage'],
+ 'Catalogs': ['shane', 'drew', 'dean', 'clark'],
+ 'Data': ['max', 'hawk', 'shane'],
+ 'Imagery': ['shane', 'dean', 'faye'],
+};
+
+// Helper functions
+export function getAgent(id: string): Agent | undefined {
+ return TEAM_AGENTS.find(a => a.id === id);
+}
+
+export function getExec(id: string): Executive | undefined {
+ return EXECUTIVES.find(e => e.id === id);
+}
+
+export function getAgentsByDept(dept: string): Agent[] {
+ return TEAM_AGENTS.filter(a => a.dept === dept);
+}
+
+export function getAgentsByExec(execId: string): Agent[] {
+ const depts = Object.entries(DEPARTMENT_GROUPS)
+ .filter(([, g]) => g.exec === execId)
+ .map(([name]) => name);
+ return TEAM_AGENTS.filter(a => depts.includes(a.dept));
+}
+
+export function getTeamsByExec(): Record<string, { exec: Executive; agents: Agent[] }> {
+ const result: Record<string, { exec: Executive; agents: Agent[] }> = {};
+ for (const exec of EXECUTIVES) {
+ result[exec.id] = { exec, agents: getAgentsByExec(exec.id) };
+ }
+ return result;
+}
+
+export function getNoteTaker(): Agent | undefined {
+ return TEAM_AGENTS.find(a => a.isNoteTaker);
+}
diff --git a/src/agents/voices.ts b/src/agents/voices.ts
new file mode 100644
index 0000000..6f66e37
--- /dev/null
+++ b/src/agents/voices.ts
@@ -0,0 +1,79 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Neural TTS Voice Map
+// Microsoft Edge Neural Voices via edge-tts
+// ═══════════════════════════════════════════════
+
+export const NEURAL_VOICES: Record<string, string> = {
+ // Executives — authoritative, distinct
+ 'ceo': 'en-US-AndrewNeural',
+ 'cfo': 'en-GB-RyanNeural',
+ 'coo': 'en-US-ChristopherNeural',
+ 'cto': 'en-GB-ThomasNeural',
+ 'vp-ops': 'en-AU-WilliamMultilingualNeural',
+ // Operations
+ 'max': 'en-US-SteffanNeural',
+ 'hawk': 'en-US-RogerNeural',
+ 'clay': 'en-US-BrianNeural',
+ 'rex': 'en-IE-ConnorNeural',
+ 'steve': 'en-US-JennyNeural',
+ 'vince': 'en-US-EricNeural',
+ 'link': 'en-US-BrianMultilingualNeural',
+ 'abby': 'en-US-JennyNeural',
+ // Commerce
+ 'shane': 'en-US-AndrewMultilingualNeural',
+ 'clyde': 'en-US-ChristopherNeural',
+ 'dean': 'en-GB-ThomasNeural',
+ 'blake': 'en-US-RogerNeural',
+ // Finance
+ 'grant': 'en-US-SteffanNeural',
+ 'nash': 'en-IE-ConnorNeural',
+ 'hugh': 'en-GB-RyanNeural',
+ 'annie': 'en-US-EmmaNeural',
+ // Legal
+ 'frank': 'en-US-EricNeural',
+ // Marketing
+ 'bob': 'en-US-GuyNeural',
+ 'connie': 'en-US-AvaNeural',
+ 'sage': 'en-US-AriaNeural',
+ 'faye': 'en-US-MichelleNeural',
+ // Catalogs
+ 'brock': 'en-US-BrianNeural',
+ 'graham': 'en-GB-RyanNeural',
+ 'york': 'en-US-AndrewNeural',
+ 'ralph': 'en-US-ChristopherNeural',
+ 'jade': 'en-GB-LibbyNeural',
+ 'tate': 'en-AU-WilliamMultilingualNeural',
+ 'sasha': 'en-US-AriaNeural',
+ 'knox': 'en-US-SteffanNeural',
+ 'cleo': 'en-GB-LibbyNeural',
+ 'elise': 'en-US-MichelleNeural',
+ 'artie': 'en-US-GuyNeural',
+ 'kira': 'en-US-EmmaNeural',
+ 'maya': 'en-US-AvaNeural',
+ 'pj': 'en-US-RogerNeural',
+ 'ines': 'en-US-JennyNeural',
+ 'beau': 'en-US-BrianNeural',
+ 'scout': 'en-US-EricNeural',
+ 'atlas': 'en-US-AndrewNeural',
+ 'will': 'en-GB-ThomasNeural',
+ // Data & Comms
+ 'nate': 'en-US-BrianNeural',
+ 'ross': 'en-US-AndrewMultilingualNeural',
+ 'chet': 'en-IE-ConnorNeural',
+ 'ken': 'en-US-RogerNeural',
+ 'george': 'en-US-SteffanNeural',
+ 'iris': 'en-US-AvaNeural',
+ // Imagery
+ 'drew': 'en-US-GuyNeural',
+ 'chase': 'en-US-SteffanNeural',
+ 'clark': 'en-US-EricNeural',
+ // System defaults
+ 'user': 'en-US-AndrewNeural',
+ 'boardroom': 'en-US-AvaMultilingualNeural',
+ 'system': 'en-US-AvaMultilingualNeural',
+ 'default': 'en-US-AriaNeural',
+};
+
+export function getVoice(agentId: string): string {
+ return NEURAL_VOICES[agentId] || NEURAL_VOICES['default'];
+}
diff --git a/src/api/middleware/auth.ts b/src/api/middleware/auth.ts
new file mode 100644
index 0000000..c171b72
--- /dev/null
+++ b/src/api/middleware/auth.ts
@@ -0,0 +1,25 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Basic Auth Middleware
+// ═══════════════════════════════════════════════
+
+import type { Request, Response, NextFunction } from 'express';
+
+const AUTH_USER = process.env.AUTH_USER || 'admin';
+const AUTH_PASS = process.env.AUTH_PASS || 'REDACTED_PASSWORD';
+
+export function basicAuth(req: Request, res: Response, next: NextFunction): void {
+ const authHeader = req.headers.authorization;
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
+ res.status(401).json({ error: 'Authentication required' });
+ return;
+ }
+
+ const decoded = Buffer.from(authHeader.slice(6), 'base64').toString();
+ const [user, pass] = decoded.split(':');
+
+ if (user === AUTH_USER && pass === AUTH_PASS) {
+ next();
+ } else {
+ res.status(401).json({ error: 'Invalid credentials' });
+ }
+}
diff --git a/src/api/routes/agenda.ts b/src/api/routes/agenda.ts
new file mode 100644
index 0000000..9e50af7
--- /dev/null
+++ b/src/api/routes/agenda.ts
@@ -0,0 +1,65 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Agenda Routes
+// ═══════════════════════════════════════════════
+
+import { Router } from 'express';
+import * as engine from '../../engine/meetingEngine';
+import { validateAgenda } from '../../agents/president';
+
+const router = Router();
+
+// GET /api/agenda/:meetingId — get items
+router.get('/:meetingId', (req, res) => {
+ try {
+ const items = engine.getAgendaItems(req.params.meetingId);
+ res.json(items);
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/agenda/:meetingId — add item
+router.post('/:meetingId', (req, res) => {
+ try {
+ const { topic, recommendation, decisionType, owner } = req.body;
+ if (!topic) {
+ res.status(400).json({ error: 'Topic is required' });
+ return;
+ }
+
+ const validation = validateAgenda([{ topic, decision_type: decisionType }]);
+ if (!validation.valid) {
+ res.status(400).json({ error: validation.errors.join('; ') });
+ return;
+ }
+
+ const item = engine.addAgendaItem(req.params.meetingId, topic, recommendation, decisionType, owner);
+ res.json(item);
+ } catch (err: any) {
+ res.status(400).json({ error: err.message });
+ }
+});
+
+// PUT /api/agenda/:meetingId/:itemId — update item
+router.put('/:meetingId/:itemId', (req, res) => {
+ try {
+ const itemId = parseInt(req.params.itemId);
+ const item = engine.updateAgendaItem(req.params.meetingId, itemId, req.body);
+ res.json(item);
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// DELETE /api/agenda/:meetingId/:itemId — delete item
+router.delete('/:meetingId/:itemId', (req, res) => {
+ try {
+ const itemId = parseInt(req.params.itemId);
+ engine.deleteAgendaItem(req.params.meetingId, itemId);
+ res.json({ deleted: true });
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+export default router;
diff --git a/src/api/routes/agents.ts b/src/api/routes/agents.ts
new file mode 100644
index 0000000..30d3171
--- /dev/null
+++ b/src/api/routes/agents.ts
@@ -0,0 +1,67 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Agent Routes
+// ═══════════════════════════════════════════════
+
+import { Router } from 'express';
+import { TEAM_AGENTS, EXECUTIVES, getAgent, getExec, getAgentsByDept, getTeamsByExec, DEPARTMENT_GROUPS } from '../../agents/registry';
+import { generateDialogue } from '../../engine/agentDialogue';
+
+const router = Router();
+
+// GET /api/agents — full roster
+router.get('/', (req, res) => {
+ const dept = req.query.dept as string | undefined;
+ let agents = dept ? getAgentsByDept(dept) : TEAM_AGENTS;
+ res.json({
+ agents,
+ executives: EXECUTIVES,
+ total: agents.length,
+ departments: Object.keys(DEPARTMENT_GROUPS),
+ });
+});
+
+// GET /api/agents/teams — grouped by exec
+router.get('/teams', (req, res) => {
+ const teams = getTeamsByExec();
+ res.json(teams);
+});
+
+// GET /api/agents/:name — agent detail
+router.get('/:name', (req, res) => {
+ const agent = getAgent(req.params.name) || getExec(req.params.name);
+ if (!agent) {
+ res.status(404).json({ error: 'Agent not found' });
+ return;
+ }
+ res.json(agent);
+});
+
+// POST /api/agents/:name/speak — generate dialogue
+router.post('/:name/speak', async (req, res) => {
+ try {
+ const agent = getAgent(req.params.name);
+ const exec = getExec(req.params.name);
+ const entity = agent || exec;
+
+ if (!entity) {
+ res.status(404).json({ error: 'Agent not found' });
+ return;
+ }
+
+ const { topic, phase, meetingType } = req.body;
+ const dialogue = await generateDialogue(
+ entity.id || req.params.name,
+ entity.name,
+ entity.role,
+ phase || 'current_business',
+ topic || 'General discussion',
+ meetingType || 'strategic'
+ );
+
+ res.json({ agent: entity.name, dialogue });
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+export default router;
diff --git a/src/api/routes/health.ts b/src/api/routes/health.ts
new file mode 100644
index 0000000..62b1def
--- /dev/null
+++ b/src/api/routes/health.ts
@@ -0,0 +1,45 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Health & Status Routes
+// ═══════════════════════════════════════════════
+
+import { Router } from 'express';
+import { getActiveMeeting, listMeetings } from '../../engine/meetingEngine';
+import { TEAM_AGENTS, EXECUTIVES } from '../../agents/registry';
+import { isGovernanceAvailable } from '../../engine/governanceClient';
+import { getConnectionCount } from '../../ws/broadcast';
+
+const router = Router();
+const startTime = Date.now();
+
+// GET /api/status — detailed status
+router.get('/status', async (req, res) => {
+ try {
+ const govAvailable = await isGovernanceAvailable();
+ const activeMeeting = getActiveMeeting();
+ const recentMeetings = listMeetings(5);
+
+ res.json({
+ service: 'dw-boardroom-v2',
+ version: '1.0.0',
+ uptime: Math.floor((Date.now() - startTime) / 1000),
+ agents: TEAM_AGENTS.length,
+ executives: EXECUTIVES.length,
+ wsClients: getConnectionCount(),
+ activeMeeting: activeMeeting ? {
+ id: activeMeeting.id,
+ type: activeMeeting.meeting_type,
+ phase: activeMeeting.phase,
+ status: activeMeeting.status,
+ } : null,
+ recentMeetings: recentMeetings.length,
+ governance: {
+ url: process.env.GOVERNANCE_URL || 'http://127.0.0.1:4020',
+ available: govAvailable,
+ },
+ });
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+export default router;
diff --git a/src/api/routes/meetings.ts b/src/api/routes/meetings.ts
new file mode 100644
index 0000000..9adc9dd
--- /dev/null
+++ b/src/api/routes/meetings.ts
@@ -0,0 +1,121 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Meeting Routes
+// ═══════════════════════════════════════════════
+
+import { Router } from 'express';
+import * as engine from '../../engine/meetingEngine';
+import type { MeetingType } from '../../types';
+
+const router = Router();
+
+// GET /api/meetings — list all meetings
+router.get('/', (req, res) => {
+ try {
+ const limit = parseInt(req.query.limit as string) || 50;
+ const meetings = engine.listMeetings(limit);
+ res.json(meetings);
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/meetings/active — current meeting
+router.get('/active', (req, res) => {
+ try {
+ const meeting = engine.getActiveMeeting();
+ if (!meeting) {
+ res.json({ active: false, meeting: null });
+ return;
+ }
+ const messages = engine.getMeetingMessages(meeting.id, 100);
+ const agenda = engine.getAgendaItems(meeting.id);
+ res.json({ active: true, meeting, messages, agenda });
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/meetings/:id — meeting detail
+router.get('/:id', (req, res) => {
+ try {
+ const meeting = engine.getMeeting(req.params.id);
+ if (!meeting) {
+ res.status(404).json({ error: 'Meeting not found' });
+ return;
+ }
+ const messages = engine.getMeetingMessages(meeting.id);
+ const agenda = engine.getAgendaItems(meeting.id);
+ res.json({ meeting, messages, agenda });
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/meetings/start/:type — start meeting
+router.post('/start/:type', async (req, res) => {
+ try {
+ const type = req.params.type as MeetingType;
+ if (!['strategic', 'initiative', 'ops', 'emergency'].includes(type)) {
+ res.status(400).json({ error: 'Invalid meeting type. Use: strategic, initiative, ops, emergency' });
+ return;
+ }
+ const meeting = await engine.startMeeting(type);
+ res.json(meeting);
+ } catch (err: any) {
+ res.status(409).json({ error: err.message });
+ }
+});
+
+// POST /api/meetings/:id/halt — halt meeting
+router.post('/:id/halt', (req, res) => {
+ try {
+ const meeting = engine.haltMeeting(req.params.id);
+ res.json(meeting);
+ } catch (err: any) {
+ res.status(400).json({ error: err.message });
+ }
+});
+
+// POST /api/meetings/:id/advance — advance phase
+router.post('/:id/advance', async (req, res) => {
+ try {
+ const meeting = await engine.advancePhase(req.params.id);
+ res.json(meeting);
+ } catch (err: any) {
+ res.status(400).json({ error: err.message });
+ }
+});
+
+// POST /api/meetings/:id/message — add message
+router.post('/:id/message', (req, res) => {
+ try {
+ const { agentId, agentName, message, messageType } = req.body;
+ if (!message) {
+ res.status(400).json({ error: 'Message required' });
+ return;
+ }
+ const msg = engine.addMessage(
+ req.params.id,
+ agentId || 'user',
+ agentName || 'User',
+ message,
+ messageType || 'dialogue'
+ );
+ res.json(msg);
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/meetings/:id/messages — get messages
+router.get('/:id/messages', (req, res) => {
+ try {
+ const limit = parseInt(req.query.limit as string) || 500;
+ const messages = engine.getMeetingMessages(req.params.id, limit);
+ res.json(messages);
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+export default router;
diff --git a/src/api/routes/voice.ts b/src/api/routes/voice.ts
new file mode 100644
index 0000000..7f14d00
--- /dev/null
+++ b/src/api/routes/voice.ts
@@ -0,0 +1,51 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Voice / TTS Routes
+// ═══════════════════════════════════════════════
+
+import { Router } from 'express';
+import { queryOne, run, query } from '../../db/sqlite';
+import { getVoice } from '../../agents/voices';
+
+const router = Router();
+
+// GET /api/voice/config — TTS settings
+router.get('/config', (req, res) => {
+ try {
+ const rows = query<{ key: string; value: string }>('SELECT key, value FROM br2_voice_config');
+ const config: Record<string, any> = {};
+ for (const row of rows) {
+ if (row.value === 'true') config[row.key] = true;
+ else if (row.value === 'false') config[row.key] = false;
+ else if (!isNaN(Number(row.value))) config[row.key] = Number(row.value);
+ else config[row.key] = row.value;
+ }
+ res.json(config);
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/voice/config — update TTS settings
+router.post('/config', (req, res) => {
+ try {
+ const updates = req.body;
+ for (const [key, value] of Object.entries(updates)) {
+ run(
+ `INSERT INTO br2_voice_config (key, value, updated_at) VALUES (?, ?, datetime('now'))
+ ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')`,
+ [key, String(value), String(value)]
+ );
+ }
+ res.json({ updated: true });
+ } catch (err: any) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/voice/agent/:id — get voice for agent
+router.get('/agent/:id', (req, res) => {
+ const voice = getVoice(req.params.id);
+ res.json({ agentId: req.params.id, voice });
+});
+
+export default router;
diff --git a/src/api/server.ts b/src/api/server.ts
new file mode 100644
index 0000000..8f0b0e9
--- /dev/null
+++ b/src/api/server.ts
@@ -0,0 +1,79 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Express Server
+// Meeting orchestration with 47 AI agents
+// Port 4040 — API + WebSocket
+// ═══════════════════════════════════════════════
+
+import dotenv from 'dotenv';
+dotenv.config();
+
+import express from 'express';
+import cors from 'cors';
+import http from 'http';
+import { basicAuth } from './middleware/auth';
+import { initWebSocket } from '../ws/broadcast';
+import { runMigration } from '../db/migrate';
+
+// Route imports
+import meetingRoutes from './routes/meetings';
+import agentRoutes from './routes/agents';
+import agendaRoutes from './routes/agenda';
+import voiceRoutes from './routes/voice';
+import healthRoutes from './routes/health';
+
+const PORT = parseInt(process.env.PORT || '4040');
+const FRONTEND_URL = process.env.FRONTEND_URL || 'http://45.61.58.125:4050';
+
+// ─── Initialize ────────────────────────────────
+
+// Run SQLite migration
+runMigration();
+
+const app = express();
+const server = http.createServer(app);
+
+// Middleware
+app.use(cors({
+ origin: [FRONTEND_URL, 'http://127.0.0.1:4050', 'http://localhost:4050', 'http://45.61.58.125:4060'],
+ credentials: true,
+}));
+app.use(express.json({ limit: '1mb' }));
+
+// Health check — no auth
+app.get('/health', (req, res) => {
+ res.json({ status: 'ok', service: 'dw-boardroom-v2', timestamp: new Date().toISOString() });
+});
+
+// Protected routes
+app.use('/api/meetings', basicAuth, meetingRoutes);
+app.use('/api/agents', basicAuth, agentRoutes);
+app.use('/api/agenda', basicAuth, agendaRoutes);
+app.use('/api/voice', basicAuth, voiceRoutes);
+app.use('/api', basicAuth, healthRoutes);
+
+// ─── WebSocket ─────────────────────────────────
+
+initWebSocket(server);
+
+// ─── Start ─────────────────────────────────────
+
+server.listen(PORT, '0.0.0.0', () => {
+ console.log(`\n═══════════════════════════════════════════════`);
+ console.log(` DW Boardroom V2`);
+ console.log(` API: http://45.61.58.125:${PORT}`);
+ console.log(` WebSocket: ws://45.61.58.125:${PORT}/ws`);
+ console.log(` Frontend: ${FRONTEND_URL}`);
+ console.log(` Governance: ${process.env.GOVERNANCE_URL || 'http://127.0.0.1:4020'}`);
+ console.log(`═══════════════════════════════════════════════\n`);
+});
+
+// Graceful shutdown
+process.on('SIGTERM', () => {
+ console.log('[server] SIGTERM received, shutting down...');
+ server.close(() => process.exit(0));
+});
+
+process.on('SIGINT', () => {
+ console.log('[server] SIGINT received, shutting down...');
+ server.close(() => process.exit(0));
+});
diff --git a/src/db/migrate.ts b/src/db/migrate.ts
new file mode 100644
index 0000000..c55376b
--- /dev/null
+++ b/src/db/migrate.ts
@@ -0,0 +1,37 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — SQLite Migration Runner
+// ═══════════════════════════════════════════════
+
+import fs from 'fs';
+import path from 'path';
+import { exec, getDb } from './sqlite';
+
+export function runMigration(): void {
+ const schemaPath = path.join(__dirname, '..', '..', 'src', 'db', 'schema.sql');
+
+ // When running from dist/, adjust path
+ const resolvedPath = fs.existsSync(schemaPath)
+ ? schemaPath
+ : path.join(__dirname, '..', '..', '..', 'src', 'db', 'schema.sql');
+
+ if (!fs.existsSync(resolvedPath)) {
+ throw new Error(`Schema file not found at ${schemaPath} or ${resolvedPath}`);
+ }
+
+ const schema = fs.readFileSync(resolvedPath, 'utf-8');
+ exec(schema);
+
+ // Verify tables exist
+ const tables = getDb()
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'br2_%'")
+ .all() as { name: string }[];
+
+ console.log(`[migrate] Created/verified ${tables.length} tables:`, tables.map(t => t.name).join(', '));
+}
+
+// Run directly if called as script
+if (require.main === module) {
+ runMigration();
+ console.log('[migrate] Done.');
+ process.exit(0);
+}
diff --git a/src/db/schema.sql b/src/db/schema.sql
new file mode 100644
index 0000000..f6ff691
--- /dev/null
+++ b/src/db/schema.sql
@@ -0,0 +1,58 @@
+-- ═══════════════════════════════════════════════
+-- DW Boardroom V2 — SQLite Schema
+-- Ephemeral meeting data (persistent governance data via API on port 4020)
+-- ═══════════════════════════════════════════════
+
+CREATE TABLE IF NOT EXISTS br2_meetings (
+ id TEXT PRIMARY KEY,
+ meeting_type TEXT NOT NULL CHECK(meeting_type IN ('strategic','initiative','ops','emergency')),
+ phase TEXT NOT NULL DEFAULT 'caucus',
+ status TEXT NOT NULL DEFAULT 'scheduled' CHECK(status IN ('scheduled','in_progress','completed','halted','crashed')),
+ started_at TEXT,
+ ended_at TEXT,
+ summary TEXT,
+ attendees TEXT DEFAULT '[]', -- JSON array of agent IDs
+ action_items TEXT DEFAULT '[]', -- JSON array of action item objects
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS br2_messages (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ meeting_id TEXT NOT NULL REFERENCES br2_meetings(id),
+ agent_id TEXT NOT NULL,
+ agent_name TEXT NOT NULL,
+ message TEXT NOT NULL,
+ message_type TEXT NOT NULL DEFAULT 'dialogue' CHECK(message_type IN ('dialogue','phase','system','action','decision')),
+ phase TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS br2_agenda (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ meeting_id TEXT NOT NULL REFERENCES br2_meetings(id),
+ topic TEXT NOT NULL,
+ recommendation TEXT,
+ decision_type TEXT DEFAULT 'B' CHECK(decision_type IN ('A','B','C')),
+ owner TEXT,
+ position INTEGER NOT NULL DEFAULT 0,
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','discussed','resolved','deferred')),
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS br2_voice_config (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL,
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+-- Indexes
+CREATE INDEX IF NOT EXISTS idx_br2_messages_meeting ON br2_messages(meeting_id);
+CREATE INDEX IF NOT EXISTS idx_br2_messages_agent ON br2_messages(agent_id);
+CREATE INDEX IF NOT EXISTS idx_br2_agenda_meeting ON br2_agenda(meeting_id);
+CREATE INDEX IF NOT EXISTS idx_br2_meetings_status ON br2_meetings(status);
+
+-- Default voice config
+INSERT OR IGNORE INTO br2_voice_config (key, value) VALUES ('tts_enabled', 'false');
+INSERT OR IGNORE INTO br2_voice_config (key, value) VALUES ('tts_speed', '1.0');
+INSERT OR IGNORE INTO br2_voice_config (key, value) VALUES ('tts_volume', '0.8');
+INSERT OR IGNORE INTO br2_voice_config (key, value) VALUES ('auto_speak', 'false');
diff --git a/src/db/sqlite.ts b/src/db/sqlite.ts
new file mode 100644
index 0000000..ed2af86
--- /dev/null
+++ b/src/db/sqlite.ts
@@ -0,0 +1,45 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — SQLite Database Wrapper
+// Ephemeral meeting data stored locally
+// ═══════════════════════════════════════════════
+
+import Database from 'better-sqlite3';
+import path from 'path';
+
+const DB_PATH = path.join(__dirname, '..', '..', 'boardroom.db');
+
+let db: Database.Database;
+
+export function getDb(): Database.Database {
+ if (!db) {
+ db = new Database(DB_PATH);
+ db.pragma('journal_mode = WAL');
+ db.pragma('foreign_keys = ON');
+ }
+ return db;
+}
+
+export function query<T = any>(sql: string, params: any[] = []): T[] {
+ const stmt = getDb().prepare(sql);
+ return stmt.all(...params) as T[];
+}
+
+export function queryOne<T = any>(sql: string, params: any[] = []): T | undefined {
+ const stmt = getDb().prepare(sql);
+ return stmt.get(...params) as T | undefined;
+}
+
+export function run(sql: string, params: any[] = []): Database.RunResult {
+ const stmt = getDb().prepare(sql);
+ return stmt.run(...params);
+}
+
+export function exec(sql: string): void {
+ getDb().exec(sql);
+}
+
+export function closeDb(): void {
+ if (db) {
+ db.close();
+ }
+}
diff --git a/src/engine/agentDialogue.ts b/src/engine/agentDialogue.ts
new file mode 100644
index 0000000..d366285
--- /dev/null
+++ b/src/engine/agentDialogue.ts
@@ -0,0 +1,153 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Gemini Agent Dialogue
+// Generates in-character speech for 47 agents
+// Anti-hallucination guards from existing boardroom
+// ═══════════════════════════════════════════════
+
+import fetch from 'node-fetch';
+
+const GEMINI_KEY = process.env.GEMINI_KEY || '';
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
+
+// Anti-hallucination patterns — ported from existing boardroom
+const HALLUCINATION_PATTERNS = [
+ /(?:I (?:just |recently )?(?:completed|finished|resolved|fixed|addressed|updated|processed|handled|executed|fulfilled)\b)/i,
+ /(?:I (?:have |'ve )?(?:sent|dispatched|delivered|forwarded|transmitted|posted)\b)/i,
+ /(?:(?:all|every) (?:systems?|services?|agents?|processes?) (?:are |is )?(?:operational|running|online|healthy|stable|green)\b)/i,
+ /(?:I (?:am )?(?:currently |actively )?(?:monitoring|tracking|watching|observing|analyzing)\b)/i,
+ /(?:(?:revenue|sales|profit|income|earnings) (?:is |are )?(?:up|increased|growing|improved)\b)/i,
+ /(?:I (?:have |'ve )?(?:detected|discovered|found|identified|noticed|observed)\b)/i,
+ /(?:(?:task|issue|problem|bug|ticket) (?:has been |was )?(?:resolved|fixed|closed|completed|addressed)\b)/i,
+ /(?:I (?:can |will |shall )?(?:confirm|verify|validate|certify|attest)\b)/i,
+ /(?:(?:email|message|notification|alert) (?:has been |was )?(?:sent|delivered|dispatched)\b)/i,
+ /(?:(?:database|server|system|service) (?:is )?(?:fully )?(?:optimized|upgraded|migrated|updated)\b)/i,
+ /(?:(?:security|compliance|audit) (?:check|scan|review) (?:passed|cleared|completed)\b)/i,
+ /(?:I (?:have |'ve )?(?:negotiated|secured|obtained|arranged|organized)\b)/i,
+ /(?:(?:inventory|stock|supply) (?:has been |is )?(?:replenished|restocked|updated|synchronized)\b)/i,
+ /(?:(?:customer|client|user) (?:satisfaction|feedback|rating) (?:is )?(?:excellent|positive|improving)\b)/i,
+ /(?:I (?:have |'ve )?(?:already|previously) (?:submitted|filed|reported|logged)\b)/i,
+];
+
+// Reality anchor — prevents Gemini from fabricating actions
+const REALITY_ANCHOR = `CRITICAL RULES:
+1. You are an AI agent in a boardroom simulation. You do NOT have access to real systems.
+2. NEVER claim to have completed actions, sent emails, fixed bugs, or monitored systems.
+3. NEVER fabricate data, metrics, numbers, or statistics.
+4. Instead: propose actions, recommend strategies, identify concerns, ask questions.
+5. Use phrases like "I recommend...", "We should consider...", "My concern is...", "I propose..."
+6. Keep response to EXACTLY 1 SHORT sentence. Blunt and direct. No fluff.
+7. Stay in character for your role but never claim to have done real work.
+8. PERSONA: Sir Steven stands at the podium as Abraham Lincoln, the 16th President. Address him as "Mr. President" or "President Lincoln".`;
+
+export async function generateDialogue(
+ agentId: string,
+ agentName: string,
+ role: string,
+ phase: string,
+ context: string,
+ meetingType: string
+): Promise<string> {
+ // Fallback if no API key
+ if (!GEMINI_KEY) {
+ return getFallbackDialogue(agentName, role, phase, context);
+ }
+
+ const prompt = `${REALITY_ANCHOR}
+
+You are ${agentName}, the ${role} at Designer Wallcoverings.
+Meeting type: ${meetingType}
+Current phase: ${phase}
+Context: ${context}
+
+Respond in EXACTLY 1 short blunt sentence. Direct. No filler words.`;
+
+ try {
+ const res = await fetch(GEMINI_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ contents: [{ parts: [{ text: prompt }] }],
+ generationConfig: {
+ maxOutputTokens: 60,
+ temperature: 0.7,
+ },
+ }),
+ });
+
+ if (!res.ok) {
+ console.error(`[agentDialogue] Gemini ${res.status}`);
+ return getFallbackDialogue(agentName, role, phase, context);
+ }
+
+ const data = await res.json() as any;
+ let text = data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
+
+ // Anti-hallucination filter
+ text = filterHallucinations(text, agentName, role);
+
+ return text || getFallbackDialogue(agentName, role, phase, context);
+ } catch (err: any) {
+ console.error(`[agentDialogue] Error:`, err.message);
+ return getFallbackDialogue(agentName, role, phase, context);
+ }
+}
+
+function filterHallucinations(text: string, agentName: string, role: string): string {
+ let filtered = text;
+
+ for (const pattern of HALLUCINATION_PATTERNS) {
+ if (pattern.test(filtered)) {
+ // Replace claims of action with recommendations
+ filtered = filtered
+ .replace(/I (?:just |recently )?(?:completed|finished|resolved|fixed)/gi, 'I recommend we address')
+ .replace(/I (?:have |'ve )?(?:sent|dispatched|delivered|forwarded)/gi, 'I suggest we send')
+ .replace(/(?:all|every) (?:systems?|services?) (?:are |is )?(?:operational|running|online)/gi, 'systems should be checked to confirm they are operational')
+ .replace(/I (?:am )?(?:currently |actively )?monitoring/gi, 'I recommend monitoring')
+ .replace(/(?:revenue|sales) (?:is |are )?(?:up|increased|growing)/gi, 'revenue trends should be reviewed')
+ .replace(/I (?:have |'ve )?(?:detected|discovered|found)/gi, 'We should investigate potential')
+ .replace(/(?:task|issue) (?:has been |was )?(?:resolved|fixed|closed)/gi, 'the task needs verification to confirm it is resolved');
+ }
+ }
+
+ return filtered.trim();
+}
+
+function getFallbackDialogue(agentName: string, role: string, phase: string, context: string): string {
+ const fallbacks: Record<string, string[]> = {
+ caucus: [
+ `${agentName} here, ready for the session.`,
+ `${agentName} present. Let's make this productive.`,
+ ],
+ call_to_order: [
+ `${agentName}, ${role}, standing by.`,
+ `Present and accounted for. ${agentName} ready to contribute.`,
+ ],
+ old_business: [
+ `From the ${role} perspective, I recommend we review outstanding items before proceeding.`,
+ `I suggest we verify completion of prior action items related to ${role.toLowerCase()}.`,
+ ],
+ current_business: [
+ `Regarding ${context.substring(0, 50)}... I recommend we evaluate this carefully.`,
+ `From my vantage as ${role}, this deserves thorough consideration.`,
+ ],
+ new_business: [
+ `I'd like to raise a point about ${role.toLowerCase()} priorities going forward.`,
+ `For new business, I propose we consider our ${role.toLowerCase()} strategy.`,
+ ],
+ breakout: [
+ `My team has several items to discuss offline. Will report back.`,
+ `We'll coordinate on the ${role.toLowerCase()} items and bring a summary.`,
+ ],
+ minutes: [
+ `Key takeaways noted for the record.`,
+ `I'll ensure all action items are properly documented.`,
+ ],
+ adjournment: [
+ `Good session. Looking forward to executing on these decisions.`,
+ `Thank you all. ${agentName} signing off.`,
+ ],
+ };
+
+ const options = fallbacks[phase] || [`${agentName} acknowledges.`];
+ return options[Math.floor(Math.random() * options.length)];
+}
diff --git a/src/engine/governanceClient.ts b/src/engine/governanceClient.ts
new file mode 100644
index 0000000..ef2f8a7
--- /dev/null
+++ b/src/engine/governanceClient.ts
@@ -0,0 +1,128 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Governance API Client
+// All persistent governance data flows through port 4020
+// ═══════════════════════════════════════════════
+
+import fetch from 'node-fetch';
+import type { GovDecision, GovDelegation, GovEscalation, GovStatus, GovDecisionStats, DecisionType } from '../types';
+
+const GOVERNANCE_URL = process.env.GOVERNANCE_URL || 'http://127.0.0.1:4020';
+const AUTH = Buffer.from(`${process.env.AUTH_USER || 'admin'}:${process.env.AUTH_PASS || 'REDACTED_PASSWORD'}`).toString('base64');
+
+const headers = {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Basic ${AUTH}`,
+};
+
+async function govFetch<T>(path: string, options: any = {}): Promise<T> {
+ const url = `${GOVERNANCE_URL}${path}`;
+ try {
+ const res = await fetch(url, { headers, ...options });
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Governance API ${res.status}: ${text}`);
+ }
+ return await res.json() as T;
+ } catch (err: any) {
+ if (err.code === 'ECONNREFUSED') {
+ console.error(`[govClient] Governance API unavailable at ${GOVERNANCE_URL}`);
+ throw new Error('Governance API unavailable');
+ }
+ throw err;
+ }
+}
+
+// ─── Decisions ─────────────────────────────────
+
+export async function createDecision(
+ topic: string,
+ decisionType: DecisionType,
+ recommendation: string,
+ owner: string,
+ source: string = 'boardroom-v2'
+): Promise<GovDecision> {
+ return govFetch<GovDecision>('/api/decisions', {
+ method: 'POST',
+ body: JSON.stringify({ topic, decisionType, recommendation, owner, source }),
+ });
+}
+
+export async function approveDecision(id: string): Promise<GovDecision> {
+ return govFetch<GovDecision>(`/api/decisions/${id}/approve`, { method: 'POST' });
+}
+
+export async function rejectDecision(id: string, reason?: string): Promise<GovDecision> {
+ return govFetch<GovDecision>(`/api/decisions/${id}/reject`, {
+ method: 'POST',
+ body: JSON.stringify({ reason }),
+ });
+}
+
+export async function deferDecision(id: string): Promise<GovDecision> {
+ return govFetch<GovDecision>(`/api/decisions/${id}/defer`, { method: 'POST' });
+}
+
+export async function getDecisions(status?: string): Promise<GovDecision[]> {
+ const qs = status ? `?status=${status}` : '';
+ return govFetch<GovDecision[]>(`/api/decisions${qs}`);
+}
+
+export async function getDecisionStats(): Promise<GovDecisionStats> {
+ return govFetch<GovDecisionStats>('/api/decisions/stats');
+}
+
+// ─── Delegations ───────────────────────────────
+
+export async function createDelegation(
+ owner: string,
+ delegate: string,
+ autonomyLevel: number,
+ metric?: string
+): Promise<GovDelegation> {
+ return govFetch<GovDelegation>('/api/delegations', {
+ method: 'POST',
+ body: JSON.stringify({ owner, delegate, autonomyLevel, metric }),
+ });
+}
+
+export async function suggestDelegate(query: string): Promise<any> {
+ return govFetch<any>('/api/delegations/suggest', {
+ method: 'POST',
+ body: JSON.stringify({ query }),
+ });
+}
+
+export async function getDelegations(): Promise<GovDelegation[]> {
+ return govFetch<GovDelegation[]>('/api/delegations');
+}
+
+// ─── Escalations ───────────────────────────────
+
+export async function triggerEscalationCheck(): Promise<any> {
+ return govFetch<any>('/api/escalations/check', { method: 'POST' });
+}
+
+export async function getActiveEscalations(): Promise<GovEscalation[]> {
+ return govFetch<GovEscalation[]>('/api/escalations/active');
+}
+
+// ─── Governance Config ─────────────────────────
+
+export async function getGovernanceStatus(): Promise<GovStatus> {
+ return govFetch<GovStatus>('/api/governance/status');
+}
+
+export async function getGovernanceConfig(): Promise<Record<string, any>> {
+ return govFetch<Record<string, any>>('/api/governance/config');
+}
+
+// ─── Health Check ──────────────────────────────
+
+export async function isGovernanceAvailable(): Promise<boolean> {
+ try {
+ const res = await fetch(`${GOVERNANCE_URL}/health`);
+ return res.ok;
+ } catch {
+ return false;
+ }
+}
diff --git a/src/engine/meetingEngine.ts b/src/engine/meetingEngine.ts
new file mode 100644
index 0000000..410cfe8
--- /dev/null
+++ b/src/engine/meetingEngine.ts
@@ -0,0 +1,376 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Meeting Engine
+// 8-phase orchestrator with mutex and auto-advance
+// Ported from boardroom meeting-engine.js
+// ═══════════════════════════════════════════════
+
+import { v4 as uuid } from 'uuid';
+import * as db from '../db/sqlite';
+import { broadcast } from '../ws/broadcast';
+import { generateDialogue } from './agentDialogue';
+import { TEAM_AGENTS, EXECUTIVES, BOSS_PERSONA, getAgentsByDept, DEPARTMENT_GROUPS } from '../agents/registry';
+import {
+ MEETING_PHASES, PHASE_DURATIONS, PHASE_LABELS,
+ type MeetingPhase, type MeetingType, type Meeting, type MeetingMessage, type AgendaItem
+} from '../types';
+
+let activeMeeting: string | null = null;
+let phaseTimer: NodeJS.Timeout | null = null;
+let meetingMutex = false;
+
+// ─── Public API ────────────────────────────────
+
+export function getActiveMeeting(): Meeting | undefined {
+ if (!activeMeeting) return undefined;
+ return db.queryOne<Meeting>('SELECT * FROM br2_meetings WHERE id = ?', [activeMeeting]);
+}
+
+export function getMeeting(id: string): Meeting | undefined {
+ return db.queryOne<Meeting>('SELECT * FROM br2_meetings WHERE id = ?', [id]);
+}
+
+export function listMeetings(limit = 50): Meeting[] {
+ return db.query<Meeting>('SELECT * FROM br2_meetings ORDER BY created_at DESC LIMIT ?', [limit]);
+}
+
+export function getMeetingMessages(meetingId: string, limit = 500): MeetingMessage[] {
+ return db.query<MeetingMessage>(
+ 'SELECT * FROM br2_messages WHERE meeting_id = ? ORDER BY created_at ASC LIMIT ?',
+ [meetingId, limit]
+ );
+}
+
+export function getAgendaItems(meetingId: string): AgendaItem[] {
+ return db.query<AgendaItem>(
+ 'SELECT * FROM br2_agenda WHERE meeting_id = ? ORDER BY position ASC',
+ [meetingId]
+ );
+}
+
+export async function startMeeting(type: MeetingType): Promise<Meeting> {
+ if (meetingMutex) {
+ throw new Error('Meeting engine is busy');
+ }
+ if (activeMeeting) {
+ throw new Error(`Meeting ${activeMeeting} is already in progress`);
+ }
+
+ meetingMutex = true;
+ try {
+ const id = uuid();
+ const attendees = JSON.stringify([
+ ...EXECUTIVES.map(e => e.id),
+ ...TEAM_AGENTS.map(a => a.id),
+ ]);
+
+ db.run(
+ `INSERT INTO br2_meetings (id, meeting_type, phase, status, started_at, attendees)
+ VALUES (?, ?, 'caucus', 'in_progress', datetime('now'), ?)`,
+ [id, type, attendees]
+ );
+
+ activeMeeting = id;
+ const meeting = getMeeting(id)!;
+
+ // President Lincoln takes the podium
+ addMessage(id, 'lincoln', BOSS_PERSONA.name, `${type.charAt(0).toUpperCase() + type.slice(1)} meeting initiated. ${BOSS_PERSONA.name} takes the podium.`, 'system');
+
+ broadcast({ type: 'meeting_started', payload: meeting, timestamp: new Date().toISOString() });
+
+ // Start phase orchestration
+ await runPhase(id, 'caucus');
+
+ return meeting;
+ } finally {
+ meetingMutex = false;
+ }
+}
+
+export function haltMeeting(meetingId: string): Meeting {
+ const meeting = getMeeting(meetingId);
+ if (!meeting || meeting.status !== 'in_progress') {
+ throw new Error('No active meeting to halt');
+ }
+
+ clearPhaseTimer();
+ db.run(
+ `UPDATE br2_meetings SET status = 'halted', ended_at = datetime('now') WHERE id = ?`,
+ [meetingId]
+ );
+ activeMeeting = null;
+
+ addMessage(meetingId, 'system', 'Boardroom', 'Meeting halted by directive.', 'system');
+ const updated = getMeeting(meetingId)!;
+ broadcast({ type: 'meeting_halted', payload: updated, timestamp: new Date().toISOString() });
+ return updated;
+}
+
+export async function advancePhase(meetingId: string): Promise<Meeting> {
+ const meeting = getMeeting(meetingId);
+ if (!meeting || meeting.status !== 'in_progress') {
+ throw new Error('No active meeting to advance');
+ }
+
+ const currentIdx = MEETING_PHASES.indexOf(meeting.phase as MeetingPhase);
+ if (currentIdx >= MEETING_PHASES.length - 1) {
+ return completeMeeting(meetingId);
+ }
+
+ clearPhaseTimer();
+ const nextPhase = MEETING_PHASES[currentIdx + 1];
+ await runPhase(meetingId, nextPhase);
+ return getMeeting(meetingId)!;
+}
+
+export function addMessage(
+ meetingId: string,
+ agentId: string,
+ agentName: string,
+ message: string,
+ messageType: string = 'dialogue'
+): MeetingMessage {
+ const meeting = getMeeting(meetingId);
+ db.run(
+ `INSERT INTO br2_messages (meeting_id, agent_id, agent_name, message, message_type, phase)
+ VALUES (?, ?, ?, ?, ?, ?)`,
+ [meetingId, agentId, agentName, message, messageType, meeting?.phase || 'unknown']
+ );
+
+ const msg = db.queryOne<MeetingMessage>(
+ 'SELECT * FROM br2_messages WHERE meeting_id = ? ORDER BY id DESC LIMIT 1',
+ [meetingId]
+ )!;
+
+ broadcast({ type: 'meeting_message', payload: msg, timestamp: new Date().toISOString() });
+ return msg;
+}
+
+// ─── Agenda CRUD ───────────────────────────────
+
+export function addAgendaItem(
+ meetingId: string,
+ topic: string,
+ recommendation?: string,
+ decisionType?: string,
+ owner?: string
+): AgendaItem {
+ const items = getAgendaItems(meetingId);
+ if (items.length >= 5) {
+ throw new Error('Maximum 5 agenda items per meeting');
+ }
+
+ db.run(
+ `INSERT INTO br2_agenda (meeting_id, topic, recommendation, decision_type, owner, position)
+ VALUES (?, ?, ?, ?, ?, ?)`,
+ [meetingId, topic, recommendation || null, decisionType || 'B', owner || null, items.length]
+ );
+
+ const item = db.queryOne<AgendaItem>(
+ 'SELECT * FROM br2_agenda WHERE meeting_id = ? ORDER BY id DESC LIMIT 1',
+ [meetingId]
+ )!;
+
+ broadcast({ type: 'agenda_updated', payload: { meetingId, items: getAgendaItems(meetingId) }, timestamp: new Date().toISOString() });
+ return item;
+}
+
+export function updateAgendaItem(meetingId: string, itemId: number, updates: Partial<AgendaItem>): AgendaItem {
+ const sets: string[] = [];
+ const params: any[] = [];
+
+ if (updates.topic !== undefined) { sets.push('topic = ?'); params.push(updates.topic); }
+ if (updates.recommendation !== undefined) { sets.push('recommendation = ?'); params.push(updates.recommendation); }
+ if (updates.decision_type !== undefined) { sets.push('decision_type = ?'); params.push(updates.decision_type); }
+ if (updates.owner !== undefined) { sets.push('owner = ?'); params.push(updates.owner); }
+ if (updates.position !== undefined) { sets.push('position = ?'); params.push(updates.position); }
+ if (updates.status !== undefined) { sets.push('status = ?'); params.push(updates.status); }
+
+ if (sets.length > 0) {
+ params.push(itemId, meetingId);
+ db.run(`UPDATE br2_agenda SET ${sets.join(', ')} WHERE id = ? AND meeting_id = ?`, params);
+ }
+
+ return db.queryOne<AgendaItem>('SELECT * FROM br2_agenda WHERE id = ?', [itemId])!;
+}
+
+export function deleteAgendaItem(meetingId: string, itemId: number): void {
+ db.run('DELETE FROM br2_agenda WHERE id = ? AND meeting_id = ?', [itemId, meetingId]);
+}
+
+// ─── Phase Orchestration ───────────────────────
+
+async function runPhase(meetingId: string, phase: MeetingPhase): Promise<void> {
+ db.run('UPDATE br2_meetings SET phase = ? WHERE id = ?', [phase, meetingId]);
+
+ const label = PHASE_LABELS[phase];
+ addMessage(meetingId, 'system', 'Boardroom', `--- Phase: ${label} ---`, 'phase');
+ broadcast({
+ type: 'meeting_phase',
+ payload: { meetingId, phase, label },
+ timestamp: new Date().toISOString(),
+ });
+
+ // Run phase-specific logic
+ try {
+ await executePhaseLogic(meetingId, phase);
+ } catch (err: any) {
+ console.error(`[meetingEngine] Phase ${phase} error:`, err.message);
+ addMessage(meetingId, 'system', 'Boardroom', `Phase error: ${err.message}`, 'system');
+ }
+
+ // Schedule auto-advance
+ const duration = PHASE_DURATIONS[phase] * 1000;
+ phaseTimer = setTimeout(async () => {
+ try {
+ const meeting = getMeeting(meetingId);
+ if (meeting && meeting.status === 'in_progress') {
+ const idx = MEETING_PHASES.indexOf(phase);
+ if (idx < MEETING_PHASES.length - 1) {
+ await runPhase(meetingId, MEETING_PHASES[idx + 1]);
+ } else {
+ completeMeeting(meetingId);
+ }
+ }
+ } catch (err: any) {
+ console.error(`[meetingEngine] Auto-advance error:`, err.message);
+ }
+ }, duration);
+}
+
+async function executePhaseLogic(meetingId: string, phase: MeetingPhase): Promise<void> {
+ const meeting = getMeeting(meetingId)!;
+ const type = meeting.meeting_type;
+ const agenda = getAgendaItems(meetingId);
+
+ switch (phase) {
+ case 'caucus': {
+ // President Lincoln opens from the podium
+ const agentCount = TEAM_AGENTS.length;
+ const execCount = EXECUTIVES.length;
+ addMessage(meetingId, 'lincoln', BOSS_PERSONA.name, `Good day. This ${type} session is now convened with ${agentCount} agents and ${execCount} executives present. Let us proceed with focus and purpose.`, 'dialogue');
+ // CEO acknowledges
+ addMessage(meetingId, 'ceo', 'Curtis', `Thank you, ${BOSS_PERSONA.title}. All departments are standing by.`, 'dialogue');
+ break;
+ }
+
+ case 'call_to_order': {
+ // Roll call — pick 3-5 key agents based on meeting type
+ const keyAgents = getKeyAgentsForType(type);
+ for (const agent of keyAgents.slice(0, 3)) {
+ const dialogue = await generateDialogue(agent.id, agent.name, agent.role, phase, `Acknowledge presence at ${type} meeting`, type);
+ addMessage(meetingId, agent.id, agent.name, dialogue, 'dialogue');
+ }
+ break;
+ }
+
+ case 'old_business': {
+ // Review previous action items — COO reports
+ const dialogue = await generateDialogue('coo', 'Conrad', 'Chief Operations Officer', phase, 'Report on outstanding action items from previous meeting', type);
+ addMessage(meetingId, 'coo', 'Conrad', dialogue, 'dialogue');
+ break;
+ }
+
+ case 'current_business': {
+ // Discuss agenda items
+ if (agenda.length === 0) {
+ addMessage(meetingId, 'system', 'Boardroom', 'No agenda items to discuss.', 'system');
+ } else {
+ for (const item of agenda.slice(0, 3)) {
+ const owner = item.owner || 'ceo';
+ const ownerAgent = TEAM_AGENTS.find(a => a.id === owner) || EXECUTIVES.find(e => e.id === owner);
+ const name = ownerAgent ? ('name' in ownerAgent ? ownerAgent.name : owner) : owner;
+ const role = ownerAgent ? ('role' in ownerAgent ? ownerAgent.role : '') : '';
+
+ const dialogue = await generateDialogue(owner, name, role, phase, `Present agenda item: ${item.topic}. Recommendation: ${item.recommendation || 'None'}`, type);
+ addMessage(meetingId, owner, name, dialogue, 'dialogue');
+
+ updateAgendaItem(meetingId, item.id, { status: 'discussed' });
+ }
+ }
+ break;
+ }
+
+ case 'new_business': {
+ // Remaining agenda items + department heads report
+ const remaining = agenda.filter(a => a.status === 'pending');
+ for (const item of remaining.slice(0, 2)) {
+ const owner = item.owner || 'vp-ops';
+ const ownerAgent = TEAM_AGENTS.find(a => a.id === owner) || EXECUTIVES.find(e => e.id === owner);
+ const name = ownerAgent ? ('name' in ownerAgent ? ownerAgent.name : owner) : owner;
+ const role = ownerAgent ? ('role' in ownerAgent ? ownerAgent.role : '') : '';
+
+ const dialogue = await generateDialogue(owner, name, role, phase, `Raise new business: ${item.topic}`, type);
+ addMessage(meetingId, owner, name, dialogue, 'dialogue');
+ updateAgendaItem(meetingId, item.id, { status: 'discussed' });
+ }
+ break;
+ }
+
+ case 'breakout': {
+ // Brief departmental breakout summaries
+ const deptLeaders = ['max', 'grant', 'shane', 'atlas'];
+ for (const leaderId of deptLeaders.slice(0, 2)) {
+ const agent = TEAM_AGENTS.find(a => a.id === leaderId);
+ if (agent) {
+ const dialogue = await generateDialogue(agent.id, agent.name, agent.role, phase, `Brief departmental summary for ${agent.dept}`, type);
+ addMessage(meetingId, agent.id, agent.name, dialogue, 'dialogue');
+ }
+ }
+ break;
+ }
+
+ case 'minutes': {
+ // Note taker summarizes
+ const dialogue = await generateDialogue('nate', 'Nate', 'Note Taker & Records', phase, `Summarize key points and action items from this ${type} meeting`, type);
+ addMessage(meetingId, 'nate', 'Nate', dialogue, 'dialogue');
+ break;
+ }
+
+ case 'adjournment': {
+ // CEO delivers closing remarks to President Lincoln
+ const dialogue = await generateDialogue('ceo', 'Curtis', 'Chief Executive', phase, `Deliver closing remarks to President Lincoln for this ${type} meeting. Address him as Mr. President.`, type);
+ addMessage(meetingId, 'ceo', 'Curtis', dialogue, 'dialogue');
+ // President Lincoln adjourns
+ addMessage(meetingId, 'lincoln', BOSS_PERSONA.name, `This ${type} session stands adjourned. Thank you all for your contributions. Dismissed.`, 'dialogue');
+ break;
+ }
+ }
+}
+
+function completeMeeting(meetingId: string): Meeting {
+ clearPhaseTimer();
+ db.run(
+ `UPDATE br2_meetings SET status = 'completed', ended_at = datetime('now'), phase = 'adjournment' WHERE id = ?`,
+ [meetingId]
+ );
+ activeMeeting = null;
+
+ addMessage(meetingId, 'system', 'Boardroom', 'Meeting adjourned.', 'system');
+ const meeting = getMeeting(meetingId)!;
+ broadcast({ type: 'meeting_completed', payload: meeting, timestamp: new Date().toISOString() });
+ return meeting;
+}
+
+function clearPhaseTimer(): void {
+ if (phaseTimer) {
+ clearTimeout(phaseTimer);
+ phaseTimer = null;
+ }
+}
+
+function getKeyAgentsForType(type: string): { id: string; name: string; role: string }[] {
+ const all = [...EXECUTIVES.map(e => ({ id: e.id, name: e.name, role: e.role }))];
+
+ switch (type) {
+ case 'strategic':
+ return [...all, { id: 'frank', name: 'Lance', role: 'Legal Compliance' }, { id: 'grant', name: 'Finn', role: 'Finance' }];
+ case 'initiative':
+ return [...all, { id: 'atlas', name: 'Porter', role: 'Product Master' }, { id: 'bob', name: 'Craig', role: 'Content Creator' }];
+ case 'ops':
+ return [...all, { id: 'max', name: 'Ford', role: 'Fleet Commander' }, { id: 'hawk', name: 'Sven', role: 'System Monitor' }];
+ case 'emergency':
+ return [...all, { id: 'max', name: 'Ford', role: 'Fleet Commander' }, { id: 'vince', name: 'Vince', role: 'Site Monitor' }];
+ default:
+ return all;
+ }
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..57e4941
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,182 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — Core Types
+// ═══════════════════════════════════════════════
+
+// Meeting phases — ported from boardroom meeting-engine.js
+export const MEETING_PHASES = [
+ 'caucus', 'call_to_order', 'old_business', 'current_business',
+ 'new_business', 'breakout', 'minutes', 'adjournment'
+] as const;
+export type MeetingPhase = typeof MEETING_PHASES[number];
+
+// Phase durations in seconds
+export const PHASE_DURATIONS: Record<MeetingPhase, number> = {
+ caucus: 120,
+ call_to_order: 60,
+ old_business: 180,
+ current_business: 300,
+ new_business: 300,
+ breakout: 180,
+ minutes: 120,
+ adjournment: 60,
+};
+
+export const PHASE_LABELS: Record<MeetingPhase, string> = {
+ caucus: 'Caucus',
+ call_to_order: 'Call to Order',
+ old_business: 'Old Business',
+ current_business: 'Current Business',
+ new_business: 'New Business',
+ breakout: 'Breakout',
+ minutes: 'Minutes',
+ adjournment: 'Adjournment',
+};
+
+export type MeetingType = 'strategic' | 'initiative' | 'ops' | 'emergency';
+export type MeetingStatus = 'scheduled' | 'in_progress' | 'completed' | 'halted' | 'crashed';
+export type MessageType = 'dialogue' | 'phase' | 'system' | 'action' | 'decision';
+export type DecisionType = 'A' | 'B' | 'C';
+export type AgendaItemStatus = 'pending' | 'discussed' | 'resolved' | 'deferred';
+
+// ─── Models ────────────────────────────────────
+
+export interface Meeting {
+ id: string;
+ meeting_type: MeetingType;
+ phase: MeetingPhase;
+ status: MeetingStatus;
+ started_at: string | null;
+ ended_at: string | null;
+ summary: string | null;
+ attendees: string; // JSON array
+ action_items: string; // JSON array
+ created_at: string;
+}
+
+export interface MeetingMessage {
+ id: number;
+ meeting_id: string;
+ agent_id: string;
+ agent_name: string;
+ message: string;
+ message_type: MessageType;
+ phase: string | null;
+ created_at: string;
+}
+
+export interface AgendaItem {
+ id: number;
+ meeting_id: string;
+ topic: string;
+ recommendation: string | null;
+ decision_type: DecisionType;
+ owner: string | null;
+ position: number;
+ status: AgendaItemStatus;
+ created_at: string;
+}
+
+// ─── Agent System ──────────────────────────────
+
+export interface Agent {
+ id: string;
+ name: string;
+ pm2: string;
+ port?: number;
+ color: string;
+ role: string;
+ dept: string;
+ hasApi: boolean;
+ subMgr?: string;
+ isNoteTaker?: boolean;
+}
+
+export interface Executive {
+ id: string;
+ name: string;
+ port: number;
+ color: string;
+ role: string;
+ avatar: string;
+}
+
+export interface DeptGroup {
+ emoji: string;
+ color: string;
+ leader: string;
+ exec: string;
+ label: string;
+}
+
+// ─── Voice ─────────────────────────────────────
+
+export interface VoiceConfig {
+ tts_enabled: boolean;
+ tts_speed: number;
+ tts_volume: number;
+ auto_speak: boolean;
+}
+
+// ─── Governance Client Types ───────────────────
+
+export interface GovDecision {
+ id: string;
+ topic: string;
+ decision_type: DecisionType;
+ recommendation: string | null;
+ owner: string;
+ status: string;
+ score: number | null;
+ created_at: string;
+}
+
+export interface GovDelegation {
+ id: string;
+ owner: string;
+ delegate: string;
+ autonomy_level: number;
+ status: string;
+}
+
+export interface GovEscalation {
+ id: number;
+ decision_id: string | null;
+ escalation_type: string;
+ hours_elapsed: number;
+ action_taken: string | null;
+}
+
+export interface GovStatus {
+ governance_mode: string;
+ active_meeting: any;
+ pending_decisions: number;
+ active_delegations: number;
+ active_escalations: number;
+}
+
+export interface GovDecisionStats {
+ total: number;
+ pending: number;
+ approved: number;
+ rejected: number;
+ deferred: number;
+}
+
+// ─── WebSocket Events ──────────────────────────
+
+export type WSEventType =
+ | 'connected'
+ | 'meeting_started'
+ | 'meeting_phase'
+ | 'meeting_halted'
+ | 'meeting_completed'
+ | 'meeting_message'
+ | 'agent_speaking'
+ | 'agenda_updated'
+ | 'governance_event';
+
+export interface WSEvent {
+ type: WSEventType;
+ payload: any;
+ timestamp: string;
+}
diff --git a/src/ws/broadcast.ts b/src/ws/broadcast.ts
new file mode 100644
index 0000000..60c94e7
--- /dev/null
+++ b/src/ws/broadcast.ts
@@ -0,0 +1,62 @@
+// ═══════════════════════════════════════════════
+// DW Boardroom V2 — WebSocket Broadcast
+// Real-time meeting events to frontend + War Room
+// ═══════════════════════════════════════════════
+
+import { WebSocketServer, WebSocket } from 'ws';
+import type { Server } from 'http';
+import type { WSEvent } from '../types';
+
+let wss: WebSocketServer;
+
+export function initWebSocket(server: Server): WebSocketServer {
+ wss = new WebSocketServer({ server, path: '/ws' });
+
+ wss.on('connection', (ws) => {
+ console.log(`[ws] Client connected (${getConnectionCount()} total)`);
+
+ ws.send(JSON.stringify({
+ type: 'connected',
+ payload: { message: 'Connected to Boardroom V2 WebSocket' },
+ timestamp: new Date().toISOString(),
+ }));
+
+ ws.on('close', () => {
+ console.log(`[ws] Client disconnected (${getConnectionCount() - 1} remaining)`);
+ });
+
+ ws.on('error', (err) => {
+ console.error('[ws] Error:', err.message);
+ });
+ });
+
+ console.log(`[ws] WebSocket server initialized on /ws`);
+ return wss;
+}
+
+export function broadcast(event: WSEvent): void {
+ if (!wss) return;
+
+ const data = JSON.stringify(event);
+ let sent = 0;
+
+ wss.clients.forEach((client) => {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(data);
+ sent++;
+ }
+ });
+
+ if (sent > 0) {
+ console.log(`[ws] Broadcast ${event.type} to ${sent} clients`);
+ }
+}
+
+export function getConnectionCount(): number {
+ if (!wss) return 0;
+ let count = 0;
+ wss.clients.forEach((client) => {
+ if (client.readyState === WebSocket.OPEN) count++;
+ });
+ return count;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..3e1f375
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "commonjs",
+ "lib": ["ES2020"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "frontend"]
+}
(oldest)
·
back to Dw Boardroom V2
·
Stop tracking SQLite WAL/shm runtime files; gitignore *.db-s 2260e48 →