From c16fc970d11dcd63b5a784bf88585f4607dbb605 Mon Sep 17 00:00:00 2001 From: Keegan Thompson Date: Tue, 14 Apr 2026 20:32:55 -0500 Subject: [PATCH] feat: add vault web dashboard and kib ui command Wire Vite/React dashboard with server API; extend CLI and core compile/diff. Made-with: Cursor --- bun.lock | 271 +++++++++++++++- packages/cli/package.json | 1 + packages/cli/src/commands/ui.ts | 38 +++ packages/cli/src/index.ts | 10 + packages/core/src/compile/compiler.ts | 10 + packages/core/src/compile/diff.ts | 87 +++++- packages/dashboard/index.html | 18 ++ packages/dashboard/package.json | 37 +++ packages/dashboard/postcss.config.mjs | 5 + packages/dashboard/src/client/App.tsx | 41 +++ packages/dashboard/src/client/api.ts | 205 ++++++++++++ .../src/client/components/BrowsePage.tsx | 239 ++++++++++++++ .../src/client/components/GraphPage.tsx | 233 ++++++++++++++ .../src/client/components/IngestPage.tsx | 86 ++++++ .../src/client/components/QueryPage.tsx | 123 ++++++++ .../src/client/components/SearchPage.tsx | 96 ++++++ .../dashboard/src/client/components/Shell.tsx | 106 +++++++ .../src/client/components/StatusPage.tsx | 216 +++++++++++++ packages/dashboard/src/client/globals.css | 118 +++++++ packages/dashboard/src/client/main.tsx | 13 + packages/dashboard/src/client/useEvents.ts | 71 +++++ packages/dashboard/src/server/api.ts | 292 ++++++++++++++++++ packages/dashboard/src/server/context.ts | 56 ++++ packages/dashboard/src/server/events.ts | 69 +++++ packages/dashboard/src/server/index.ts | 73 +++++ packages/dashboard/src/server/stream.ts | 44 +++ packages/dashboard/tsconfig.json | 18 ++ packages/dashboard/vite.config.ts | 16 + 28 files changed, 2576 insertions(+), 16 deletions(-) create mode 100644 packages/cli/src/commands/ui.ts create mode 100644 packages/dashboard/index.html create mode 100644 packages/dashboard/package.json create mode 100644 packages/dashboard/postcss.config.mjs create mode 100644 packages/dashboard/src/client/App.tsx create mode 100644 packages/dashboard/src/client/api.ts create mode 100644 packages/dashboard/src/client/components/BrowsePage.tsx create mode 100644 packages/dashboard/src/client/components/GraphPage.tsx create mode 100644 packages/dashboard/src/client/components/IngestPage.tsx create mode 100644 packages/dashboard/src/client/components/QueryPage.tsx create mode 100644 packages/dashboard/src/client/components/SearchPage.tsx create mode 100644 packages/dashboard/src/client/components/Shell.tsx create mode 100644 packages/dashboard/src/client/components/StatusPage.tsx create mode 100644 packages/dashboard/src/client/globals.css create mode 100644 packages/dashboard/src/client/main.tsx create mode 100644 packages/dashboard/src/client/useEvents.ts create mode 100644 packages/dashboard/src/server/api.ts create mode 100644 packages/dashboard/src/server/context.ts create mode 100644 packages/dashboard/src/server/events.ts create mode 100644 packages/dashboard/src/server/index.ts create mode 100644 packages/dashboard/src/server/stream.ts create mode 100644 packages/dashboard/tsconfig.json create mode 100644 packages/dashboard/vite.config.ts diff --git a/bun.lock b/bun.lock index c89da5b..34c0de6 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "kib-monorepo", @@ -10,12 +9,13 @@ }, "packages/cli": { "name": "@kibhq/cli", - "version": "0.4.3", + "version": "1.1.0", "bin": { "kib": "./bin/kib.ts", }, "dependencies": { - "@kibhq/core": "^0.4.2", + "@kibhq/core": "^1.0.0", + "@kibhq/dashboard": "^1.1.0", "@modelcontextprotocol/sdk": "^1.29.0", "chalk": "^5.4.1", "commander": "^14.0.0", @@ -29,7 +29,7 @@ }, "packages/core": { "name": "@kibhq/core", - "version": "0.4.3", + "version": "1.1.0", "dependencies": { "@anthropic-ai/sdk": "^0.82.0", "@iarna/toml": "^2.2.5", @@ -47,6 +47,33 @@ "typescript": "^5.8.3", }, }, + "packages/dashboard": { + "name": "@kibhq/dashboard", + "version": "1.1.0", + "dependencies": { + "@kibhq/core": "^1.1.0", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "lucide-react": "^0.475.0", + "marked": "^15.0.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.7", + "@types/d3-force": "^3.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-zoom": "^3.0.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.7", + "typescript": "^5.8.3", + "vite": "^6.3.0", + }, + }, "packages/extension": { "name": "@kibhq/extension", "version": "0.6.0", @@ -88,8 +115,46 @@ "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.82.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-xdHTjL1GlUlDugHq/I47qdOKp/ROPvuHl7ROJCgUQigbvPu7asf9KcAcU1EqdrP2LuVhEKaTs7Z+ShpZDRzHdQ=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "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" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "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" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "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" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "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" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@biomejs/biome": ["@biomejs/biome@2.4.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.10", "@biomejs/cli-darwin-x64": "2.4.10", "@biomejs/cli-linux-arm64": "2.4.10", "@biomejs/cli-linux-arm64-musl": "2.4.10", "@biomejs/cli-linux-x64": "2.4.10", "@biomejs/cli-linux-x64-musl": "2.4.10", "@biomejs/cli-win32-arm64": "2.4.10", "@biomejs/cli-win32-x64": "2.4.10" }, "bin": { "biome": "bin/biome" } }, "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw=="], @@ -110,6 +175,58 @@ "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="], "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], @@ -178,6 +295,8 @@ "@kibhq/core": ["@kibhq/core@workspace:packages/core"], + "@kibhq/dashboard": ["@kibhq/dashboard@workspace:packages/dashboard"], + "@kibhq/extension": ["@kibhq/extension@workspace:packages/extension"], "@kibhq/web": ["@kibhq/web@workspace:packages/web"], @@ -254,6 +373,58 @@ "@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], @@ -286,8 +457,28 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -298,6 +489,8 @@ "@vercel/analytics": ["@vercel/analytics@2.0.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "nuxt": ">= 3", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "nuxt", "react", "svelte", "vue", "vue-router"] }, "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "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" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -306,10 +499,14 @@ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -342,6 +539,8 @@ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -356,6 +555,28 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -374,6 +595,8 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "electron-to-chromium": ["electron-to-chromium@1.5.334", "", {}, "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -390,6 +613,10 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "esbuild": ["esbuild@0.25.12", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -406,14 +633,20 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -454,12 +687,18 @@ "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], @@ -486,10 +725,14 @@ "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lucide-react": ["lucide-react@0.475.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], @@ -510,6 +753,8 @@ "next": ["next@15.5.14", "", { "dependencies": { "@next/env": "15.5.14", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.14", "@next/swc-darwin-x64": "15.5.14", "@next/swc-linux-arm64-gnu": "15.5.14", "@next/swc-linux-arm64-musl": "15.5.14", "@next/swc-linux-x64-gnu": "15.5.14", "@next/swc-linux-x64-musl": "15.5.14", "@next/swc-win32-arm64-msvc": "15.5.14", "@next/swc-win32-x64-msvc": "15.5.14", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ=="], + "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -544,6 +789,8 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], @@ -560,17 +807,21 @@ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -612,6 +863,8 @@ "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], @@ -630,8 +883,12 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vite": ["vite@6.4.2", "", { "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" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], @@ -642,6 +899,8 @@ "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -670,6 +929,8 @@ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], } } diff --git a/packages/cli/package.json b/packages/cli/package.json index c4f2ae9..c4f2856 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@kibhq/core": "^1.0.0", + "@kibhq/dashboard": "^1.1.0", "@modelcontextprotocol/sdk": "^1.29.0", "chalk": "^5.4.1", "commander": "^14.0.0", diff --git a/packages/cli/src/commands/ui.ts b/packages/cli/src/commands/ui.ts new file mode 100644 index 0000000..89766ca --- /dev/null +++ b/packages/cli/src/commands/ui.ts @@ -0,0 +1,38 @@ +import { resolveVaultRoot, VaultNotFoundError } from "@kibhq/core"; +import chalk from "chalk"; + +export async function ui(opts: { port?: string; open?: boolean }) { + let root: string; + try { + root = resolveVaultRoot(); + } catch (e) { + if (e instanceof VaultNotFoundError) { + console.error(e.message); + process.exit(1); + } + throw e; + } + + // Load saved API keys + const { loadCredentials } = await import("../ui/setup-provider.js"); + loadCredentials(); + + const port = Number.parseInt(opts.port ?? "4848", 10); + + const { startServer } = await import("@kibhq/dashboard"); + const { url } = await startServer(root, port); + + console.log(chalk.bold(`\n kib dashboard running at ${chalk.cyan(url)}\n`)); + + // Auto-open browser unless --no-open + if (opts.open !== false) { + try { + Bun.spawn(["open", url]); + } catch { + // Non-macOS or open not available — that's fine + } + } + + // Keep process alive + await new Promise(() => {}); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d4510f9..6eae2ee 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -157,6 +157,16 @@ program await serve(); }); +program + .command("ui") + .description("Launch the local web dashboard") + .option("--port ", "server port", "4848") + .option("--no-open", "don't auto-open browser") + .action(async (opts) => { + const { ui } = await import("./commands/ui.js"); + await ui(opts); + }); + program .command("mcp [subcommand]") .description("Configure MCP in AI clients (default: setup)") diff --git a/packages/core/src/compile/compiler.ts b/packages/core/src/compile/compiler.ts index d06b5cc..c8ee4ee 100644 --- a/packages/core/src/compile/compiler.ts +++ b/packages/core/src/compile/compiler.ts @@ -51,6 +51,8 @@ export interface CompileOptions { onProgress?: (msg: string) => void; /** Callback fired for each article as it is processed */ onArticle?: (event: ArticleEvent) => void; + /** Signal to abort compilation between sources */ + signal?: AbortSignal; } // ─── Token estimation ────────────────────────────────────────── @@ -511,6 +513,10 @@ async function compileVaultInner( // Process in batches for (let i = 0; i < sourcesToCompile.length; i += maxParallel) { + if (options.signal?.aborted) { + options.onProgress?.("Compile aborted."); + break; + } // Check token budget before starting a new batch if (maxTokensPerPass && totalInputTokens >= maxTokensPerPass) { allWarnings.push( @@ -580,6 +586,10 @@ async function compileVaultInner( } else { // Sequential compilation (original behavior) for (const [sourceId, sourcePath] of sourcesToCompile) { + if (options.signal?.aborted) { + options.onProgress?.("Compile aborted."); + break; + } // Check token budget if (maxTokensPerPass && totalInputTokens >= maxTokensPerPass) { allWarnings.push( diff --git a/packages/core/src/compile/diff.ts b/packages/core/src/compile/diff.ts index 522eb5d..d3095bc 100644 --- a/packages/core/src/compile/diff.ts +++ b/packages/core/src/compile/diff.ts @@ -28,28 +28,93 @@ export function parseCompileOutput(raw: string): FileOperation[] { /** * Extract JSON array from LLM output that may contain surrounding text. + * Handles: code fences, leading/trailing prose, truncated output, nested brackets in strings. */ function extractJson(raw: string): string { let text = raw.trim(); - // Strip markdown code fences - text = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, ""); + // Strip all markdown code fences (including nested ones wrapping the whole output) + text = text.replace(/^```(?:json)?\s*\n?/gi, "").replace(/\n?```\s*$/gi, ""); text = text.trim(); - // If it already starts with [, try it directly - if (text.startsWith("[")) { - return text; - } - - // Try to find a JSON array in the text + // Find the first top-level [ and walk to its matching ] const arrayStart = text.indexOf("["); - const arrayEnd = text.lastIndexOf("]"); + if (arrayStart === -1) return text; + + let depth = 0; + let inString = false; + let escaped = false; + let arrayEnd = -1; - if (arrayStart !== -1 && arrayEnd !== -1 && arrayEnd > arrayStart) { + for (let i = arrayStart; i < text.length; i++) { + const ch = text[i]; + + if (escaped) { + escaped = false; + continue; + } + + if (ch === "\\") { + escaped = true; + continue; + } + + if (ch === '"') { + inString = !inString; + continue; + } + + if (inString) continue; + + if (ch === "[" || ch === "{") depth++; + else if (ch === "]" || ch === "}") { + depth--; + if (depth === 0) { + arrayEnd = i; + break; + } + } + } + + if (arrayEnd !== -1) { return text.slice(arrayStart, arrayEnd + 1); } - // Nothing worked, return as-is and let JSON.parse fail with a clear error + // Truncated output — try to repair by closing open structures + if (depth > 0) { + let repaired = text.slice(arrayStart); + // If we're inside a string, close it + if (inString) repaired += '"'; + // Strip any trailing incomplete key-value pair + repaired = repaired.replace(/,\s*"[^"]*"?\s*:?\s*"?[^"]*$/, ""); + // Close remaining open structures + // Find what's still open by re-scanning + let s = false; + let esc = false; + const stack: string[] = []; + for (const c of repaired) { + if (esc) { + esc = false; + continue; + } + if (c === "\\") { + esc = true; + continue; + } + if (c === '"') { + s = !s; + continue; + } + if (s) continue; + if (c === "[") stack.push("]"); + else if (c === "{") stack.push("}"); + else if (c === "]" || c === "}") stack.pop(); + } + // Close in reverse order + repaired += stack.reverse().join(""); + return repaired; + } + return text; } diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html new file mode 100644 index 0000000..539eb4a --- /dev/null +++ b/packages/dashboard/index.html @@ -0,0 +1,18 @@ + + + + + + kib + + + + + +
+ + + diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json new file mode 100644 index 0000000..d208f2f --- /dev/null +++ b/packages/dashboard/package.json @@ -0,0 +1,37 @@ +{ + "name": "@kibhq/dashboard", + "version": "1.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/server/index.ts" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@kibhq/core": "^1.1.0", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "lucide-react": "^0.475.0", + "marked": "^15.0.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.7", + "@types/d3-force": "^3.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-zoom": "^3.0.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.7", + "typescript": "^5.8.3", + "vite": "^6.3.0" + } +} diff --git a/packages/dashboard/postcss.config.mjs b/packages/dashboard/postcss.config.mjs new file mode 100644 index 0000000..017b34b --- /dev/null +++ b/packages/dashboard/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/packages/dashboard/src/client/App.tsx b/packages/dashboard/src/client/App.tsx new file mode 100644 index 0000000..c2cf948 --- /dev/null +++ b/packages/dashboard/src/client/App.tsx @@ -0,0 +1,41 @@ +import { useCallback, useEffect, useState } from "react"; +import { api } from "./api.js"; +import { BrowsePage } from "./components/BrowsePage.js"; +import { GraphPage } from "./components/GraphPage.js"; +import { IngestPage } from "./components/IngestPage.js"; +import { QueryPage } from "./components/QueryPage.js"; +import { SearchPage } from "./components/SearchPage.js"; +import { type Page, Shell } from "./components/Shell.js"; +import { StatusPage } from "./components/StatusPage.js"; +import { useEvents } from "./useEvents.js"; + +export function App() { + const [page, setPage] = useState("status"); + const [vaultPath, setVaultPath] = useState(); + const { revision, lastEvent } = useEvents(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: revision triggers re-fetch on vault changes + useEffect(() => { + api + .getStatus() + .then((s) => setVaultPath(s.root)) + .catch(() => {}); + }, [revision]); + + const handleNavigateToArticle = useCallback((_path: string) => { + setPage("browse"); + }, []); + + return ( + + {page === "status" && } + {page === "browse" && } + {page === "search" && } + {page === "query" && } + {page === "graph" && ( + + )} + {page === "ingest" && } + + ); +} diff --git a/packages/dashboard/src/client/api.ts b/packages/dashboard/src/client/api.ts new file mode 100644 index 0000000..44f4818 --- /dev/null +++ b/packages/dashboard/src/client/api.ts @@ -0,0 +1,205 @@ +const BASE = "/api"; + +async function get(path: string): Promise { + const res = await fetch(`${BASE}${path}`); + if (!res.ok) { + const body = await res.json().catch(() => ({ error: res.statusText })); + throw new Error((body as { error?: string }).error ?? res.statusText); + } + return res.json() as Promise; +} + +async function post(path: string, body: unknown): Promise { + const res = await fetch(`${BASE}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({ error: res.statusText })); + throw new Error((data as { error?: string }).error ?? res.statusText); + } + return res.json() as Promise; +} + +// --- Types --- + +export interface VaultStatus { + vault: { + name: string; + created: string; + lastCompiled: string | null; + provider: string; + model: string; + }; + root: string; + stats: { + totalSources: number; + totalArticles: number; + totalWords: number; + lastLintAt: string | null; + }; + provider: { + name: string; + model: string; + ready: boolean; + apiKeyHint: string | null; + }; +} + +export interface ArticleItem { + path: string; + slug: string; + title?: string; + category: string; + tags: string[]; + summary: string; + wordCount: number; + lastUpdated?: string; +} + +export interface RawItem { + path: string; + title: string; + sourceType?: string; + ingestedAt?: string; + wordCount?: number; +} + +export interface ArticleContent { + path: string; + content: string; +} + +export interface SearchResult { + path: string; + score: number; + snippet: string; + title?: string; + scope: "wiki" | "raw"; +} + +export interface GraphData { + nodes: { + id: string; + category: string; + tags: string[]; + wordCount: number; + summary: string; + }[]; + edges: { source: string; target: string }[]; +} + +export interface IngestResult { + sourceId: string; + path: string; + sourceType: string; + title: string; + wordCount: number; + skipped: boolean; + skipReason?: string; +} + +export interface CompileResult { + sourcesCompiled: number; + articlesCreated: number; + articlesUpdated: number; + articlesDeleted: number; +} + +// --- API Functions --- + +export const api = { + getStatus: () => get("/status"), + + compile: (force = false) => post<{ started: boolean }>("/compile", { force }), + + stopCompile: () => post<{ stopped: boolean }>("/compile/stop", {}), + + getArticles: (scope: "wiki" | "raw" = "wiki") => get(`/articles?scope=${scope}`), + + getRawSources: () => get("/articles?scope=raw"), + + readArticle: (path: string, scope: "wiki" | "raw" = "wiki") => + get(`/articles/${path}?scope=${scope}`), + + search: (q: string, opts?: { limit?: number; tag?: string; since?: string }) => { + const params = new URLSearchParams({ q }); + if (opts?.limit) params.set("limit", String(opts.limit)); + if (opts?.tag) params.set("tag", opts.tag); + if (opts?.since) params.set("since", opts.since); + return get(`/search?${params}`); + }, + + queryStream: ( + question: string, + onChunk: (text: string) => void, + onDone: (sourcePaths: string[]) => void, + onError: (error: string) => void, + ) => { + const controller = new AbortController(); + + fetch(`${BASE}/query`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ question }), + signal: controller.signal, + }) + .then(async (res) => { + if (!res.ok || !res.body) { + onError("Failed to start query stream"); + return; + } + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + if (!line.startsWith("data: ")) continue; + const payload = line.slice(6); + try { + const data = JSON.parse(payload) as { + text?: string; + done?: boolean; + sourcePaths?: string[]; + error?: string; + }; + if (data.error) { + onError(data.error); + return; + } + if (data.text) onChunk(data.text); + if (data.done && data.sourcePaths) onDone(data.sourcePaths); + } catch { + // ignore malformed lines + } + } + } + }) + .catch((err) => { + if ((err as Error).name !== "AbortError") { + onError((err as Error).message); + } + }); + + return () => controller.abort(); + }, + + ingest: (urlOrContent: string, title?: string) => { + const isUrl = urlOrContent.startsWith("http://") || urlOrContent.startsWith("https://"); + return post( + "/ingest", + isUrl ? { url: urlOrContent } : { content: urlOrContent, title }, + ); + }, + + getGraph: () => get("/graph"), +}; diff --git a/packages/dashboard/src/client/components/BrowsePage.tsx b/packages/dashboard/src/client/components/BrowsePage.tsx new file mode 100644 index 0000000..7671b25 --- /dev/null +++ b/packages/dashboard/src/client/components/BrowsePage.tsx @@ -0,0 +1,239 @@ +import { ArrowLeft, FileText, Tag } from "lucide-react"; +import { Marked } from "marked"; +import { useEffect, useMemo, useState } from "react"; +import { type ArticleItem, api, type RawItem } from "../api.js"; + +const marked = new Marked(); + +const CATEGORY_COLORS: Record = { + concept: "bg-blue-100 text-blue-700", + topic: "bg-green-100 text-green-700", + reference: "bg-orange-100 text-orange-700", + output: "bg-purple-100 text-purple-700", +}; + +type Scope = "wiki" | "raw"; + +export function BrowsePage({ revision = 0 }: { revision?: number }) { + const [scope, setScope] = useState("raw"); + const [wikiArticles, setWikiArticles] = useState([]); + const [rawSources, setRawSources] = useState([]); + const [selectedPath, setSelectedPath] = useState(null); + const [content, setContent] = useState(""); + const [filter, setFilter] = useState(""); + const [categoryFilter, setCategoryFilter] = useState("all"); + const [loading, setLoading] = useState(true); + + // biome-ignore lint/correctness/useExhaustiveDependencies: revision triggers re-fetch on vault changes + useEffect(() => { + setLoading(true); + Promise.all([api.getArticles("wiki"), api.getRawSources()]).then(([wiki, raw]) => { + setWikiArticles(wiki); + setRawSources(raw); + // Default to whichever has content + if (wiki.length === 0 && raw.length > 0) setScope("raw"); + else if (wiki.length > 0) setScope("wiki"); + setLoading(false); + }); + }, [revision]); + + const categories = useMemo(() => { + if (scope === "raw") return ["all"]; + const cats = new Set(wikiArticles.map((a) => a.category)); + return ["all", ...Array.from(cats).sort()]; + }, [wikiArticles, scope]); + + const filteredWiki = useMemo(() => { + return wikiArticles.filter((a) => { + if (categoryFilter !== "all" && a.category !== categoryFilter) return false; + if (filter) { + const q = filter.toLowerCase(); + return ( + a.slug.toLowerCase().includes(q) || + a.summary.toLowerCase().includes(q) || + a.tags.some((t) => t.toLowerCase().includes(q)) + ); + } + return true; + }); + }, [wikiArticles, filter, categoryFilter]); + + const filteredRaw = useMemo(() => { + if (!filter) return rawSources; + const q = filter.toLowerCase(); + return rawSources.filter( + (s) => s.title.toLowerCase().includes(q) || s.path.toLowerCase().includes(q), + ); + }, [rawSources, filter]); + + const openArticle = async (path: string, articleScope: Scope) => { + setSelectedPath(path); + const data = await api.readArticle(path, articleScope); + setContent(data.content); + }; + + const renderedContent = useMemo(() => { + if (!content) return ""; + const stripped = content.replace(/^---[\s\S]*?---\n*/, ""); + return marked.parse(stripped) as string; + }, [content]); + + if (selectedPath) { + return ( +
+ +
+
+ ); + } + + return ( +
+

Browse

+ + {/* Scope tabs */} +
+ + +
+ +
+ setFilter(e.target.value)} + className="flex-1 px-3 py-2 border rounded-md text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)] focus:ring-offset-1" + /> + {scope === "wiki" && categories.length > 1 && ( + + )} +
+ + {loading ? ( +

Loading...

+ ) : scope === "wiki" ? ( + filteredWiki.length === 0 ? ( +

+ {wikiArticles.length === 0 + ? "No compiled articles yet. Sources are available in the Sources tab." + : "No matching articles."} +

+ ) : ( +
+ {filteredWiki.map((article) => ( + + ))} +
+ ) + ) : filteredRaw.length === 0 ? ( +

+ {rawSources.length === 0 ? "No sources yet." : "No matching sources."} +

+ ) : ( +
+ {filteredRaw.map((source) => ( + + ))} +
+ )} +
+ ); +} diff --git a/packages/dashboard/src/client/components/GraphPage.tsx b/packages/dashboard/src/client/components/GraphPage.tsx new file mode 100644 index 0000000..f320e10 --- /dev/null +++ b/packages/dashboard/src/client/components/GraphPage.tsx @@ -0,0 +1,233 @@ +import { + forceCenter, + forceCollide, + forceLink, + forceManyBody, + forceSimulation, + type SimulationNodeDatum, +} from "d3-force"; +import { select } from "d3-selection"; +import { zoom, zoomIdentity } from "d3-zoom"; +import { useEffect, useRef, useState } from "react"; +import { api, type GraphData } from "../api.js"; + +interface GraphNode extends SimulationNodeDatum { + id: string; + category: string; + summary: string; +} + +interface GraphLink { + source: string | GraphNode; + target: string | GraphNode; +} + +const CATEGORY_COLORS: Record = { + concept: "#3b82f6", + topic: "#22c55e", + reference: "#f97316", + output: "#a855f7", +}; + +export function GraphPage({ + onNavigateToArticle, + revision = 0, +}: { + onNavigateToArticle?: (slug: string) => void; + revision?: number; +}) { + const canvasRef = useRef(null); + const [data, setData] = useState(null); + const [hovered, setHovered] = useState(null); + const [loading, setLoading] = useState(true); + + // biome-ignore lint/correctness/useExhaustiveDependencies: revision triggers re-fetch on vault changes + useEffect(() => { + api + .getGraph() + .then(setData) + .finally(() => setLoading(false)); + }, [revision]); + + useEffect(() => { + if (!data || !canvasRef.current) return; + if (data.nodes.length === 0) return; + + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const width = canvas.parentElement?.clientWidth ?? 800; + const height = canvas.parentElement?.clientHeight ?? 600; + canvas.width = width * devicePixelRatio; + canvas.height = height * devicePixelRatio; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + ctx.scale(devicePixelRatio, devicePixelRatio); + + const nodes: GraphNode[] = data.nodes.map((n) => ({ ...n })); + const links: GraphLink[] = data.edges.map((e) => ({ + source: e.source, + target: e.target, + })); + + let currentTransform = zoomIdentity; + + const simulation = forceSimulation(nodes) + .force( + "link", + forceLink(links) + .id((d) => d.id) + .distance(80), + ) + .force("charge", forceManyBody().strength(-200)) + .force("center", forceCenter(width / 2, height / 2)) + .force("collide", forceCollide(20)); + + function draw() { + if (!ctx) return; + ctx.save(); + ctx.clearRect(0, 0, width, height); + + ctx.translate(currentTransform.x, currentTransform.y); + ctx.scale(currentTransform.k, currentTransform.k); + + // Draw edges + ctx.strokeStyle = "#e0e0e0"; + ctx.lineWidth = 1; + for (const link of links) { + const source = link.source as GraphNode; + const target = link.target as GraphNode; + if (source.x == null || source.y == null || target.x == null || target.y == null) continue; + ctx.beginPath(); + ctx.moveTo(source.x, source.y); + ctx.lineTo(target.x, target.y); + ctx.stroke(); + } + + // Draw nodes + for (const node of nodes) { + if (node.x == null || node.y == null) continue; + const radius = 6 + Math.min(node.id.length * 0.3, 6); + const color = CATEGORY_COLORS[node.category] ?? "#888"; + + ctx.beginPath(); + ctx.arc(node.x, node.y, radius, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + + // Label + ctx.fillStyle = "#333"; + ctx.font = "10px JetBrains Mono, monospace"; + ctx.textAlign = "center"; + ctx.fillText(node.id, node.x, node.y + radius + 12); + } + + ctx.restore(); + } + + simulation.on("tick", draw); + + // Zoom + const zoomBehavior = zoom() + .scaleExtent([0.2, 5]) + .on("zoom", (event) => { + currentTransform = event.transform; + draw(); + }); + + select(canvas).call(zoomBehavior); + + // Hover detection + const handleMouseMove = (e: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + const mx = (e.clientX - rect.left - currentTransform.x) / currentTransform.k; + const my = (e.clientY - rect.top - currentTransform.y) / currentTransform.k; + + let found: GraphNode | null = null; + for (const node of nodes) { + if (node.x == null || node.y == null) continue; + const dx = mx - node.x; + const dy = my - node.y; + if (dx * dx + dy * dy < 200) { + found = node; + break; + } + } + setHovered(found); + canvas.style.cursor = found ? "pointer" : "default"; + }; + + const handleClick = (e: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + const mx = (e.clientX - rect.left - currentTransform.x) / currentTransform.k; + const my = (e.clientY - rect.top - currentTransform.y) / currentTransform.k; + + for (const node of nodes) { + if (node.x == null || node.y == null) continue; + const dx = mx - node.x; + const dy = my - node.y; + if (dx * dx + dy * dy < 200) { + onNavigateToArticle?.(node.id); + break; + } + } + }; + + canvas.addEventListener("mousemove", handleMouseMove); + canvas.addEventListener("click", handleClick); + + return () => { + simulation.stop(); + canvas.removeEventListener("mousemove", handleMouseMove); + canvas.removeEventListener("click", handleClick); + }; + }, [data, onNavigateToArticle]); + + if (loading) { + return ( +
+

Loading graph...

+
+ ); + } + + if (!data || data.nodes.length === 0) { + return ( +
+

Knowledge Graph

+

+ No graph data yet. Compile some sources to build connections. +

+
+ ); + } + + return ( +
+
+

Knowledge Graph

+
+ {Object.entries(CATEGORY_COLORS).map(([cat, color]) => ( +
+ + {cat} +
+ ))} +
+
+
+ + {hovered && ( +
+

{hovered.id}

+

{hovered.category}

+ {hovered.summary && ( +

{hovered.summary}

+ )} +
+ )} +
+
+ ); +} diff --git a/packages/dashboard/src/client/components/IngestPage.tsx b/packages/dashboard/src/client/components/IngestPage.tsx new file mode 100644 index 0000000..f2bf696 --- /dev/null +++ b/packages/dashboard/src/client/components/IngestPage.tsx @@ -0,0 +1,86 @@ +import { Check, Loader2, Plus } from "lucide-react"; +import { useState } from "react"; +import { api, type IngestResult } from "../api.js"; + +export function IngestPage() { + const [url, setUrl] = useState(""); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!url.trim() || loading) return; + + setLoading(true); + setResult(null); + setError(null); + + try { + const res = await api.ingest(url); + setResult(res); + setUrl(""); + } catch (err) { + setError((err as Error).message); + } finally { + setLoading(false); + } + }; + + return ( +
+

Ingest

+

+ Add a URL to ingest into your knowledge base. Supports web pages, YouTube videos, GitHub + repos, and more. +

+ +
+ setUrl(e.target.value)} + disabled={loading} + className="flex-1 px-3 py-2.5 border rounded-md text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)] focus:ring-offset-1 disabled:opacity-50" + /> + +
+ + {error && ( +
+

{error}

+
+ )} + + {result && ( +
+
+ + + {result.skipped ? "Already ingested" : "Ingested successfully"} + +
+
+

+ Title: {result.title} +

+

+ Type: {result.sourceType} +

+

+ Words: {result.wordCount.toLocaleString()} +

+
+
+ )} +
+ ); +} diff --git a/packages/dashboard/src/client/components/QueryPage.tsx b/packages/dashboard/src/client/components/QueryPage.tsx new file mode 100644 index 0000000..a8f343e --- /dev/null +++ b/packages/dashboard/src/client/components/QueryPage.tsx @@ -0,0 +1,123 @@ +import { Loader2, Send } from "lucide-react"; +import { Marked } from "marked"; +import { useMemo, useRef, useState } from "react"; +import { api } from "../api.js"; + +const marked = new Marked(); + +export function QueryPage() { + const [question, setQuestion] = useState(""); + const [answer, setAnswer] = useState(""); + const [sources, setSources] = useState([]); + const [streaming, setStreaming] = useState(false); + const [error, setError] = useState(null); + const cancelRef = useRef<(() => void) | null>(null); + + const renderedAnswer = useMemo(() => { + if (!answer) return ""; + return marked.parse(answer) as string; + }, [answer]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!question.trim() || streaming) return; + + setAnswer(""); + setSources([]); + setError(null); + setStreaming(true); + + cancelRef.current = api.queryStream( + question, + (text) => setAnswer((prev) => prev + text), + (sourcePaths) => { + setSources(sourcePaths); + setStreaming(false); + }, + (err) => { + setError(err); + setStreaming(false); + }, + ); + }; + + const handleCancel = () => { + cancelRef.current?.(); + setStreaming(false); + }; + + return ( +
+

Query

+ +
+ setQuestion(e.target.value)} + disabled={streaming} + className="flex-1 px-3 py-2.5 border rounded-md text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)] focus:ring-offset-1 disabled:opacity-50" + /> + {streaming ? ( + + ) : ( + + )} +
+ + {error && ( +
+

{error}

+
+ )} + + {(answer || streaming) && ( +
+ {streaming && !answer && ( +
+ + Thinking... +
+ )} + {answer && ( +
+ )} + {streaming && answer && ( + + )} +
+ )} + + {sources.length > 0 && ( +
+

+ Sources +

+
+ {sources.map((src) => ( +

+ {src} +

+ ))} +
+
+ )} +
+ ); +} diff --git a/packages/dashboard/src/client/components/SearchPage.tsx b/packages/dashboard/src/client/components/SearchPage.tsx new file mode 100644 index 0000000..8b83bd4 --- /dev/null +++ b/packages/dashboard/src/client/components/SearchPage.tsx @@ -0,0 +1,96 @@ +import { FileText, Search } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { api, type SearchResult } from "../api.js"; + +export function SearchPage({ + onNavigateToArticle, +}: { + onNavigateToArticle?: (path: string) => void; +}) { + const [query, setQuery] = useState(""); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [searched, setSearched] = useState(false); + const debounceRef = useRef>(); + + const doSearch = useCallback(async (q: string) => { + if (!q.trim()) { + setResults([]); + setSearched(false); + return; + } + setLoading(true); + try { + const res = await api.search(q, { limit: 20 }); + setResults(res); + setSearched(true); + } catch { + setResults([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => doSearch(query), 250); + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, [query, doSearch]); + + return ( +
+

Search

+ +
+ + setQuery(e.target.value)} + // biome-ignore lint/a11y/noAutofocus: search page should focus input on mount + autoFocus + className="w-full pl-10 pr-4 py-2.5 border rounded-md text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)] focus:ring-offset-1" + /> +
+ + {loading &&

Searching...

} + + {!loading && searched && results.length === 0 && ( +

No results found.

+ )} + + {!loading && results.length > 0 && ( +
+ {results.map((result) => ( + + ))} +
+ )} +
+ ); +} diff --git a/packages/dashboard/src/client/components/Shell.tsx b/packages/dashboard/src/client/components/Shell.tsx new file mode 100644 index 0000000..3c53843 --- /dev/null +++ b/packages/dashboard/src/client/components/Shell.tsx @@ -0,0 +1,106 @@ +import { + BookOpen, + Compass, + LayoutDashboard, + type LucideIcon, + MessageSquare, + Plus, + Search, +} from "lucide-react"; +import { type ReactNode, useEffect, useState } from "react"; +import type { VaultEvent } from "../useEvents.js"; + +export type Page = "status" | "browse" | "search" | "query" | "graph" | "ingest"; + +interface NavItem { + page: Page; + label: string; + icon: LucideIcon; +} + +const NAV_ITEMS: NavItem[] = [ + { page: "status", label: "Dashboard", icon: LayoutDashboard }, + { page: "browse", label: "Browse", icon: BookOpen }, + { page: "search", label: "Search", icon: Search }, + { page: "query", label: "Query", icon: MessageSquare }, + { page: "graph", label: "Graph", icon: Compass }, + { page: "ingest", label: "Ingest", icon: Plus }, +]; + +interface ShellProps { + currentPage: Page; + onNavigate: (page: Page) => void; + vaultPath?: string; + lastEvent?: VaultEvent | null; + children: ReactNode; +} + +export function Shell({ currentPage, onNavigate, vaultPath, lastEvent, children }: ShellProps) { + const [toast, setToast] = useState(null); + + useEffect(() => { + if (!lastEvent) return; + if (lastEvent.type === "ingest") { + setToast(`Ingested: ${lastEvent.title}`); + } else if (lastEvent.type === "compile_done") { + setToast(`Compiled: ${lastEvent.articlesCreated} articles created`); + } else if (lastEvent.type === "compile_article") { + setToast(`${lastEvent.op === "create" ? "Created" : "Updated"}: ${lastEvent.title}`); + } + const timer = setTimeout(() => setToast(null), 3000); + return () => clearTimeout(timer); + }, [lastEvent]); + + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+ {children} + + {/* Toast notification */} + {toast && ( +
+ {toast} +
+ )} +
+
+ ); +} diff --git a/packages/dashboard/src/client/components/StatusPage.tsx b/packages/dashboard/src/client/components/StatusPage.tsx new file mode 100644 index 0000000..42731b3 --- /dev/null +++ b/packages/dashboard/src/client/components/StatusPage.tsx @@ -0,0 +1,216 @@ +import { BookOpen, Database, FileText, Loader2, Play, Zap } from "lucide-react"; +import { useEffect, useState } from "react"; +import { api, type VaultStatus } from "../api.js"; +import type { VaultEvent } from "../useEvents.js"; + +export function StatusPage({ + revision = 0, + lastEvent, +}: { + revision?: number; + lastEvent?: VaultEvent | null; +}) { + const [status, setStatus] = useState(null); + const [error, setError] = useState(null); + const [compiling, setCompiling] = useState(false); + const [compileLog, setCompileLog] = useState([]); + const [compileError, setCompileError] = useState(null); + + // biome-ignore lint/correctness/useExhaustiveDependencies: revision triggers re-fetch on vault changes + useEffect(() => { + api + .getStatus() + .then(setStatus) + .catch((e) => setError((e as Error).message)); + }, [revision]); + + // Handle compile events + useEffect(() => { + if (!lastEvent) return; + if (lastEvent.type === "compile_started") { + setCompiling(true); + setCompileLog([]); + setCompileError(null); + } else if (lastEvent.type === "compile_progress" && lastEvent.message) { + setCompileLog((prev) => [...prev.slice(-19), lastEvent.message!]); + } else if (lastEvent.type === "compile_article" && lastEvent.title) { + setCompileLog((prev) => [ + ...prev.slice(-19), + `${lastEvent.op === "create" ? "+" : "\u2713"} ${lastEvent.title}`, + ]); + } else if (lastEvent.type === "compile_done") { + setCompiling(false); + setCompileLog((prev) => [ + ...prev, + `Done: ${lastEvent.sourcesCompiled} sources \u2192 ${lastEvent.articlesCreated} created, ${lastEvent.articlesUpdated} updated`, + ]); + } else if (lastEvent.type === "compile_error") { + setCompiling(false); + setCompileError(lastEvent.message ?? "Compile failed"); + } + }, [lastEvent]); + + const handleCompile = async () => { + setCompileError(null); + try { + await api.compile(); + } catch (e) { + setCompileError((e as Error).message); + } + }; + + const handleStop = async () => { + try { + await api.stopCompile(); + } catch (e) { + setCompileError((e as Error).message); + } + }; + + if (error) { + return ( +
+

Error: {error}

+
+ ); + } + + if (!status) { + return ( +
+

Loading...

+
+ ); + } + + const uncompiled = status.stats.totalSources - status.stats.totalArticles; + + const cards = [ + { + label: "Articles", + value: status.stats.totalArticles, + icon: BookOpen, + color: "text-blue-500", + }, + { + label: "Sources", + value: status.stats.totalSources, + icon: FileText, + color: "text-green-500", + }, + { + label: "Words", + value: status.stats.totalWords.toLocaleString(), + icon: Database, + color: "text-orange-500", + }, + { + label: "Provider", + value: status.provider.ready ? status.provider.name : "Not configured", + icon: Zap, + color: status.provider.ready ? "text-purple-500" : "text-red-400", + }, + ]; + + return ( +
+
+
+

{status.vault.name}

+

+ Created {new Date(status.vault.created).toLocaleDateString()} + {status.vault.lastCompiled && + ` \u00b7 Last compiled ${new Date(status.vault.lastCompiled).toLocaleDateString()}`} +

+
+ {status.provider.ready && !compiling && ( + + )} + {compiling && ( + + )} +
+ + {compileError && ( +
+

{compileError}

+
+ )} + + {compileLog.length > 0 && ( +
+ {compileLog.map((line) => ( +
+ {line} +
+ ))} + {compiling && ( +
waiting for LLM...
+ )} +
+ )} + + {uncompiled > 0 && !compiling && compileLog.length === 0 && ( +
+

+ {uncompiled} uncompiled source{uncompiled > 1 ? "s" : ""} pending. + {status.provider.ready + ? " Hit Compile to generate wiki articles." + : " Configure a provider to compile."} +

+
+ )} + +
+ {cards.map((card) => ( +
+
+ + + {card.label} + +
+

{card.value}

+
+ ))} +
+ +
+

Configuration

+
+
+ Provider + {status.provider.name} +
+
+ Model + {status.provider.model} +
+
+ API Key + {status.provider.apiKeyHint ?? "Not set"} +
+
+ Status + + {status.provider.ready ? "Ready" : "Not configured"} + +
+
+
+
+ ); +} diff --git a/packages/dashboard/src/client/globals.css b/packages/dashboard/src/client/globals.css new file mode 100644 index 0000000..c034ab2 --- /dev/null +++ b/packages/dashboard/src/client/globals.css @@ -0,0 +1,118 @@ +@import "tailwindcss"; + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Code", monospace; + + --color-background: #fafafa; + --color-foreground: #111111; + --color-muted: #777777; + --color-muted-foreground: #999999; + --color-border: #e5e5e5; + --color-sidebar: #111111; + --color-sidebar-fg: #e0e0e0; + --color-sidebar-hover: #222222; + --color-sidebar-active: #333333; + --color-accent: #2563eb; + --color-accent-light: #dbeafe; + + --color-cat-concept: #3b82f6; + --color-cat-topic: #22c55e; + --color-cat-reference: #f97316; + --color-cat-output: #a855f7; +} + +* { + border-color: var(--color-border); +} + +body { + margin: 0; + background-color: var(--color-background); + color: var(--color-foreground); + font-family: var(--font-mono); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Markdown article content */ +.article-content h1 { + font-size: 1.5rem; + font-weight: 700; + margin: 1.5rem 0 0.75rem; +} +.article-content h2 { + font-size: 1.25rem; + font-weight: 600; + margin: 1.25rem 0 0.5rem; +} +.article-content h3 { + font-size: 1.1rem; + font-weight: 600; + margin: 1rem 0 0.5rem; +} +.article-content p { + margin: 0.5rem 0; + line-height: 1.7; +} +.article-content ul, +.article-content ol { + margin: 0.5rem 0; + padding-left: 1.5rem; +} +.article-content li { + margin: 0.25rem 0; + line-height: 1.6; +} +.article-content pre { + background: #1e1e1e; + color: #d4d4d4; + padding: 1rem; + border-radius: 0.375rem; + overflow-x: auto; + margin: 0.75rem 0; + font-size: 0.85rem; +} +.article-content code { + background: #f0f0f0; + padding: 0.15rem 0.35rem; + border-radius: 0.25rem; + font-size: 0.9em; +} +.article-content pre code { + background: none; + padding: 0; +} +.article-content blockquote { + border-left: 3px solid var(--color-border); + padding-left: 1rem; + color: var(--color-muted); + margin: 0.75rem 0; +} +.article-content a { + color: var(--color-accent); + text-decoration: underline; +} +.article-content hr { + border: none; + border-top: 1px solid var(--color-border); + margin: 1.5rem 0; +} +.article-content img { + max-width: 100%; + border-radius: 0.375rem; +} + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +.animate-fade-in { + animation: fade-in 0.2s ease-out; +} diff --git a/packages/dashboard/src/client/main.tsx b/packages/dashboard/src/client/main.tsx new file mode 100644 index 0000000..8e6cfae --- /dev/null +++ b/packages/dashboard/src/client/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App.js"; +import "./globals.css"; + +const root = document.getElementById("root"); +if (root) { + createRoot(root).render( + + + , + ); +} diff --git a/packages/dashboard/src/client/useEvents.ts b/packages/dashboard/src/client/useEvents.ts new file mode 100644 index 0000000..74ee379 --- /dev/null +++ b/packages/dashboard/src/client/useEvents.ts @@ -0,0 +1,71 @@ +import { useEffect, useRef, useState } from "react"; + +export interface VaultEvent { + type: + | "ingest" + | "compile_started" + | "compile_progress" + | "compile_article" + | "compile_done" + | "compile_error" + | "search_invalidated" + | "error"; + sourceId?: string; + title?: string; + op?: string; + articlesCreated?: number; + articlesUpdated?: number; + sourcesCompiled?: number; + message?: string; +} + +/** + * Subscribe to real-time vault events via SSE. + * Returns a revision counter that increments on every event — use it as a + * dependency in useEffect to trigger re-fetches. + */ +export function useEvents(): { revision: number; lastEvent: VaultEvent | null } { + const [revision, setRevision] = useState(0); + const [lastEvent, setLastEvent] = useState(null); + const retryRef = useRef(0); + + useEffect(() => { + let es: EventSource | null = null; + let closed = false; + + function connect() { + if (closed) return; + es = new EventSource("/api/events"); + + es.onmessage = (e) => { + try { + const event = JSON.parse(e.data) as VaultEvent; + setLastEvent(event); + setRevision((r) => r + 1); + retryRef.current = 0; + } catch { + // ignore malformed events + } + }; + + es.onerror = () => { + es?.close(); + if (!closed) { + // Reconnect with backoff + const delay = Math.min(1000 * 2 ** retryRef.current, 10_000); + retryRef.current++; + setTimeout(connect, delay); + } + }; + } + + connect(); + + return () => { + closed = true; + es?.close(); + }; + }, []); + + return { revision, lastEvent }; +} diff --git a/packages/dashboard/src/server/api.ts b/packages/dashboard/src/server/api.ts new file mode 100644 index 0000000..bd1048e --- /dev/null +++ b/packages/dashboard/src/server/api.ts @@ -0,0 +1,292 @@ +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join, relative } from "node:path"; +import { + buildLinkGraph, + compileVault, + computeStats, + ingestSource, + listRaw, + listWiki, + readRaw, + readWiki, +} from "@kibhq/core"; +import type { DashboardContext } from "./context.js"; +import { emit, handleEventsStream } from "./events.js"; +import { handleQueryStream } from "./stream.js"; + +function json(data: unknown, status = 200): Response { + return new Response(JSON.stringify(data), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +function error(message: string, status = 400): Response { + return json({ error: message }, status); +} + +let compileAbort: AbortController | null = null; + +export async function handleApi(url: URL, req: Request, ctx: DashboardContext): Promise { + const path = url.pathname.replace(/^\/api/, ""); + + try { + // GET /api/events — SSE stream for real-time updates + if (req.method === "GET" && path === "/events") { + return handleEventsStream(); + } + + // GET /api/status + if (req.method === "GET" && path === "/status") { + const manifest = await ctx.getManifest(); + const config = await ctx.getConfig(); + const stats = await computeStats(ctx.root); + + let providerReady = false; + try { + await ctx.getProvider(); + providerReady = true; + } catch { + // Provider not configured + } + + // Read API key and truncate for display + let apiKeyHint: string | null = null; + try { + const credsPath = join(homedir(), ".config", "kib", "credentials"); + const creds = readFileSync(credsPath, "utf-8"); + const providerName = config.provider.default; + const envKey = + providerName === "anthropic" + ? "ANTHROPIC_API_KEY" + : providerName === "openai" + ? "OPENAI_API_KEY" + : null; + if (envKey) { + const match = creds.match(new RegExp(`${envKey}=(.+)`)); + if (match?.[1]) { + const key = match[1].trim(); + apiKeyHint = `${key.slice(0, 10)}...${key.slice(-4)}`; + } + } + } catch { + // No credentials file or env var + const envKey = + config.provider.default === "anthropic" + ? process.env.ANTHROPIC_API_KEY + : config.provider.default === "openai" + ? process.env.OPENAI_API_KEY + : null; + if (envKey) { + apiKeyHint = `${envKey.slice(0, 10)}...${envKey.slice(-4)}`; + } + } + + return json({ + vault: manifest.vault, + root: ctx.root, + stats: { + ...manifest.stats, + ...stats, + }, + provider: { + name: config.provider.default, + model: config.provider.model, + ready: providerReady, + apiKeyHint, + }, + }); + } + + // GET /api/articles?scope=wiki|raw + if (req.method === "GET" && path === "/articles") { + const scope = url.searchParams.get("scope") ?? "wiki"; + const manifest = await ctx.getManifest(); + + if (scope === "raw") { + const files = await listRaw(ctx.root); + const rawDir = join(ctx.root, "raw"); + const items = files.map((f) => { + const rel = relative(rawDir, f); + const sourceId = Object.keys(manifest.sources).find((id) => { + const s = manifest.sources[id]; + return s && (f.endsWith(id) || rel === id || f.includes(id.replace(/\.md$/, ""))); + }); + const meta = sourceId ? manifest.sources[sourceId] : undefined; + return { + path: rel, + title: meta?.metadata?.title ?? rel.replace(/\.md$/, ""), + sourceType: meta?.sourceType, + ingestedAt: meta?.ingestedAt, + wordCount: meta?.metadata?.wordCount, + }; + }); + return json(items); + } + + const files = await listWiki(ctx.root); + const wikiDir = join(ctx.root, "wiki"); + const items = files + .filter((f) => !f.endsWith("INDEX.md") && !f.endsWith("GRAPH.md") && !f.endsWith("LOG.md")) + .map((f) => { + const rel = relative(wikiDir, f); + const slug = rel.replace(/\.md$/, "").split("/").pop() ?? rel; + const meta = manifest.articles[slug]; + return { + path: rel, + slug, + title: meta?.summary ? slug : slug, + category: meta?.category ?? "topic", + tags: meta?.tags ?? [], + summary: meta?.summary ?? "", + wordCount: meta?.wordCount ?? 0, + lastUpdated: meta?.lastUpdated, + }; + }); + return json(items); + } + + // GET /api/articles/* — read a single file + if (req.method === "GET" && path.startsWith("/articles/")) { + const filePath = path.replace("/articles/", ""); + const scope = url.searchParams.get("scope") ?? "wiki"; + + const content = + scope === "raw" ? await readRaw(ctx.root, filePath) : await readWiki(ctx.root, filePath); + return json({ path: filePath, content }); + } + + // GET /api/search?q=...&limit=...&tag=...&since=... + if (req.method === "GET" && path === "/search") { + const q = url.searchParams.get("q"); + if (!q) return error("Missing query parameter: q"); + + const limit = Number.parseInt(url.searchParams.get("limit") ?? "20", 10); + const tag = url.searchParams.get("tag") ?? undefined; + const since = url.searchParams.get("since") ?? undefined; + const scope = (url.searchParams.get("scope") as "wiki" | "raw" | "all") ?? "all"; + + const index = await ctx.getSearchIndex(); + const results = index.search(q, { limit, tag, since, scope }); + + const wikiDir = join(ctx.root, "wiki"); + const rawDir = join(ctx.root, "raw"); + const items = results.map((r) => ({ + ...r, + path: r.path.startsWith(wikiDir) + ? relative(wikiDir, r.path) + : r.path.startsWith(rawDir) + ? relative(rawDir, r.path) + : r.path, + scope: r.path.startsWith(wikiDir) ? "wiki" : "raw", + })); + return json(items); + } + + // POST /api/query — SSE streaming + if (req.method === "POST" && path === "/query") { + const body = (await req.json()) as { + question: string; + maxArticles?: number; + source?: string; + }; + if (!body.question) return error("Missing field: question"); + return handleQueryStream(ctx, body); + } + + // POST /api/ingest + if (req.method === "POST" && path === "/ingest") { + const body = (await req.json()) as { url?: string; content?: string; title?: string }; + const source = body.url ?? body.content; + if (!source) return error("Missing field: url or content"); + + const result = await ingestSource(ctx.root, source, { + tags: [], + }); + ctx.invalidateSearch(); + if (!result.skipped) { + emit({ type: "ingest", sourceId: result.sourceId, title: result.title }); + } + return json(result); + } + + // POST /api/compile — runs in background, streams progress via SSE events + if (req.method === "POST" && path === "/compile") { + if (compileAbort) return error("Compile already running", 409); + + const provider = await ctx.getProvider(); + const config = await ctx.getConfig(); + const body = (await req.json().catch(() => ({}))) as { force?: boolean }; + + compileAbort = new AbortController(); + emit({ type: "compile_started" }); + + compileVault(ctx.root, provider, config, { + force: body.force ?? false, + signal: compileAbort.signal, + onProgress: (msg) => emit({ type: "compile_progress", message: msg }), + onArticle: (event) => emit({ type: "compile_article", op: event.op, title: event.title }), + }) + .then((result) => { + ctx.invalidateSearch(); + emit({ + type: "compile_done", + articlesCreated: result.articlesCreated, + articlesUpdated: result.articlesUpdated, + sourcesCompiled: result.sourcesCompiled, + }); + }) + .catch((err) => { + emit({ type: "compile_error", message: (err as Error).message }); + }) + .finally(() => { + compileAbort = null; + }); + + return json({ started: true }); + } + + // POST /api/compile/stop + if (req.method === "POST" && path === "/compile/stop") { + if (!compileAbort) return json({ stopped: false, reason: "No compile running" }); + compileAbort.abort(); + compileAbort = null; + emit({ type: "compile_done", articlesCreated: 0, articlesUpdated: 0, sourcesCompiled: 0 }); + return json({ stopped: true }); + } + + // GET /api/graph + if (req.method === "GET" && path === "/graph") { + const manifest = await ctx.getManifest(); + const graph = await buildLinkGraph(ctx.root); + + const nodes = [...new Set([...graph.forwardLinks.keys(), ...graph.backlinks.keys()])].map( + (slug) => { + const meta = manifest.articles[slug]; + return { + id: slug, + category: meta?.category ?? "topic", + tags: meta?.tags ?? [], + wordCount: meta?.wordCount ?? 0, + summary: meta?.summary ?? "", + }; + }, + ); + + const edges: { source: string; target: string }[] = []; + for (const [slug, targets] of graph.forwardLinks) { + for (const target of targets) { + edges.push({ source: slug, target }); + } + } + + return json({ nodes, edges }); + } + + return error("Not found", 404); + } catch (err) { + console.error("API error:", err); + return error((err as Error).message, 500); + } +} diff --git a/packages/dashboard/src/server/context.ts b/packages/dashboard/src/server/context.ts new file mode 100644 index 0000000..9cb249f --- /dev/null +++ b/packages/dashboard/src/server/context.ts @@ -0,0 +1,56 @@ +import { + createProvider, + type LLMProvider, + loadConfig, + loadManifest, + type Manifest, + SearchIndex, + type VaultConfig, +} from "@kibhq/core"; + +export interface DashboardContext { + root: string; + getConfig(): Promise; + getManifest(): Promise; + getProvider(): Promise; + getSearchIndex(): Promise; + invalidateSearch(): void; +} + +export function createContext(root: string): DashboardContext { + let cachedConfig: VaultConfig | null = null; + let cachedProvider: LLMProvider | null = null; + let cachedIndex: SearchIndex | null = null; + + return { + root, + async getConfig() { + if (!cachedConfig) cachedConfig = await loadConfig(root); + return cachedConfig; + }, + async getManifest() { + return loadManifest(root); + }, + async getProvider() { + if (!cachedProvider) { + const config = await this.getConfig(); + cachedProvider = await createProvider(config.provider.default, config.provider.model); + } + return cachedProvider; + }, + async getSearchIndex() { + if (!cachedIndex) { + cachedIndex = new SearchIndex(); + const loaded = await cachedIndex.load(root); + if (!loaded) { + await cachedIndex.build(root, "all"); + await cachedIndex.save(root); + } + } + return cachedIndex; + }, + invalidateSearch() { + cachedIndex = null; + }, + }; +} diff --git a/packages/dashboard/src/server/events.ts b/packages/dashboard/src/server/events.ts new file mode 100644 index 0000000..925f14f --- /dev/null +++ b/packages/dashboard/src/server/events.ts @@ -0,0 +1,69 @@ +export type VaultEvent = + | { type: "ingest"; sourceId: string; title: string } + | { type: "compile_started" } + | { type: "compile_progress"; message: string } + | { type: "compile_article"; op: string; title: string } + | { + type: "compile_done"; + articlesCreated: number; + articlesUpdated: number; + sourcesCompiled: number; + } + | { type: "compile_error"; message: string } + | { type: "search_invalidated" } + | { type: "error"; message: string }; + +type Listener = (event: VaultEvent) => void; + +const listeners = new Set(); + +export function emit(event: VaultEvent) { + for (const listener of listeners) { + listener(event); + } +} + +export function subscribe(listener: Listener): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +export function handleEventsStream(): Response { + const encoder = new TextEncoder(); + let unsubscribe: (() => void) | null = null; + let keepalive: ReturnType | null = null; + + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(": connected\n\n")); + + unsubscribe = subscribe((event) => { + try { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); + } catch { + // Client disconnected + } + }); + + keepalive = setInterval(() => { + try { + controller.enqueue(encoder.encode(": keepalive\n\n")); + } catch { + // Client disconnected + } + }, 30_000); + }, + cancel() { + unsubscribe?.(); + if (keepalive) clearInterval(keepalive); + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); +} diff --git a/packages/dashboard/src/server/index.ts b/packages/dashboard/src/server/index.ts new file mode 100644 index 0000000..5076270 --- /dev/null +++ b/packages/dashboard/src/server/index.ts @@ -0,0 +1,73 @@ +import { existsSync } from "node:fs"; +import { extname, join } from "node:path"; +import { handleApi } from "./api.js"; +import { createContext } from "./context.js"; + +const MIME_TYPES: Record = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", +}; + +export async function startServer( + root: string, + port: number, +): Promise<{ url: string; stop: () => void }> { + const ctx = createContext(root); + + // Resolve the built client assets directory + const distDir = join(import.meta.dir, "../../dist"); + const hasBuiltAssets = existsSync(distDir); + + const server = Bun.serve({ + port, + async fetch(req) { + const url = new URL(req.url); + + // API routes + if (url.pathname.startsWith("/api/")) { + return handleApi(url, req, ctx); + } + + // Serve built static assets + if (hasBuiltAssets) { + const filePath = + url.pathname === "/" ? join(distDir, "index.html") : join(distDir, url.pathname); + + const file = Bun.file(filePath); + if (await file.exists()) { + const ext = extname(filePath); + const contentType = MIME_TYPES[ext]; + return new Response( + file, + contentType ? { headers: { "Content-Type": contentType } } : undefined, + ); + } + + // SPA fallback — serve index.html for client-side routing + return new Response(Bun.file(join(distDir, "index.html")), { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + + // No built assets — show helpful message + return new Response("Dashboard not built. Run: bun run --filter @kibhq/dashboard build", { + status: 503, + headers: { "Content-Type": "text/plain" }, + }); + }, + }); + + const serverUrl = `http://localhost:${server.port}`; + return { + url: serverUrl, + stop: () => server.stop(), + }; +} diff --git a/packages/dashboard/src/server/stream.ts b/packages/dashboard/src/server/stream.ts new file mode 100644 index 0000000..f313b34 --- /dev/null +++ b/packages/dashboard/src/server/stream.ts @@ -0,0 +1,44 @@ +import { queryVault } from "@kibhq/core"; +import type { DashboardContext } from "./context.js"; + +export async function handleQueryStream( + ctx: DashboardContext, + body: { question: string; maxArticles?: number; source?: string }, +): Promise { + const provider = await ctx.getProvider(); + + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + async start(controller) { + try { + const result = await queryVault(ctx.root, body.question, provider, { + onChunk(text: string) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`)); + }, + maxArticles: body.maxArticles, + source: body.source, + }); + + controller.enqueue( + encoder.encode( + `data: ${JSON.stringify({ done: true, sourcePaths: result.sourcePaths, usage: result.usage })}\n\n`, + ), + ); + controller.close(); + } catch (err) { + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ error: (err as Error).message })}\n\n`), + ); + controller.close(); + } + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); +} diff --git a/packages/dashboard/tsconfig.json b/packages/dashboard/tsconfig.json new file mode 100644 index 0000000..d605a7f --- /dev/null +++ b/packages/dashboard/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["bun-types"] + }, + "include": ["src"] +} diff --git a/packages/dashboard/vite.config.ts b/packages/dashboard/vite.config.ts new file mode 100644 index 0000000..deb5ea4 --- /dev/null +++ b/packages/dashboard/vite.config.ts @@ -0,0 +1,16 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + root: ".", + build: { + outDir: "dist", + emptyOutDir: true, + }, + server: { + proxy: { + "/api": "http://localhost:4848", + }, + }, +});