diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index 7d0c29f7..be1aa3e8 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -10,13 +10,23 @@ Registry personal de codigo reutilizable con busqueda FTS. Diseñado para compos
---
-## Explorar el registry (USAR SIEMPRE)
+## Explorar el registry (OBLIGATORIO)
-Antes de escribir codigo, SIEMPRE consulta registry.db para evitar duplicados y descubrir funciones reutilizables.
+**SIEMPRE** consulta registry.db antes de escribir codigo, crear funciones, o responder sobre el registry. No uses grep/glob sobre archivos .go/.md — la BD es la fuente de verdad.
+
+**La BD contiene el codigo y la documentacion completa** de cada funcion y tipo en los campos `code`, `documentation` y `notes`. Estos campos tambien estan indexados en FTS5, asi que puedes buscar dentro del codigo y la documentacion directamente. Para leer el codigo de una funcion: `SELECT code FROM functions WHERE id = '...'`. Para leer su documentacion: `SELECT documentation FROM functions WHERE id = '...'`.
+
+**Busquedas FTS5 obligatorias:** Usa SIEMPRE la tabla FTS5 para buscar tanto por `name` como por `description`. Esto encuentra coincidencias parciales y similares que una busqueda exacta perderia. Usa operadores FTS5: `OR` para ampliar, `*` para prefijos, `NEAR` para proximidad.
```bash
-# FTS5
-sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'slice') ORDER BY name;"
+# Busqueda FTS5 por nombre Y descripcion (USAR SIEMPRE ESTE PATRON)
+sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slice OR description:slice') ORDER BY name;"
+
+# FTS5 con prefijo (encuentra slice, slicing, sliced...)
+sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slic* OR description:slic*') ORDER BY name;"
+
+# FTS5 en tipos
+sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:result OR description:result') ORDER BY name;"
# Por dominio
sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'finance' ORDER BY name;"
@@ -24,7 +34,7 @@ sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain =
# Puras de un dominio
sqlite3 registry.db "SELECT id, signature FROM functions WHERE domain = 'core' AND purity = 'pure' ORDER BY name;"
-# Tipos
+# Tipos por dominio
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'cybersecurity';"
# Dependencias
@@ -37,6 +47,8 @@ sqlite3 registry.db "SELECT id, kind, status, title FROM proposals WHERE status
sqlite3 registry.db ".schema"
```
+**Regla:** Si necesitas saber si algo existe o hay algo similar, haz la consulta FTS5 sobre la BD. No asumas que no existe sin consultar primero.
+
---
## Estructura
@@ -45,8 +57,10 @@ sqlite3 registry.db ".schema"
fn-registry/
functions/{domain}/ # .go + .md por funcion (core, finance, datascience, cybersecurity)
functions/pipelines/ # Composiciones, siempre impuras
- functions/components/ # React (.tsx)
types/{domain}/ # .go + .md por tipo
+ frontend/ # pnpm + vite + react + tailwind + shadcn
+ frontend/functions/ # .tsx/.ts + .md (core para TS puro, ui para componentes React)
+ frontend/types/ # .ts + .md por tipo
registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones
fn_operations/ # Paquete Go: operations database (libreria)
apps/ # Apps ejecutables (TUIs, CLIs) — modulos Go independientes, cada una con su operations.db
diff --git a/cmd/fn/main.go b/cmd/fn/main.go
index 497ecead..cfa6a068 100644
--- a/cmd/fn/main.go
+++ b/cmd/fn/main.go
@@ -289,6 +289,15 @@ func printFunction(f *registry.Function) {
if f.Example != "" {
fmt.Printf("\nExample:\n%s\n", f.Example)
}
+ if f.Notes != "" {
+ fmt.Printf("\nNotes:\n%s\n", f.Notes)
+ }
+ if f.Documentation != "" {
+ fmt.Printf("\nDocumentation:\n%s\n", f.Documentation)
+ }
+ if f.Code != "" {
+ fmt.Printf("\nCode:\n%s\n", f.Code)
+ }
if f.Kind == registry.KindComponent {
fmt.Printf("Framework: %s\n", f.Framework)
if f.HasState != nil {
@@ -316,6 +325,18 @@ func printType(t *registry.Type) {
if t.Definition != "" {
fmt.Printf("\nDefinition:\n%s\n", t.Definition)
}
+ if t.Examples != "" {
+ fmt.Printf("\nExamples:\n%s\n", t.Examples)
+ }
+ if t.Notes != "" {
+ fmt.Printf("\nNotes:\n%s\n", t.Notes)
+ }
+ if t.Documentation != "" {
+ fmt.Printf("\nDocumentation:\n%s\n", t.Documentation)
+ }
+ if t.Code != "" {
+ fmt.Printf("\nCode:\n%s\n", t.Code)
+ }
}
// --- add ---
diff --git a/docs/templates/component.md b/docs/templates/component.md
index 7cfcff70..b1d97ec8 100644
--- a/docs/templates/component.md
+++ b/docs/templates/component.md
@@ -17,7 +17,7 @@ imports: [react]
tested: false
tests: []
test_file_path: ""
-file_path: "functions/components/DataTable.tsx"
+file_path: "frontend/functions/ui/data_table.tsx"
props:
- name: data
type: "T[]"
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 00000000..b9470778
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+dist/
diff --git a/frontend/components.json b/frontend/components.json
new file mode 100644
index 00000000..6167d26f
--- /dev/null
+++ b/frontend/components.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "base-nova",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "rtl": false,
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "menuColor": "default",
+ "menuAccent": "subtle",
+ "registries": {}
+}
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 00000000..143fb45d
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ fn-registry frontend
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 00000000..0de59957
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "frontend",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "add": "pnpm dlx shadcn@latest add"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "packageManager": "pnpm@10.17.0",
+ "dependencies": {
+ "@base-ui/react": "^1.3.0",
+ "@fontsource-variable/geist": "^5.2.8",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.7.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "shadcn": "^4.1.1",
+ "tailwind-merge": "^3.5.0",
+ "tw-animate-css": "^1.4.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.2.2",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "tailwindcss": "^4.2.2",
+ "typescript": "^6.0.2",
+ "vite": "^8.0.3"
+ }
+}
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
new file mode 100644
index 00000000..bdb9c5eb
--- /dev/null
+++ b/frontend/pnpm-lock.yaml
@@ -0,0 +1,3609 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@base-ui/react':
+ specifier: ^1.3.0
+ version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@fontsource-variable/geist':
+ specifier: ^5.2.8
+ version: 5.2.8
+ class-variance-authority:
+ specifier: ^0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ lucide-react:
+ specifier: ^1.7.0
+ version: 1.7.0(react@19.2.4)
+ react:
+ specifier: ^19.2.4
+ version: 19.2.4
+ react-dom:
+ specifier: ^19.2.4
+ version: 19.2.4(react@19.2.4)
+ shadcn:
+ specifier: ^4.1.1
+ version: 4.1.1(typescript@6.0.2)
+ tailwind-merge:
+ specifier: ^3.5.0
+ version: 3.5.0
+ tw-animate-css:
+ specifier: ^1.4.0
+ version: 1.4.0
+ devDependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.2.2
+ version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))
+ '@types/react':
+ specifier: ^19.2.14
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.14)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.1
+ version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))
+ tailwindcss:
+ specifier: ^4.2.2
+ version: 4.2.2
+ typescript:
+ specifier: ^6.0.2
+ version: 6.0.2
+ vite:
+ specifier: ^8.0.3
+ version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)
+
+packages:
+
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.0':
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.28.6':
+ resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-replace-supers@7.28.6':
+ resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.2':
+ resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.2':
+ resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-syntax-jsx@7.28.6':
+ resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-typescript@7.28.6':
+ resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6':
+ resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typescript@7.28.6':
+ resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-typescript@7.28.5':
+ resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/runtime@7.29.2':
+ resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
+ '@base-ui/react@1.3.0':
+ resolution: {integrity: sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@types/react': ^17 || ^18 || ^19
+ react: ^17 || ^18 || ^19
+ react-dom: ^17 || ^18 || ^19
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@base-ui/utils@0.2.6':
+ resolution: {integrity: sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw==}
+ peerDependencies:
+ '@types/react': ^17 || ^18 || ^19
+ react: ^17 || ^18 || ^19
+ react-dom: ^17 || ^18 || ^19
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@dotenvx/dotenvx@1.59.0':
+ resolution: {integrity: sha512-+LthcJBVj18x5B1Quua4XditjxYdafOmrXT6xxq+wnUldFQ41hfv/vrP/Z4CZkAk1OTdQSqgFIlVc/pUU+pIzQ==}
+ hasBin: true
+
+ '@ecies/ciphers@0.2.5':
+ resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==}
+ engines: {bun: '>=1', deno: '>=2', node: '>=16'}
+ peerDependencies:
+ '@noble/ciphers': ^1.0.0
+
+ '@emnapi/core@1.9.1':
+ resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
+
+ '@emnapi/runtime@1.9.1':
+ resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
+
+ '@emnapi/wasi-threads@1.2.0':
+ resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
+
+ '@floating-ui/core@1.7.5':
+ resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
+
+ '@floating-ui/dom@1.7.6':
+ resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
+
+ '@floating-ui/react-dom@2.1.8':
+ resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/utils@0.2.11':
+ resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+
+ '@fontsource-variable/geist@5.2.8':
+ resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==}
+
+ '@hono/node-server@1.19.11':
+ resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
+ engines: {node: '>=18.14.1'}
+ peerDependencies:
+ hono: ^4
+
+ '@inquirer/ansi@1.0.2':
+ resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==}
+ engines: {node: '>=18'}
+
+ '@inquirer/confirm@5.1.21':
+ resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/core@10.3.2':
+ resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/figures@1.0.15':
+ resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==}
+ engines: {node: '>=18'}
+
+ '@inquirer/type@3.0.10':
+ resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@modelcontextprotocol/sdk@1.28.0':
+ resolution: {integrity: sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@cfworker/json-schema': ^4.1.1
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ '@cfworker/json-schema':
+ optional: true
+
+ '@mswjs/interceptors@0.41.3':
+ resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==}
+ engines: {node: '>=18'}
+
+ '@napi-rs/wasm-runtime@1.1.2':
+ resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@noble/ciphers@1.3.0':
+ resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@noble/curves@1.9.7':
+ resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@noble/hashes@1.8.0':
+ resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@open-draft/deferred-promise@2.2.0':
+ resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
+
+ '@open-draft/logger@0.3.0':
+ resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==}
+
+ '@open-draft/until@2.1.0':
+ resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+
+ '@oxc-project/types@0.122.0':
+ resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.12':
+ resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.12':
+ resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.12':
+ resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.12':
+ resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
+ resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
+ resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
+ resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
+ resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
+ resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
+ resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
+ resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
+ resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
+ resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
+ resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
+ resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.0-rc.12':
+ resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==}
+
+ '@rolldown/pluginutils@1.0.0-rc.7':
+ resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
+
+ '@sec-ant/readable-stream@0.4.1':
+ resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
+
+ '@sindresorhus/merge-streams@4.0.0':
+ resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
+ engines: {node: '>=18'}
+
+ '@tailwindcss/node@4.2.2':
+ resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
+
+ '@tailwindcss/oxide-android-arm64@4.2.2':
+ resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.2':
+ resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.2.2':
+ resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.2':
+ resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
+ resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
+ resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
+ resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
+ resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.2':
+ resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.2':
+ resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
+ resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.2':
+ resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.2.2':
+ resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.2.2':
+ resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@ts-morph/common@0.27.0':
+ resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
+
+ '@tybys/wasm-util@0.10.1':
+ resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.14':
+ resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
+
+ '@types/statuses@2.0.6':
+ resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
+
+ '@types/validate-npm-package-name@4.0.2':
+ resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
+
+ '@vitejs/plugin-react@6.0.1':
+ resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
+ babel-plugin-react-compiler: ^1.0.0
+ vite: ^8.0.0
+ peerDependenciesMeta:
+ '@rolldown/plugin-babel':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+
+ accepts@2.0.0:
+ resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
+ engines: {node: '>= 0.6'}
+
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ ajv-formats@3.0.1:
+ resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
+ peerDependencies:
+ ajv: ^8.0.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+
+ ajv@8.18.0:
+ resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ ast-types@0.16.1:
+ resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
+ engines: {node: '>=4'}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ baseline-browser-mapping@2.10.12:
+ resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ body-parser@2.2.2:
+ resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
+ engines: {node: '>=18'}
+
+ brace-expansion@5.0.5:
+ resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
+ engines: {node: 18 || 20 || >=22}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ bundle-name@4.1.0:
+ resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
+ engines: {node: '>=18'}
+
+ bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001781:
+ resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==}
+
+ chalk@5.6.2:
+ resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
+ cli-cursor@5.0.0:
+ resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
+ engines: {node: '>=18'}
+
+ cli-spinners@2.9.2:
+ resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
+ engines: {node: '>=6'}
+
+ cli-width@4.1.0:
+ resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
+ engines: {node: '>= 12'}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ code-block-writer@13.0.3:
+ resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ commander@11.1.0:
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
+ engines: {node: '>=16'}
+
+ commander@14.0.3:
+ resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
+ engines: {node: '>=20'}
+
+ content-disposition@1.0.1:
+ resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
+ engines: {node: '>=18'}
+
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie-signature@1.2.2:
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
+ engines: {node: '>=6.6.0'}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ cors@2.8.6:
+ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
+ engines: {node: '>= 0.10'}
+
+ cosmiconfig@9.0.1:
+ resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ data-uri-to-buffer@4.0.1:
+ resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
+ engines: {node: '>= 12'}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ dedent@1.7.2:
+ resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==}
+ peerDependencies:
+ babel-plugin-macros: ^3.1.0
+ peerDependenciesMeta:
+ babel-plugin-macros:
+ optional: true
+
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
+ default-browser-id@5.0.1:
+ resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
+ engines: {node: '>=18'}
+
+ default-browser@5.5.0:
+ resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
+ engines: {node: '>=18'}
+
+ define-lazy-prop@3.0.0:
+ resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
+ engines: {node: '>=12'}
+
+ depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ diff@8.0.4:
+ resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
+ engines: {node: '>=0.3.1'}
+
+ dotenv@17.3.1:
+ resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
+ engines: {node: '>=12'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ eciesjs@0.4.18:
+ resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==}
+ engines: {bun: '>=1', deno: '>=2', node: '>=16'}
+
+ ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
+ electron-to-chromium@1.5.328:
+ resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==}
+
+ emoji-regex@10.6.0:
+ resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ encodeurl@2.0.0:
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+ engines: {node: '>= 0.8'}
+
+ enhanced-resolve@5.20.1:
+ resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
+ engines: {node: '>=10.13.0'}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+
+ eventsource-parser@3.0.6:
+ resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
+ engines: {node: '>=18.0.0'}
+
+ eventsource@3.0.7:
+ resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
+ engines: {node: '>=18.0.0'}
+
+ execa@5.1.1:
+ resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
+ engines: {node: '>=10'}
+
+ execa@9.6.1:
+ resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
+ engines: {node: ^18.19.0 || >=20.5.0}
+
+ express-rate-limit@8.3.1:
+ resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ express: '>= 4.11'
+
+ express@5.2.1:
+ resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
+ engines: {node: '>= 18'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-uri@3.1.0:
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+
+ fastq@1.20.1:
+ resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fetch-blob@3.2.0:
+ resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
+ engines: {node: ^12.20 || >= 14.13}
+
+ figures@6.1.0:
+ resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
+ engines: {node: '>=18'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ finalhandler@2.1.1:
+ resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
+ engines: {node: '>= 18.0.0'}
+
+ formdata-polyfill@4.0.10:
+ resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
+ engines: {node: '>=12.20.0'}
+
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
+ fresh@2.0.0:
+ resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
+ engines: {node: '>= 0.8'}
+
+ fs-extra@11.3.4:
+ resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
+ engines: {node: '>=14.14'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ fuzzysort@3.1.0:
+ resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-east-asian-width@1.5.0:
+ resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
+ engines: {node: '>=18'}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-own-enumerable-keys@1.0.0:
+ resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==}
+ engines: {node: '>=14.16'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-stream@6.0.1:
+ resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
+ engines: {node: '>=10'}
+
+ get-stream@9.0.1:
+ resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
+ engines: {node: '>=18'}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ graphql@16.13.2:
+ resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==}
+ engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ headers-polyfill@4.0.3:
+ resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
+
+ hono@4.12.9:
+ resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==}
+ engines: {node: '>=16.9.0'}
+
+ http-errors@2.0.1:
+ resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
+ engines: {node: '>= 0.8'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ human-signals@2.1.0:
+ resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
+ engines: {node: '>=10.17.0'}
+
+ human-signals@8.0.1:
+ resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
+ engines: {node: '>=18.18.0'}
+
+ iconv-lite@0.7.2:
+ resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
+ engines: {node: '>=0.10.0'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ip-address@10.1.0:
+ resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
+ engines: {node: '>= 12'}
+
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+ is-docker@3.0.0:
+ resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ hasBin: true
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-in-ssh@1.0.0:
+ resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
+ engines: {node: '>=20'}
+
+ is-inside-container@1.0.0:
+ resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
+ engines: {node: '>=14.16'}
+ hasBin: true
+
+ is-interactive@2.0.0:
+ resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
+ engines: {node: '>=12'}
+
+ is-node-process@1.2.0:
+ resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-obj@3.0.0:
+ resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==}
+ engines: {node: '>=12'}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ is-promise@4.0.0:
+ resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+
+ is-regexp@3.1.0:
+ resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==}
+ engines: {node: '>=12'}
+
+ is-stream@2.0.1:
+ resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+ engines: {node: '>=8'}
+
+ is-stream@4.0.1:
+ resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
+ engines: {node: '>=18'}
+
+ is-unicode-supported@1.3.0:
+ resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
+ engines: {node: '>=12'}
+
+ is-unicode-supported@2.1.0:
+ resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
+ engines: {node: '>=18'}
+
+ is-wsl@3.1.1:
+ resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
+ engines: {node: '>=16'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ isexe@3.1.5:
+ resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==}
+ engines: {node: '>=18'}
+
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
+ jose@6.2.2:
+ resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json-schema-typed@8.0.2:
+ resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+
+ kleur@3.0.3:
+ resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
+ engines: {node: '>=6'}
+
+ kleur@4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ log-symbols@6.0.0:
+ resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
+ engines: {node: '>=18'}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ lucide-react@1.7.0:
+ resolution: {integrity: sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ media-typer@1.1.0:
+ resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
+ engines: {node: '>= 0.8'}
+
+ merge-descriptors@2.0.0:
+ resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
+ engines: {node: '>=18'}
+
+ merge-stream@2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.54.0:
+ resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@3.0.2:
+ resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
+ engines: {node: '>=18'}
+
+ mimic-fn@2.1.0:
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ engines: {node: '>=6'}
+
+ mimic-function@5.0.1:
+ resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
+ engines: {node: '>=18'}
+
+ minimatch@10.2.4:
+ resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
+ engines: {node: 18 || 20 || >=22}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ msw@2.12.14:
+ resolution: {integrity: sha512-4KXa4nVBIBjbDbd7vfQNuQ25eFxug0aropCQFoI0JdOBuJWamkT1yLVIWReFI8SiTRc+H1hKzaNk+cLk2N9rtQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ typescript: '>= 4.8.x'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ mute-stream@2.0.0:
+ resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ negotiator@1.0.0:
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+ engines: {node: '>= 0.6'}
+
+ node-domexception@1.0.0:
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+ engines: {node: '>=10.5.0'}
+ deprecated: Use your platform's native DOMException instead
+
+ node-fetch@3.3.2:
+ resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ node-releases@2.0.36:
+ resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
+
+ npm-run-path@4.0.1:
+ resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
+ engines: {node: '>=8'}
+
+ npm-run-path@6.0.0:
+ resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
+ engines: {node: '>=18'}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-treeify@1.1.33:
+ resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==}
+ engines: {node: '>= 10'}
+
+ on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ onetime@5.1.2:
+ resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+ engines: {node: '>=6'}
+
+ onetime@7.0.0:
+ resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
+ engines: {node: '>=18'}
+
+ open@11.0.0:
+ resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
+ engines: {node: '>=20'}
+
+ ora@8.2.0:
+ resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
+ engines: {node: '>=18'}
+
+ outvariant@1.4.3:
+ resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ parse-ms@4.0.0:
+ resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
+ engines: {node: '>=18'}
+
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
+
+ path-to-regexp@6.3.0:
+ resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+
+ path-to-regexp@8.4.0:
+ resolution: {integrity: sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.2:
+ resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ pkce-challenge@5.0.1:
+ resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
+ engines: {node: '>=16.20.0'}
+
+ postcss-selector-parser@7.1.1:
+ resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.8:
+ resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ powershell-utils@0.1.0:
+ resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
+ engines: {node: '>=20'}
+
+ pretty-ms@9.3.0:
+ resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
+ engines: {node: '>=18'}
+
+ prompts@2.4.2:
+ resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
+ engines: {node: '>= 6'}
+
+ proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+
+ qs@6.15.0:
+ resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
+ engines: {node: '>=0.6'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+
+ raw-body@3.0.2:
+ resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
+ engines: {node: '>= 0.10'}
+
+ react-dom@19.2.4:
+ resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
+ peerDependencies:
+ react: ^19.2.4
+
+ react@19.2.4:
+ resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
+ engines: {node: '>=0.10.0'}
+
+ recast@0.23.11:
+ resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
+ engines: {node: '>= 4'}
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ reselect@5.1.1:
+ resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ restore-cursor@5.1.0:
+ resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
+ engines: {node: '>=18'}
+
+ rettime@0.10.1:
+ resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rolldown@1.0.0-rc.12:
+ resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ router@2.2.0:
+ resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
+ engines: {node: '>= 18'}
+
+ run-applescript@7.1.0:
+ resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
+ engines: {node: '>=18'}
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ send@1.2.1:
+ resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
+ engines: {node: '>= 18'}
+
+ serve-static@2.2.1:
+ resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
+ engines: {node: '>= 18'}
+
+ setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+
+ shadcn@4.1.1:
+ resolution: {integrity: sha512-nBj+7LYC9kzV9v9QmRPpoOhfW4KctJVQejywdAt/K+K+z4RYlJOcO2a4AaF7elrRWkfCbgXeGK02liV0KB9HvQ==}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ sisteransi@1.0.5:
+ resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ statuses@2.0.2:
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
+ engines: {node: '>= 0.8'}
+
+ stdin-discarder@0.2.2:
+ resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
+ engines: {node: '>=18'}
+
+ strict-event-emitter@0.5.1:
+ resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@7.2.0:
+ resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+ engines: {node: '>=18'}
+
+ stringify-object@5.0.0:
+ resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==}
+ engines: {node: '>=14.16'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
+ strip-bom@3.0.0:
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+ engines: {node: '>=4'}
+
+ strip-final-newline@2.0.0:
+ resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
+ engines: {node: '>=6'}
+
+ strip-final-newline@4.0.0:
+ resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
+ engines: {node: '>=18'}
+
+ tabbable@6.4.0:
+ resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
+
+ tagged-tag@1.0.0:
+ resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
+ engines: {node: '>=20'}
+
+ tailwind-merge@3.5.0:
+ resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
+
+ tailwindcss@4.2.2:
+ resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
+
+ tapable@2.3.2:
+ resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
+ engines: {node: '>=6'}
+
+ tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tldts-core@7.0.27:
+ resolution: {integrity: sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==}
+
+ tldts@7.0.27:
+ resolution: {integrity: sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==}
+ hasBin: true
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+
+ tough-cookie@6.0.1:
+ resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
+ engines: {node: '>=16'}
+
+ ts-morph@26.0.0:
+ resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==}
+
+ tsconfig-paths@4.2.0:
+ resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
+ engines: {node: '>=6'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ tw-animate-css@1.4.0:
+ resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
+
+ type-fest@5.5.0:
+ resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==}
+ engines: {node: '>=20'}
+
+ type-is@2.0.1:
+ resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
+ engines: {node: '>= 0.6'}
+
+ typescript@6.0.2:
+ resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ unicorn-magic@0.3.0:
+ resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
+ engines: {node: '>=18'}
+
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
+ until-async@3.0.2:
+ resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ validate-npm-package-name@7.0.2:
+ resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
+ vite@8.0.3:
+ resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.0
+ esbuild: ^0.27.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ web-streams-polyfill@3.3.3:
+ resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+ engines: {node: '>= 8'}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ which@4.0.0:
+ resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
+ engines: {node: ^16.13.0 || >=18.0.0}
+ hasBin: true
+
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ wsl-utils@0.3.1:
+ resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
+ engines: {node: '>=20'}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
+ yoctocolors-cjs@2.1.3:
+ resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
+ engines: {node: '>=18'}
+
+ yoctocolors@2.1.2:
+ resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
+ engines: {node: '>=18'}
+
+ zod-to-json-schema@3.25.2:
+ resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==}
+ peerDependencies:
+ zod: ^3.25.28 || ^4
+
+ zod@3.25.76:
+ resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+
+snapshots:
+
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.0': {}
+
+ '@babel/core@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.29.2
+ '@babel/parser': 7.29.2
+ '@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.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.1':
+ dependencies:
+ '@babel/parser': 7.29.2
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.0
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.1
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/traverse': 7.29.0
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-plugin-utils@7.28.6': {}
+
+ '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.29.2':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+
+ '@babel/parser@7.29.2':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/runtime@7.29.2': {}
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.2
+ '@babel/types': 7.29.0
+
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.2
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@base-ui/react@1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@babel/runtime': 7.29.2
+ '@base-ui/utils': 0.2.6(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@floating-ui/utils': 0.2.11
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ tabbable: 6.4.0
+ use-sync-external-store: 1.6.0(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@base-ui/utils@0.2.6(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@babel/runtime': 7.29.2
+ '@floating-ui/utils': 0.2.11
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ reselect: 5.1.1
+ use-sync-external-store: 1.6.0(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@dotenvx/dotenvx@1.59.0':
+ dependencies:
+ commander: 11.1.0
+ dotenv: 17.3.1
+ eciesjs: 0.4.18
+ execa: 5.1.1
+ fdir: 6.5.0(picomatch@4.0.4)
+ ignore: 5.3.2
+ object-treeify: 1.1.33
+ picomatch: 4.0.4
+ which: 4.0.0
+
+ '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)':
+ dependencies:
+ '@noble/ciphers': 1.3.0
+
+ '@emnapi/core@1.9.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.0
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.9.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@floating-ui/core@1.7.5':
+ dependencies:
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/dom@1.7.6':
+ dependencies:
+ '@floating-ui/core': 1.7.5
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ '@floating-ui/utils@0.2.11': {}
+
+ '@fontsource-variable/geist@5.2.8': {}
+
+ '@hono/node-server@1.19.11(hono@4.12.9)':
+ dependencies:
+ hono: 4.12.9
+
+ '@inquirer/ansi@1.0.2': {}
+
+ '@inquirer/confirm@5.1.21':
+ dependencies:
+ '@inquirer/core': 10.3.2
+ '@inquirer/type': 3.0.10
+
+ '@inquirer/core@10.3.2':
+ dependencies:
+ '@inquirer/ansi': 1.0.2
+ '@inquirer/figures': 1.0.15
+ '@inquirer/type': 3.0.10
+ cli-width: 4.1.0
+ mute-stream: 2.0.0
+ signal-exit: 4.1.0
+ wrap-ansi: 6.2.0
+ yoctocolors-cjs: 2.1.3
+
+ '@inquirer/figures@1.0.15': {}
+
+ '@inquirer/type@3.0.10': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@modelcontextprotocol/sdk@1.28.0(zod@3.25.76)':
+ dependencies:
+ '@hono/node-server': 1.19.11(hono@4.12.9)
+ ajv: 8.18.0
+ ajv-formats: 3.0.1(ajv@8.18.0)
+ content-type: 1.0.5
+ cors: 2.8.6
+ cross-spawn: 7.0.6
+ eventsource: 3.0.7
+ eventsource-parser: 3.0.6
+ express: 5.2.1
+ express-rate-limit: 8.3.1(express@5.2.1)
+ hono: 4.12.9
+ jose: 6.2.2
+ json-schema-typed: 8.0.2
+ pkce-challenge: 5.0.1
+ raw-body: 3.0.2
+ zod: 3.25.76
+ zod-to-json-schema: 3.25.2(zod@3.25.76)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mswjs/interceptors@0.41.3':
+ dependencies:
+ '@open-draft/deferred-promise': 2.2.0
+ '@open-draft/logger': 0.3.0
+ '@open-draft/until': 2.1.0
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+ strict-event-emitter: 0.5.1
+
+ '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)':
+ dependencies:
+ '@emnapi/core': 1.9.1
+ '@emnapi/runtime': 1.9.1
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
+ '@noble/ciphers@1.3.0': {}
+
+ '@noble/curves@1.9.7':
+ dependencies:
+ '@noble/hashes': 1.8.0
+
+ '@noble/hashes@1.8.0': {}
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.20.1
+
+ '@open-draft/deferred-promise@2.2.0': {}
+
+ '@open-draft/logger@0.3.0':
+ dependencies:
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+
+ '@open-draft/until@2.1.0': {}
+
+ '@oxc-project/types@0.122.0': {}
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-rc.12': {}
+
+ '@rolldown/pluginutils@1.0.0-rc.7': {}
+
+ '@sec-ant/readable-stream@0.4.1': {}
+
+ '@sindresorhus/merge-streams@4.0.0': {}
+
+ '@tailwindcss/node@4.2.2':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.20.1
+ jiti: 2.6.1
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.2.2
+
+ '@tailwindcss/oxide-android-arm64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide@4.2.2':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.2.2
+ '@tailwindcss/oxide-darwin-arm64': 4.2.2
+ '@tailwindcss/oxide-darwin-x64': 4.2.2
+ '@tailwindcss/oxide-freebsd-x64': 4.2.2
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2
+ '@tailwindcss/oxide-linux-arm64-musl': 4.2.2
+ '@tailwindcss/oxide-linux-x64-gnu': 4.2.2
+ '@tailwindcss/oxide-linux-x64-musl': 4.2.2
+ '@tailwindcss/oxide-wasm32-wasi': 4.2.2
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
+ '@tailwindcss/oxide-win32-x64-msvc': 4.2.2
+
+ '@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))':
+ dependencies:
+ '@tailwindcss/node': 4.2.2
+ '@tailwindcss/oxide': 4.2.2
+ tailwindcss: 4.2.2
+ vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)
+
+ '@ts-morph/common@0.27.0':
+ dependencies:
+ fast-glob: 3.3.3
+ minimatch: 10.2.4
+ path-browserify: 1.0.1
+
+ '@tybys/wasm-util@0.10.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/react-dom@19.2.3(@types/react@19.2.14)':
+ dependencies:
+ '@types/react': 19.2.14
+
+ '@types/react@19.2.14':
+ dependencies:
+ csstype: 3.2.3
+
+ '@types/statuses@2.0.6': {}
+
+ '@types/validate-npm-package-name@4.0.2': {}
+
+ '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.0-rc.7
+ vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)
+
+ accepts@2.0.0:
+ dependencies:
+ mime-types: 3.0.2
+ negotiator: 1.0.0
+
+ agent-base@7.1.4: {}
+
+ ajv-formats@3.0.1(ajv@8.18.0):
+ optionalDependencies:
+ ajv: 8.18.0
+
+ ajv@8.18.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ ast-types@0.16.1:
+ dependencies:
+ tslib: 2.8.1
+
+ balanced-match@4.0.4: {}
+
+ baseline-browser-mapping@2.10.12: {}
+
+ body-parser@2.2.2:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 4.4.3
+ http-errors: 2.0.1
+ iconv-lite: 0.7.2
+ on-finished: 2.4.1
+ qs: 6.15.0
+ raw-body: 3.0.2
+ type-is: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ brace-expansion@5.0.5:
+ dependencies:
+ balanced-match: 4.0.4
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.10.12
+ caniuse-lite: 1.0.30001781
+ electron-to-chromium: 1.5.328
+ node-releases: 2.0.36
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
+
+ bundle-name@4.1.0:
+ dependencies:
+ run-applescript: 7.1.0
+
+ bytes@3.1.2: {}
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001781: {}
+
+ chalk@5.6.2: {}
+
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
+ cli-cursor@5.0.0:
+ dependencies:
+ restore-cursor: 5.1.0
+
+ cli-spinners@2.9.2: {}
+
+ cli-width@4.1.0: {}
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ clsx@2.1.1: {}
+
+ code-block-writer@13.0.3: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ commander@11.1.0: {}
+
+ commander@14.0.3: {}
+
+ content-disposition@1.0.1: {}
+
+ content-type@1.0.5: {}
+
+ convert-source-map@2.0.0: {}
+
+ cookie-signature@1.2.2: {}
+
+ cookie@0.7.2: {}
+
+ cookie@1.1.1: {}
+
+ cors@2.8.6:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+
+ cosmiconfig@9.0.1(typescript@6.0.2):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 6.0.2
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ cssesc@3.0.0: {}
+
+ csstype@3.2.3: {}
+
+ data-uri-to-buffer@4.0.1: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ dedent@1.7.2: {}
+
+ deepmerge@4.3.1: {}
+
+ default-browser-id@5.0.1: {}
+
+ default-browser@5.5.0:
+ dependencies:
+ bundle-name: 4.1.0
+ default-browser-id: 5.0.1
+
+ define-lazy-prop@3.0.0: {}
+
+ depd@2.0.0: {}
+
+ detect-libc@2.1.2: {}
+
+ diff@8.0.4: {}
+
+ dotenv@17.3.1: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ eciesjs@0.4.18:
+ dependencies:
+ '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0)
+ '@noble/ciphers': 1.3.0
+ '@noble/curves': 1.9.7
+ '@noble/hashes': 1.8.0
+
+ ee-first@1.1.1: {}
+
+ electron-to-chromium@1.5.328: {}
+
+ emoji-regex@10.6.0: {}
+
+ emoji-regex@8.0.0: {}
+
+ encodeurl@2.0.0: {}
+
+ enhanced-resolve@5.20.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.2
+
+ env-paths@2.2.1: {}
+
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ escalade@3.2.0: {}
+
+ escape-html@1.0.3: {}
+
+ esprima@4.0.1: {}
+
+ etag@1.8.1: {}
+
+ eventsource-parser@3.0.6: {}
+
+ eventsource@3.0.7:
+ dependencies:
+ eventsource-parser: 3.0.6
+
+ execa@5.1.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ get-stream: 6.0.1
+ human-signals: 2.1.0
+ is-stream: 2.0.1
+ merge-stream: 2.0.0
+ npm-run-path: 4.0.1
+ onetime: 5.1.2
+ signal-exit: 3.0.7
+ strip-final-newline: 2.0.0
+
+ execa@9.6.1:
+ dependencies:
+ '@sindresorhus/merge-streams': 4.0.0
+ cross-spawn: 7.0.6
+ figures: 6.1.0
+ get-stream: 9.0.1
+ human-signals: 8.0.1
+ is-plain-obj: 4.1.0
+ is-stream: 4.0.1
+ npm-run-path: 6.0.0
+ pretty-ms: 9.3.0
+ signal-exit: 4.1.0
+ strip-final-newline: 4.0.0
+ yoctocolors: 2.1.2
+
+ express-rate-limit@8.3.1(express@5.2.1):
+ dependencies:
+ express: 5.2.1
+ ip-address: 10.1.0
+
+ express@5.2.1:
+ dependencies:
+ accepts: 2.0.0
+ body-parser: 2.2.2
+ content-disposition: 1.0.1
+ content-type: 1.0.5
+ cookie: 0.7.2
+ cookie-signature: 1.2.2
+ debug: 4.4.3
+ depd: 2.0.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 2.1.1
+ fresh: 2.0.0
+ http-errors: 2.0.1
+ merge-descriptors: 2.0.0
+ mime-types: 3.0.2
+ on-finished: 2.4.1
+ once: 1.4.0
+ parseurl: 1.3.3
+ proxy-addr: 2.0.7
+ qs: 6.15.0
+ range-parser: 1.2.1
+ router: 2.2.0
+ send: 1.2.1
+ serve-static: 2.2.1
+ statuses: 2.0.2
+ type-is: 2.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-uri@3.1.0: {}
+
+ fastq@1.20.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ fetch-blob@3.2.0:
+ dependencies:
+ node-domexception: 1.0.0
+ web-streams-polyfill: 3.3.3
+
+ figures@6.1.0:
+ dependencies:
+ is-unicode-supported: 2.1.0
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ finalhandler@2.1.1:
+ dependencies:
+ debug: 4.4.3
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ formdata-polyfill@4.0.10:
+ dependencies:
+ fetch-blob: 3.2.0
+
+ forwarded@0.2.0: {}
+
+ fresh@2.0.0: {}
+
+ fs-extra@11.3.4:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ fuzzysort@3.1.0: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-caller-file@2.0.5: {}
+
+ get-east-asian-width@1.5.0: {}
+
+ 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
+
+ get-own-enumerable-keys@1.0.0: {}
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-stream@6.0.1: {}
+
+ get-stream@9.0.1:
+ dependencies:
+ '@sec-ant/readable-stream': 0.4.1
+ is-stream: 4.0.1
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ graphql@16.13.2: {}
+
+ has-symbols@1.1.0: {}
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ headers-polyfill@4.0.3: {}
+
+ hono@4.12.9: {}
+
+ http-errors@2.0.1:
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.2
+ toidentifier: 1.0.1
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ human-signals@2.1.0: {}
+
+ human-signals@8.0.1: {}
+
+ iconv-lite@0.7.2:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ ignore@5.3.2: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ inherits@2.0.4: {}
+
+ ip-address@10.1.0: {}
+
+ ipaddr.js@1.9.1: {}
+
+ is-arrayish@0.2.1: {}
+
+ is-docker@3.0.0: {}
+
+ is-extglob@2.1.1: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-in-ssh@1.0.0: {}
+
+ is-inside-container@1.0.0:
+ dependencies:
+ is-docker: 3.0.0
+
+ is-interactive@2.0.0: {}
+
+ is-node-process@1.2.0: {}
+
+ is-number@7.0.0: {}
+
+ is-obj@3.0.0: {}
+
+ is-plain-obj@4.1.0: {}
+
+ is-promise@4.0.0: {}
+
+ is-regexp@3.1.0: {}
+
+ is-stream@2.0.1: {}
+
+ is-stream@4.0.1: {}
+
+ is-unicode-supported@1.3.0: {}
+
+ is-unicode-supported@2.1.0: {}
+
+ is-wsl@3.1.1:
+ dependencies:
+ is-inside-container: 1.0.0
+
+ isexe@2.0.0: {}
+
+ isexe@3.1.5: {}
+
+ jiti@2.6.1: {}
+
+ jose@6.2.2: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-parse-even-better-errors@2.3.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json-schema-typed@8.0.2: {}
+
+ json5@2.2.3: {}
+
+ jsonfile@6.2.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
+ kleur@3.0.3: {}
+
+ kleur@4.1.5: {}
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ 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
+
+ lines-and-columns@1.2.4: {}
+
+ log-symbols@6.0.0:
+ dependencies:
+ chalk: 5.6.2
+ is-unicode-supported: 1.3.0
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ lucide-react@1.7.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ math-intrinsics@1.1.0: {}
+
+ media-typer@1.1.0: {}
+
+ merge-descriptors@2.0.0: {}
+
+ merge-stream@2.0.0: {}
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.2
+
+ mime-db@1.54.0: {}
+
+ mime-types@3.0.2:
+ dependencies:
+ mime-db: 1.54.0
+
+ mimic-fn@2.1.0: {}
+
+ mimic-function@5.0.1: {}
+
+ minimatch@10.2.4:
+ dependencies:
+ brace-expansion: 5.0.5
+
+ minimist@1.2.8: {}
+
+ ms@2.1.3: {}
+
+ msw@2.12.14(typescript@6.0.2):
+ dependencies:
+ '@inquirer/confirm': 5.1.21
+ '@mswjs/interceptors': 0.41.3
+ '@open-draft/deferred-promise': 2.2.0
+ '@types/statuses': 2.0.6
+ cookie: 1.1.1
+ graphql: 16.13.2
+ headers-polyfill: 4.0.3
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+ path-to-regexp: 6.3.0
+ picocolors: 1.1.1
+ rettime: 0.10.1
+ statuses: 2.0.2
+ strict-event-emitter: 0.5.1
+ tough-cookie: 6.0.1
+ type-fest: 5.5.0
+ until-async: 3.0.2
+ yargs: 17.7.2
+ optionalDependencies:
+ typescript: 6.0.2
+ transitivePeerDependencies:
+ - '@types/node'
+
+ mute-stream@2.0.0: {}
+
+ nanoid@3.3.11: {}
+
+ negotiator@1.0.0: {}
+
+ node-domexception@1.0.0: {}
+
+ node-fetch@3.3.2:
+ dependencies:
+ data-uri-to-buffer: 4.0.1
+ fetch-blob: 3.2.0
+ formdata-polyfill: 4.0.10
+
+ node-releases@2.0.36: {}
+
+ npm-run-path@4.0.1:
+ dependencies:
+ path-key: 3.1.1
+
+ npm-run-path@6.0.0:
+ dependencies:
+ path-key: 4.0.0
+ unicorn-magic: 0.3.0
+
+ object-assign@4.1.1: {}
+
+ object-inspect@1.13.4: {}
+
+ object-treeify@1.1.33: {}
+
+ on-finished@2.4.1:
+ dependencies:
+ ee-first: 1.1.1
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ onetime@5.1.2:
+ dependencies:
+ mimic-fn: 2.1.0
+
+ onetime@7.0.0:
+ dependencies:
+ mimic-function: 5.0.1
+
+ open@11.0.0:
+ dependencies:
+ default-browser: 5.5.0
+ define-lazy-prop: 3.0.0
+ is-in-ssh: 1.0.0
+ is-inside-container: 1.0.0
+ powershell-utils: 0.1.0
+ wsl-utils: 0.3.1
+
+ ora@8.2.0:
+ dependencies:
+ chalk: 5.6.2
+ cli-cursor: 5.0.0
+ cli-spinners: 2.9.2
+ is-interactive: 2.0.0
+ is-unicode-supported: 2.1.0
+ log-symbols: 6.0.0
+ stdin-discarder: 0.2.2
+ string-width: 7.2.0
+ strip-ansi: 7.2.0
+
+ outvariant@1.4.3: {}
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ parse-ms@4.0.0: {}
+
+ parseurl@1.3.3: {}
+
+ path-browserify@1.0.1: {}
+
+ path-key@3.1.1: {}
+
+ path-key@4.0.0: {}
+
+ path-to-regexp@6.3.0: {}
+
+ path-to-regexp@8.4.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.2: {}
+
+ picomatch@4.0.4: {}
+
+ pkce-challenge@5.0.1: {}
+
+ postcss-selector-parser@7.1.1:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss@8.5.8:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ powershell-utils@0.1.0: {}
+
+ pretty-ms@9.3.0:
+ dependencies:
+ parse-ms: 4.0.0
+
+ prompts@2.4.2:
+ dependencies:
+ kleur: 3.0.3
+ sisteransi: 1.0.5
+
+ proxy-addr@2.0.7:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
+ qs@6.15.0:
+ dependencies:
+ side-channel: 1.1.0
+
+ queue-microtask@1.2.3: {}
+
+ range-parser@1.2.1: {}
+
+ raw-body@3.0.2:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.1
+ iconv-lite: 0.7.2
+ unpipe: 1.0.0
+
+ react-dom@19.2.4(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ scheduler: 0.27.0
+
+ react@19.2.4: {}
+
+ recast@0.23.11:
+ dependencies:
+ ast-types: 0.16.1
+ esprima: 4.0.1
+ source-map: 0.6.1
+ tiny-invariant: 1.3.3
+ tslib: 2.8.1
+
+ require-directory@2.1.1: {}
+
+ require-from-string@2.0.2: {}
+
+ reselect@5.1.1: {}
+
+ resolve-from@4.0.0: {}
+
+ restore-cursor@5.1.0:
+ dependencies:
+ onetime: 7.0.0
+ signal-exit: 4.1.0
+
+ rettime@0.10.1: {}
+
+ reusify@1.1.0: {}
+
+ rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1):
+ dependencies:
+ '@oxc-project/types': 0.122.0
+ '@rolldown/pluginutils': 1.0.0-rc.12
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-rc.12
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.12
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.12
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.12
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+
+ router@2.2.0:
+ dependencies:
+ debug: 4.4.3
+ depd: 2.0.0
+ is-promise: 4.0.0
+ parseurl: 1.3.3
+ path-to-regexp: 8.4.0
+ transitivePeerDependencies:
+ - supports-color
+
+ run-applescript@7.1.0: {}
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ safer-buffer@2.1.2: {}
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ 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
+ transitivePeerDependencies:
+ - supports-color
+
+ serve-static@2.2.1:
+ dependencies:
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 1.2.1
+ transitivePeerDependencies:
+ - supports-color
+
+ setprototypeof@1.2.0: {}
+
+ shadcn@4.1.1(typescript@6.0.2):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.2
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
+ '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0)
+ '@dotenvx/dotenvx': 1.59.0
+ '@modelcontextprotocol/sdk': 1.28.0(zod@3.25.76)
+ '@types/validate-npm-package-name': 4.0.2
+ browserslist: 4.28.1
+ commander: 14.0.3
+ cosmiconfig: 9.0.1(typescript@6.0.2)
+ dedent: 1.7.2
+ deepmerge: 4.3.1
+ diff: 8.0.4
+ execa: 9.6.1
+ fast-glob: 3.3.3
+ fs-extra: 11.3.4
+ fuzzysort: 3.1.0
+ https-proxy-agent: 7.0.6
+ kleur: 4.1.5
+ msw: 2.12.14(typescript@6.0.2)
+ node-fetch: 3.3.2
+ open: 11.0.0
+ ora: 8.2.0
+ postcss: 8.5.8
+ postcss-selector-parser: 7.1.1
+ prompts: 2.4.2
+ recast: 0.23.11
+ stringify-object: 5.0.0
+ tailwind-merge: 3.5.0
+ ts-morph: 26.0.0
+ tsconfig-paths: 4.2.0
+ validate-npm-package-name: 7.0.2
+ zod: 3.25.76
+ zod-to-json-schema: 3.25.2(zod@3.25.76)
+ transitivePeerDependencies:
+ - '@cfworker/json-schema'
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - typescript
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
+ signal-exit@3.0.7: {}
+
+ signal-exit@4.1.0: {}
+
+ sisteransi@1.0.5: {}
+
+ source-map-js@1.2.1: {}
+
+ source-map@0.6.1: {}
+
+ statuses@2.0.2: {}
+
+ stdin-discarder@0.2.2: {}
+
+ strict-event-emitter@0.5.1: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@7.2.0:
+ dependencies:
+ emoji-regex: 10.6.0
+ get-east-asian-width: 1.5.0
+ strip-ansi: 7.2.0
+
+ stringify-object@5.0.0:
+ dependencies:
+ get-own-enumerable-keys: 1.0.0
+ is-obj: 3.0.0
+ is-regexp: 3.1.0
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.2.2
+
+ strip-bom@3.0.0: {}
+
+ strip-final-newline@2.0.0: {}
+
+ strip-final-newline@4.0.0: {}
+
+ tabbable@6.4.0: {}
+
+ tagged-tag@1.0.0: {}
+
+ tailwind-merge@3.5.0: {}
+
+ tailwindcss@4.2.2: {}
+
+ tapable@2.3.2: {}
+
+ tiny-invariant@1.3.3: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ tldts-core@7.0.27: {}
+
+ tldts@7.0.27:
+ dependencies:
+ tldts-core: 7.0.27
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ toidentifier@1.0.1: {}
+
+ tough-cookie@6.0.1:
+ dependencies:
+ tldts: 7.0.27
+
+ ts-morph@26.0.0:
+ dependencies:
+ '@ts-morph/common': 0.27.0
+ code-block-writer: 13.0.3
+
+ tsconfig-paths@4.2.0:
+ dependencies:
+ json5: 2.2.3
+ minimist: 1.2.8
+ strip-bom: 3.0.0
+
+ tslib@2.8.1: {}
+
+ tw-animate-css@1.4.0: {}
+
+ type-fest@5.5.0:
+ dependencies:
+ tagged-tag: 1.0.0
+
+ type-is@2.0.1:
+ dependencies:
+ content-type: 1.0.5
+ media-typer: 1.1.0
+ mime-types: 3.0.2
+
+ typescript@6.0.2: {}
+
+ unicorn-magic@0.3.0: {}
+
+ universalify@2.0.1: {}
+
+ unpipe@1.0.0: {}
+
+ until-async@3.0.2: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
+ dependencies:
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ use-sync-external-store@1.6.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
+ util-deprecate@1.0.2: {}
+
+ validate-npm-package-name@7.0.2: {}
+
+ vary@1.1.2: {}
+
+ vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.8
+ rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+
+ web-streams-polyfill@3.3.3: {}
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ which@4.0.0:
+ dependencies:
+ isexe: 3.1.5
+
+ wrap-ansi@6.2.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrappy@1.0.2: {}
+
+ wsl-utils@0.3.1:
+ dependencies:
+ is-wsl: 3.1.1
+ powershell-utils: 0.1.0
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
+ yoctocolors-cjs@2.1.3: {}
+
+ yoctocolors@2.1.2: {}
+
+ zod-to-json-schema@3.25.2(zod@3.25.76):
+ dependencies:
+ zod: 3.25.76
+
+ zod@3.25.76: {}
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
new file mode 100644
index 00000000..444f4ef0
--- /dev/null
+++ b/frontend/src/components/ui/button.tsx
@@ -0,0 +1,58 @@
+import { Button as ButtonPrimitive } from "@base-ui/react/button"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
+ outline:
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
+ ghost:
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
+ destructive:
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default:
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
+ icon: "size-8",
+ "icon-xs":
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
+ "icon-sm":
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
+ "icon-lg": "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant = "default",
+ size = "default",
+ ...props
+}: ButtonPrimitive.Props & VariantProps) {
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/frontend/src/globals.css b/frontend/src/globals.css
new file mode 100644
index 00000000..fb3c7e98
--- /dev/null
+++ b/frontend/src/globals.css
@@ -0,0 +1,130 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+@import "shadcn/tailwind.css";
+@import "@fontsource-variable/geist";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --font-heading: var(--font-sans);
+ --font-sans: 'Geist Variable', sans-serif;
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --color-foreground: var(--foreground);
+ --color-background: var(--background);
+ --radius-sm: calc(var(--radius) * 0.6);
+ --radius-md: calc(var(--radius) * 0.8);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) * 1.4);
+ --radius-2xl: calc(var(--radius) * 1.8);
+ --radius-3xl: calc(var(--radius) * 2.2);
+ --radius-4xl: calc(var(--radius) * 2.6);
+}
+
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ html {
+ @apply font-sans;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
new file mode 100644
index 00000000..bd0c391d
--- /dev/null
+++ b/frontend/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 00000000..77f6fcb0
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1 @@
+import "./globals.css";
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 00000000..75de54ff
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "ignoreDeprecations": "6.0"
+ },
+ "include": ["src", "functions", "types"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 00000000..90dac2e4
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
+import path from "path";
+
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});
diff --git a/functions/infra/metabase_auth.go b/functions/infra/metabase_auth.go
new file mode 100644
index 00000000..38d62ecd
--- /dev/null
+++ b/functions/infra/metabase_auth.go
@@ -0,0 +1,49 @@
+package infra
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+// MetabaseAuth autentica con email y password contra una instancia Metabase.
+// Retorna un MetabaseClient con el session token listo para usar.
+// baseURL es la URL base sin trailing slash (ej: "http://localhost:3000").
+func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error) {
+ payload, _ := json.Marshal(map[string]string{
+ "username": email,
+ "password": password,
+ })
+
+ resp, err := http.Post(baseURL+"/api/session", "application/json", bytes.NewReader(payload))
+ if err != nil {
+ return MetabaseClient{}, fmt.Errorf("metabase auth: %w", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return MetabaseClient{}, fmt.Errorf("read auth response: %w", err)
+ }
+
+ if resp.StatusCode != 200 {
+ return MetabaseClient{}, fmt.Errorf("metabase auth: status %d: %s", resp.StatusCode, string(body))
+ }
+
+ var result struct {
+ ID string `json:"id"`
+ }
+ if err := json.Unmarshal(body, &result); err != nil {
+ return MetabaseClient{}, fmt.Errorf("parse auth response: %w", err)
+ }
+
+ return MetabaseClient{BaseURL: baseURL, Token: result.ID}, nil
+}
+
+// MetabaseNewClient crea un MetabaseClient usando una API key en lugar de session token.
+// Las API keys se crean en Settings > Authentication > API Keys del admin de Metabase.
+func MetabaseNewClient(baseURL, apiKey string) MetabaseClient {
+ return MetabaseClient{BaseURL: baseURL, Token: apiKey}
+}
diff --git a/functions/infra/metabase_auth.md b/functions/infra/metabase_auth.md
new file mode 100644
index 00000000..8234de47
--- /dev/null
+++ b/functions/infra/metabase_auth.md
@@ -0,0 +1,50 @@
+---
+name: metabase_auth
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error)"
+description: "Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias (configurable con MAX_SESSION_AGE en Metabase). Endpoint: POST /api/session."
+tags: [metabase, auth, session, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: [MetabaseClient_go_infra]
+returns_optional: false
+error_type: "error_go_core"
+imports: [bytes, encoding/json, fmt, io, net/http]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_auth.go"
+---
+
+## Ejemplo
+
+```go
+// Autenticar con credenciales
+client, err := MetabaseAuth("http://localhost:3000", "admin@example.com", "password123")
+if err != nil {
+ log.Fatal(err)
+}
+// client.Token contiene el session token
+
+// Alternativa: usar API key directamente
+client := MetabaseNewClient("http://localhost:3000", "mb_api_key_xxxxx")
+```
+
+## Notas
+
+Dos formas de obtener un MetabaseClient:
+- `MetabaseAuth`: login con email/password, obtiene session token via POST /api/session. Token expira en 14 dias por defecto.
+- `MetabaseNewClient`: usa una API key creada en el admin UI. No expira. Recomendado para automatizacion.
+
+El token se envia como header `X-Metabase-Session` en todas las llamadas subsiguientes.
+
+### Para un LLM que use estas funciones
+
+1. Primero obtener un client con `MetabaseAuth()` o `MetabaseNewClient()`
+2. Pasar el client a todas las funciones CRUD (usuarios, cards, dashboards)
+3. Si recibes error 401, el token expiro — re-autenticar
+4. Rate limiting: Metabase limita intentos de login fallidos
diff --git a/functions/infra/metabase_create_card.go b/functions/infra/metabase_create_card.go
new file mode 100644
index 00000000..8ffe3818
--- /dev/null
+++ b/functions/infra/metabase_create_card.go
@@ -0,0 +1,35 @@
+package infra
+
+import "fmt"
+
+// MetabaseCreateCard crea una nueva card/pregunta en Metabase.
+// name: nombre de la pregunta (obligatorio).
+// datasetQuery: query de la card (obligatorio). Estructura:
+//
+// SQL nativo: {"database": 1, "type": "native", "native": {"query": "SELECT ..."}}
+// MBQL: {"database": 1, "type": "query", "query": {"source-table": 4, ...}}
+//
+// display: tipo de visualizacion ("table", "bar", "line", "pie", "scalar", etc.).
+// collectionID: ID de la coleccion/carpeta (0 = root).
+// description: descripcion opcional (vacio = sin descripcion).
+func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error) {
+ body := map[string]any{
+ "name": name,
+ "dataset_query": datasetQuery,
+ "display": display,
+ "visualization_settings": map[string]any{},
+ }
+ if collectionID > 0 {
+ body["collection_id"] = collectionID
+ }
+ if description != "" {
+ body["description"] = description
+ }
+
+ result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/card", body)
+ if err != nil {
+ return nil, fmt.Errorf("metabase create card: %w", err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_create_card.md b/functions/infra/metabase_create_card.md
new file mode 100644
index 00000000..abeab982
--- /dev/null
+++ b/functions/infra/metabase_create_card.md
@@ -0,0 +1,86 @@
+---
+name: metabase_create_card
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error)"
+description: "Crea una nueva card/pregunta en Metabase con query SQL nativa o MBQL. Endpoint: POST /api/card."
+tags: [metabase, card, question, create, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_create_card.go"
+---
+
+## Ejemplo
+
+```go
+// Crear pregunta con SQL nativo
+card, err := MetabaseCreateCard(client, "Revenue by Month", map[string]any{
+ "database": 1,
+ "type": "native",
+ "native": map[string]any{
+ "query": "SELECT date_trunc('month', created_at) as month, SUM(total) as revenue FROM orders GROUP BY 1 ORDER BY 1",
+ },
+}, "line", 5, "Monthly revenue trend")
+
+// Crear pregunta con MBQL (structured query)
+card, err := MetabaseCreateCard(client, "Order Count", map[string]any{
+ "database": 1,
+ "type": "query",
+ "query": map[string]any{
+ "source-table": 4,
+ "aggregation": []any{[]any{"count"}},
+ },
+}, "scalar", 0, "Total number of orders")
+```
+
+## Notas
+
+### Parametros para un LLM
+
+| Parametro | Tipo | Requerido | Descripcion |
+|-----------|------|-----------|-------------|
+| client | MetabaseClient | si | Cliente autenticado |
+| name | string | si | Nombre de la pregunta |
+| datasetQuery | map[string]any | si | Query. Ver estructura abajo |
+| display | string | si | Tipo de visualizacion |
+| collectionID | int | no | ID de coleccion. 0 = root collection |
+| description | string | no | Descripcion. Vacio = sin descripcion |
+
+### Estructura de datasetQuery
+
+**SQL nativo:**
+```json
+{
+ "database": ,
+ "type": "native",
+ "native": {"query": "SELECT ..."}
+}
+```
+
+**MBQL (structured):**
+```json
+{
+ "database": ,
+ "type": "query",
+ "query": {
+ "source-table": ,
+ "aggregation": [["count"]],
+ "breakout": [["field", , {"temporal-unit": "month"}]],
+ "filter": ["=", ["field", , null], "value"]
+ }
+}
+```
+
+### Valores de display
+
+table, bar, line, pie, scalar, area, row, combo, funnel, map, scatter, waterfall, progress, gauge
diff --git a/functions/infra/metabase_create_dashboard.go b/functions/infra/metabase_create_dashboard.go
new file mode 100644
index 00000000..955100f9
--- /dev/null
+++ b/functions/infra/metabase_create_dashboard.go
@@ -0,0 +1,26 @@
+package infra
+
+import "fmt"
+
+// MetabaseCreateDashboard crea un nuevo dashboard en Metabase.
+// name: nombre del dashboard (obligatorio).
+// description: descripcion opcional (vacio = sin descripcion).
+// collectionID: ID de la coleccion/carpeta (0 = root).
+func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error) {
+ body := map[string]any{
+ "name": name,
+ }
+ if description != "" {
+ body["description"] = description
+ }
+ if collectionID > 0 {
+ body["collection_id"] = collectionID
+ }
+
+ result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/dashboard", body)
+ if err != nil {
+ return nil, fmt.Errorf("metabase create dashboard: %w", err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_create_dashboard.md b/functions/infra/metabase_create_dashboard.md
new file mode 100644
index 00000000..120fb0e6
--- /dev/null
+++ b/functions/infra/metabase_create_dashboard.md
@@ -0,0 +1,53 @@
+---
+name: metabase_create_dashboard
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error)"
+description: "Crea un nuevo dashboard vacio en Metabase. Para agregar cards usar MetabaseUpdateDashboard con el campo dashcards. Endpoint: POST /api/dashboard."
+tags: [metabase, dashboard, create, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_create_dashboard.go"
+---
+
+## Ejemplo
+
+```go
+// Crear dashboard vacio
+dashboard, err := MetabaseCreateDashboard(client, "Sales Overview", "KPIs de ventas", 5)
+if err != nil {
+ log.Fatal(err)
+}
+dashboardID := int(dashboard["id"].(float64))
+
+// Luego agregar cards con MetabaseUpdateDashboard
+MetabaseUpdateDashboard(client, dashboardID, map[string]any{
+ "dashcards": []map[string]any{
+ {"id": -1, "card_id": 42, "size_x": 6, "size_y": 4, "col": 0, "row": 0},
+ },
+})
+```
+
+## Notas
+
+### Parametros para un LLM
+
+| Parametro | Tipo | Requerido | Descripcion |
+|-----------|------|-----------|-------------|
+| client | MetabaseClient | si | Cliente autenticado |
+| name | string | si | Nombre del dashboard |
+| description | string | no | Descripcion. Vacio = sin descripcion |
+| collectionID | int | no | Coleccion destino. 0 = root |
+
+El dashboard se crea vacio. Para agregar cards, usar MetabaseUpdateDashboard con el array dashcards.
+Retorna el objeto dashboard creado.
diff --git a/functions/infra/metabase_create_user.go b/functions/infra/metabase_create_user.go
new file mode 100644
index 00000000..c72e7dea
--- /dev/null
+++ b/functions/infra/metabase_create_user.go
@@ -0,0 +1,28 @@
+package infra
+
+import "fmt"
+
+// MetabaseCreateUser crea un nuevo usuario en Metabase.
+// firstName, lastName y email son obligatorios.
+// password es opcional: si esta vacio, Metabase envia email de invitacion.
+// groupIDs es opcional: IDs de grupos a asignar (nil = solo grupo default).
+func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error) {
+ body := map[string]any{
+ "first_name": firstName,
+ "last_name": lastName,
+ "email": email,
+ }
+ if password != "" {
+ body["password"] = password
+ }
+ if len(groupIDs) > 0 {
+ body["group_ids"] = groupIDs
+ }
+
+ result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/user", body)
+ if err != nil {
+ return nil, fmt.Errorf("metabase create user: %w", err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_create_user.md b/functions/infra/metabase_create_user.md
new file mode 100644
index 00000000..21b62345
--- /dev/null
+++ b/functions/infra/metabase_create_user.md
@@ -0,0 +1,47 @@
+---
+name: metabase_create_user
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error)"
+description: "Crea un nuevo usuario en Metabase. Si no se provee password, Metabase envia email de invitacion. Requiere permisos de superusuario. Endpoint: POST /api/user."
+tags: [metabase, user, create, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_create_user.go"
+---
+
+## Ejemplo
+
+```go
+// Crear usuario con password
+user, err := MetabaseCreateUser(client, "John", "Doe", "john@example.com", "securePass123", nil)
+
+// Crear usuario sin password (envia invitacion por email)
+user, err := MetabaseCreateUser(client, "Jane", "Smith", "jane@example.com", "", []int{1, 3})
+```
+
+## Notas
+
+### Parametros para un LLM
+
+| Parametro | Tipo | Requerido | Descripcion |
+|-----------|------|-----------|-------------|
+| client | MetabaseClient | si | Cliente autenticado con permisos admin |
+| firstName | string | si | Nombre del usuario |
+| lastName | string | si | Apellido del usuario |
+| email | string | si | Email unico del usuario |
+| password | string | no | Password. Vacio = Metabase envia invitacion |
+| groupIDs | []int | no | IDs de grupos. nil = solo grupo default |
+
+El email debe ser unico. Si ya existe, retorna error 400.
+Retorna el objeto usuario creado como map (mismos campos que MetabaseGetUser).
diff --git a/functions/infra/metabase_deactivate_user.go b/functions/infra/metabase_deactivate_user.go
new file mode 100644
index 00000000..df2cba9a
--- /dev/null
+++ b/functions/infra/metabase_deactivate_user.go
@@ -0,0 +1,17 @@
+package infra
+
+import "fmt"
+
+// MetabaseDeactivateUser desactiva (soft-delete) un usuario en Metabase.
+// El usuario no se elimina permanentemente, solo se marca como inactivo.
+// Requiere permisos de superusuario.
+func MetabaseDeactivateUser(client MetabaseClient, userID int) error {
+ path := fmt.Sprintf("/api/user/%d", userID)
+
+ _, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return fmt.Errorf("metabase deactivate user %d: %w", userID, err)
+ }
+
+ return nil
+}
diff --git a/functions/infra/metabase_deactivate_user.md b/functions/infra/metabase_deactivate_user.md
new file mode 100644
index 00000000..dbb0d541
--- /dev/null
+++ b/functions/infra/metabase_deactivate_user.md
@@ -0,0 +1,40 @@
+---
+name: metabase_deactivate_user
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseDeactivateUser(client MetabaseClient, userID int) error"
+description: "Desactiva (soft-delete) un usuario en Metabase. El usuario no se elimina permanentemente, solo se marca como inactivo. Para reactivar, usar PUT /api/user/:id/reactivate. Endpoint: DELETE /api/user/:id."
+tags: [metabase, user, delete, deactivate, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_deactivate_user.go"
+---
+
+## Ejemplo
+
+```go
+err := MetabaseDeactivateUser(client, 5)
+if err != nil {
+ log.Fatal(err)
+}
+// Usuario 5 ahora esta inactivo
+// Para ver desactivados: MetabaseListUsers(client, "deactivated", "", 0, 0)
+```
+
+## Notas
+
+Es un soft-delete: el usuario se desactiva pero no se borra. Se puede reactivar con PUT /api/user/:id/reactivate.
+
+Para listar usuarios desactivados, usar `MetabaseListUsers` con status "deactivated".
+
+Requiere permisos de superusuario. Error 403 si no eres admin.
diff --git a/functions/infra/metabase_delete_card.go b/functions/infra/metabase_delete_card.go
new file mode 100644
index 00000000..db4982ed
--- /dev/null
+++ b/functions/infra/metabase_delete_card.go
@@ -0,0 +1,16 @@
+package infra
+
+import "fmt"
+
+// MetabaseDeleteCard elimina permanentemente una card/pregunta de Metabase.
+// Para soft-delete, usar MetabaseUpdateCard con archived: true.
+func MetabaseDeleteCard(client MetabaseClient, cardID int) error {
+ path := fmt.Sprintf("/api/card/%d", cardID)
+
+ _, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return fmt.Errorf("metabase delete card %d: %w", cardID, err)
+ }
+
+ return nil
+}
diff --git a/functions/infra/metabase_delete_card.md b/functions/infra/metabase_delete_card.md
new file mode 100644
index 00000000..d2c296f7
--- /dev/null
+++ b/functions/infra/metabase_delete_card.md
@@ -0,0 +1,37 @@
+---
+name: metabase_delete_card
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseDeleteCard(client MetabaseClient, cardID int) error"
+description: "Elimina permanentemente una card/pregunta de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateCard con archived:true. Endpoint: DELETE /api/card/:id."
+tags: [metabase, card, question, delete, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_delete_card.go"
+---
+
+## Ejemplo
+
+```go
+// Eliminar permanentemente
+err := MetabaseDeleteCard(client, 42)
+
+// Preferir soft-delete cuando sea posible:
+// MetabaseUpdateCard(client, 42, map[string]any{"archived": true})
+```
+
+## Notas
+
+**ATENCION**: Esta operacion es irreversible. La card se elimina permanentemente.
+
+Para un borrado seguro, preferir archivar con `MetabaseUpdateCard(client, cardID, map[string]any{"archived": true})` que permite recuperar la card despues.
diff --git a/functions/infra/metabase_delete_dashboard.go b/functions/infra/metabase_delete_dashboard.go
new file mode 100644
index 00000000..f12584a8
--- /dev/null
+++ b/functions/infra/metabase_delete_dashboard.go
@@ -0,0 +1,16 @@
+package infra
+
+import "fmt"
+
+// MetabaseDeleteDashboard elimina permanentemente un dashboard de Metabase.
+// Para soft-delete, usar MetabaseUpdateDashboard con archived: true.
+func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error {
+ path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
+
+ _, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return fmt.Errorf("metabase delete dashboard %d: %w", dashboardID, err)
+ }
+
+ return nil
+}
diff --git a/functions/infra/metabase_delete_dashboard.md b/functions/infra/metabase_delete_dashboard.md
new file mode 100644
index 00000000..a9e03fe8
--- /dev/null
+++ b/functions/infra/metabase_delete_dashboard.md
@@ -0,0 +1,37 @@
+---
+name: metabase_delete_dashboard
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error"
+description: "Elimina permanentemente un dashboard de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateDashboard con archived:true. Endpoint: DELETE /api/dashboard/:id."
+tags: [metabase, dashboard, delete, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_delete_dashboard.go"
+---
+
+## Ejemplo
+
+```go
+// Eliminar permanentemente
+err := MetabaseDeleteDashboard(client, 1)
+
+// Preferir soft-delete:
+// MetabaseUpdateDashboard(client, 1, map[string]any{"archived": true})
+```
+
+## Notas
+
+**ATENCION**: Esta operacion es irreversible. El dashboard y todas sus dashcards se eliminan permanentemente.
+
+Para un borrado seguro, preferir archivar con `MetabaseUpdateDashboard(client, dashboardID, map[string]any{"archived": true})`.
diff --git a/functions/infra/metabase_execute_card.go b/functions/infra/metabase_execute_card.go
new file mode 100644
index 00000000..f7cda011
--- /dev/null
+++ b/functions/infra/metabase_execute_card.go
@@ -0,0 +1,22 @@
+package infra
+
+import "fmt"
+
+// MetabaseExecuteCard ejecuta la query de una card/pregunta guardada.
+// parameters: parametros de la query (nil si no tiene parametros).
+// Retorna los resultados con columnas y filas.
+func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error) {
+ path := fmt.Sprintf("/api/card/%d/query", cardID)
+
+ var body map[string]any
+ if len(parameters) > 0 {
+ body = map[string]any{"parameters": parameters}
+ }
+
+ result, err := metabaseRequest("POST", client.BaseURL, client.Token, path, body)
+ if err != nil {
+ return nil, fmt.Errorf("metabase execute card %d: %w", cardID, err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_execute_card.md b/functions/infra/metabase_execute_card.md
new file mode 100644
index 00000000..18aff315
--- /dev/null
+++ b/functions/infra/metabase_execute_card.md
@@ -0,0 +1,71 @@
+---
+name: metabase_execute_card
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error)"
+description: "Ejecuta la query de una card/pregunta guardada en Metabase y retorna los resultados. Soporta parametros para queries parametrizadas. Endpoint: POST /api/card/:id/query."
+tags: [metabase, card, question, execute, query, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_execute_card.go"
+---
+
+## Ejemplo
+
+```go
+// Ejecutar sin parametros
+result, err := MetabaseExecuteCard(client, 42, nil)
+if err != nil {
+ log.Fatal(err)
+}
+data := result["data"].(map[string]any)
+rows := data["rows"].([]any)
+fmt.Printf("Filas: %d\n", len(rows))
+
+// Ejecutar con parametros
+result, err := MetabaseExecuteCard(client, 42, []map[string]any{
+ {
+ "type": "category",
+ "target": []any{"variable", []any{"template-tag", "status"}},
+ "value": "active",
+ },
+})
+```
+
+## Notas
+
+### Estructura de la respuesta
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| status | string | "completed" o "failed" |
+| row_count | float64 | Numero de filas |
+| running_time | float64 | Tiempo de ejecucion en ms |
+| data.columns | []string | Nombres de columnas |
+| data.rows | [][]any | Filas de datos |
+| data.cols | []map | Metadata de columnas (name, base_type, display_name) |
+| data.native_form.query | string | SQL ejecutado |
+
+### Parametros para queries parametrizadas
+
+```go
+[]map[string]any{
+ {
+ "type": "category", // tipo del parametro
+ "target": []any{"variable", []any{"template-tag", "tag"}}, // referencia al template-tag
+ "value": "valor", // valor a inyectar
+ },
+}
+```
+
+Limite por defecto: 2000 filas. Para queries ad-hoc sin card, usar MetabaseExecuteQuery.
diff --git a/functions/infra/metabase_execute_query.go b/functions/infra/metabase_execute_query.go
new file mode 100644
index 00000000..5beed0ec
--- /dev/null
+++ b/functions/infra/metabase_execute_query.go
@@ -0,0 +1,28 @@
+package infra
+
+import "fmt"
+
+// MetabaseExecuteQuery ejecuta una query ad-hoc (sin guardar como card) en Metabase.
+// databaseID: ID de la base de datos en Metabase.
+// sql: query SQL a ejecutar.
+// maxResults: limite de filas (0 = default 2000 de Metabase).
+func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error) {
+ body := map[string]any{
+ "database": databaseID,
+ "type": "native",
+ "native": map[string]any{"query": sql},
+ }
+ if maxResults > 0 {
+ body["constraints"] = map[string]any{
+ "max-results": maxResults,
+ "max-results-bare-rows": maxResults,
+ }
+ }
+
+ result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/dataset", body)
+ if err != nil {
+ return nil, fmt.Errorf("metabase execute query: %w", err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_execute_query.md b/functions/infra/metabase_execute_query.md
new file mode 100644
index 00000000..a3bbe527
--- /dev/null
+++ b/functions/infra/metabase_execute_query.md
@@ -0,0 +1,63 @@
+---
+name: metabase_execute_query
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error)"
+description: "Ejecuta una query SQL ad-hoc contra una database de Metabase sin guardarla como card. Util para consultas rapidas y exploracion. Endpoint: POST /api/dataset."
+tags: [metabase, query, execute, sql, dataset, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_execute_query.go"
+---
+
+## Ejemplo
+
+```go
+// Query simple
+result, err := MetabaseExecuteQuery(client, 1, "SELECT * FROM users LIMIT 10", 0)
+if err != nil {
+ log.Fatal(err)
+}
+data := result["data"].(map[string]any)
+rows := data["rows"].([]any)
+
+// Query con limite custom
+result, err := MetabaseExecuteQuery(client, 1, "SELECT * FROM orders", 5000)
+```
+
+## Notas
+
+### Parametros para un LLM
+
+| Parametro | Tipo | Requerido | Descripcion |
+|-----------|------|-----------|-------------|
+| client | MetabaseClient | si | Cliente autenticado |
+| databaseID | int | si | ID de la database en Metabase (obtener con GET /api/database) |
+| sql | string | si | Query SQL a ejecutar |
+| maxResults | int | no | Limite de filas. 0 = default 2000 |
+
+### Diferencia con MetabaseExecuteCard
+
+- `MetabaseExecuteQuery`: query ad-hoc, no se guarda. Usa POST /api/dataset.
+- `MetabaseExecuteCard`: ejecuta una card ya guardada. Usa POST /api/card/:id/query.
+
+Usar esta funcion para exploracion rapida. Si la query se va a reutilizar, crear una card con MetabaseCreateCard.
+
+### Estructura de la respuesta
+
+Misma estructura que MetabaseExecuteCard:
+- `data.columns`: nombres de columnas
+- `data.rows`: filas de datos
+- `row_count`: numero de filas
+- `running_time`: tiempo en ms
+- `status`: "completed" o "failed"
diff --git a/functions/infra/metabase_get_card.go b/functions/infra/metabase_get_card.go
new file mode 100644
index 00000000..e68c3168
--- /dev/null
+++ b/functions/infra/metabase_get_card.go
@@ -0,0 +1,15 @@
+package infra
+
+import "fmt"
+
+// MetabaseGetCard obtiene una card/pregunta de Metabase por su ID.
+func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error) {
+ path := fmt.Sprintf("/api/card/%d", cardID)
+
+ result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return nil, fmt.Errorf("metabase get card %d: %w", cardID, err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_get_card.md b/functions/infra/metabase_get_card.md
new file mode 100644
index 00000000..57d3b5aa
--- /dev/null
+++ b/functions/infra/metabase_get_card.md
@@ -0,0 +1,52 @@
+---
+name: metabase_get_card
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error)"
+description: "Obtiene los detalles completos de una card/pregunta de Metabase por su ID. Incluye la query, visualizacion y metadata. Endpoint: GET /api/card/:id."
+tags: [metabase, card, question, get, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_get_card.go"
+---
+
+## Ejemplo
+
+```go
+card, err := MetabaseGetCard(client, 42)
+if err != nil {
+ log.Fatal(err)
+}
+fmt.Println(card["name"], card["display"])
+```
+
+## Notas
+
+Retorna el objeto card completo. Error 404 si no existe.
+
+### Campos principales
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| id | float64 | ID de la card |
+| name | string | Nombre |
+| description | string | Descripcion |
+| display | string | Tipo visualizacion |
+| dataset_query | map | Query (native.query para SQL, query para MBQL) |
+| visualization_settings | map | Config de visualizacion |
+| collection_id | float64 | Coleccion contenedora |
+| database_id | float64 | Database asociada |
+| archived | bool | Archivada |
+| creator | map | Objeto del usuario creador |
+| created_at | string | Fecha creacion |
+| updated_at | string | Fecha actualizacion |
diff --git a/functions/infra/metabase_get_dashboard.go b/functions/infra/metabase_get_dashboard.go
new file mode 100644
index 00000000..9bc7f3ce
--- /dev/null
+++ b/functions/infra/metabase_get_dashboard.go
@@ -0,0 +1,15 @@
+package infra
+
+import "fmt"
+
+// MetabaseGetDashboard obtiene un dashboard completo de Metabase incluyendo sus cards.
+func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error) {
+ path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
+
+ result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return nil, fmt.Errorf("metabase get dashboard %d: %w", dashboardID, err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_get_dashboard.md b/functions/infra/metabase_get_dashboard.md
new file mode 100644
index 00000000..07147b91
--- /dev/null
+++ b/functions/infra/metabase_get_dashboard.md
@@ -0,0 +1,71 @@
+---
+name: metabase_get_dashboard
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error)"
+description: "Obtiene un dashboard completo de Metabase incluyendo todas sus dashcards (cards posicionadas en el dashboard), tabs y parametros. Endpoint: GET /api/dashboard/:id."
+tags: [metabase, dashboard, get, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_get_dashboard.go"
+---
+
+## Ejemplo
+
+```go
+dashboard, err := MetabaseGetDashboard(client, 1)
+if err != nil {
+ log.Fatal(err)
+}
+fmt.Println(dashboard["name"])
+
+// Acceder a las cards del dashboard
+dashcards := dashboard["dashcards"].([]any)
+for _, dc := range dashcards {
+ card := dc.(map[string]any)
+ fmt.Printf("Card ID: %v, Position: (%v, %v)\n",
+ card["card_id"], card["col"], card["row"])
+}
+```
+
+## Notas
+
+### Campos principales
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| id | float64 | ID del dashboard |
+| name | string | Nombre |
+| description | string | Descripcion |
+| dashcards | []map | Array de dashcards (cards posicionadas) |
+| parameters | []map | Filtros del dashboard |
+| tabs | []map | Tabs del dashboard |
+| collection_id | float64 | Coleccion contenedora |
+| archived | bool | Archivado |
+
+### Estructura de cada dashcard
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| id | float64 | ID del dashcard (positivo) |
+| card_id | float64 | ID de la card/pregunta asociada |
+| card | map | Objeto card completo |
+| size_x | float64 | Ancho en grid (1-18) |
+| size_y | float64 | Alto en grid |
+| col | float64 | Columna en grid (0-based) |
+| row | float64 | Fila en grid (0-based) |
+| dashboard_tab_id | float64 | Tab al que pertenece (null = sin tabs) |
+| parameter_mappings | []map | Mapeo de filtros a la card |
+| visualization_settings | map | Settings de visualizacion |
+
+Usar estos datos para construir el payload de MetabaseUpdateDashboard.
diff --git a/functions/infra/metabase_get_user.go b/functions/infra/metabase_get_user.go
new file mode 100644
index 00000000..308f47d9
--- /dev/null
+++ b/functions/infra/metabase_get_user.go
@@ -0,0 +1,15 @@
+package infra
+
+import "fmt"
+
+// MetabaseGetUser obtiene un usuario de Metabase por su ID.
+func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error) {
+ path := fmt.Sprintf("/api/user/%d", userID)
+
+ result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return nil, fmt.Errorf("metabase get user %d: %w", userID, err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_get_user.md b/functions/infra/metabase_get_user.md
new file mode 100644
index 00000000..e884f25c
--- /dev/null
+++ b/functions/infra/metabase_get_user.md
@@ -0,0 +1,51 @@
+---
+name: metabase_get_user
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error)"
+description: "Obtiene los detalles de un usuario de Metabase por su ID numerico. Endpoint: GET /api/user/:id."
+tags: [metabase, user, get, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_get_user.go"
+---
+
+## Ejemplo
+
+```go
+user, err := MetabaseGetUser(client, 1)
+if err != nil {
+ log.Fatal(err)
+}
+fmt.Println(user["email"], user["first_name"])
+```
+
+## Notas
+
+Retorna el objeto usuario completo como map. Error 404 si el ID no existe.
+
+### Campos del usuario retornado
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| id | float64 | ID numerico |
+| email | string | Email |
+| first_name | string | Nombre |
+| last_name | string | Apellido |
+| is_superuser | bool | Es admin |
+| is_active | bool | Esta activo |
+| common_name | string | Nombre completo |
+| date_joined | string | Fecha de creacion |
+| last_login | string | Ultimo login |
+| group_ids | []float64 | IDs de grupos |
+| locale | string | Locale del usuario |
diff --git a/functions/infra/metabase_http.go b/functions/infra/metabase_http.go
new file mode 100644
index 00000000..d894bebe
--- /dev/null
+++ b/functions/infra/metabase_http.go
@@ -0,0 +1,107 @@
+package infra
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+// metabaseRequest ejecuta una peticion HTTP contra la API de Metabase.
+// method: GET, POST, PUT, DELETE
+// baseURL: URL base sin trailing slash
+// token: session token o API key
+// path: ruta relativa (ej: "/api/user")
+// body: payload JSON (nil para requests sin body)
+// Retorna el body deserializado como map o nil si el body esta vacio.
+func metabaseRequest(method, baseURL, token, path string, body map[string]any) (map[string]any, error) {
+ var reqBody io.Reader
+ if body != nil {
+ data, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("marshal body: %w", err)
+ }
+ reqBody = bytes.NewReader(data)
+ }
+
+ req, err := http.NewRequest(method, baseURL+path, reqBody)
+ if err != nil {
+ return nil, fmt.Errorf("new request: %w", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-Metabase-Session", token)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("http %s %s: %w", method, path, err)
+ }
+ defer resp.Body.Close()
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("read response: %w", err)
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return nil, fmt.Errorf("metabase %s %s: status %d: %s", method, path, resp.StatusCode, string(respBody))
+ }
+
+ if len(respBody) == 0 {
+ return nil, nil
+ }
+
+ var result map[string]any
+ if err := json.Unmarshal(respBody, &result); err != nil {
+ return nil, fmt.Errorf("unmarshal response: %w", err)
+ }
+
+ return result, nil
+}
+
+// metabaseRequestList es como metabaseRequest pero para endpoints que retornan un array JSON.
+func metabaseRequestList(method, baseURL, token, path string, body map[string]any) ([]map[string]any, error) {
+ var reqBody io.Reader
+ if body != nil {
+ data, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("marshal body: %w", err)
+ }
+ reqBody = bytes.NewReader(data)
+ }
+
+ req, err := http.NewRequest(method, baseURL+path, reqBody)
+ if err != nil {
+ return nil, fmt.Errorf("new request: %w", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-Metabase-Session", token)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("http %s %s: %w", method, path, err)
+ }
+ defer resp.Body.Close()
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("read response: %w", err)
+ }
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return nil, fmt.Errorf("metabase %s %s: status %d: %s", method, path, resp.StatusCode, string(respBody))
+ }
+
+ if len(respBody) == 0 {
+ return nil, nil
+ }
+
+ var result []map[string]any
+ if err := json.Unmarshal(respBody, &result); err != nil {
+ return nil, fmt.Errorf("unmarshal response: %w", err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_list_cards.go b/functions/infra/metabase_list_cards.go
new file mode 100644
index 00000000..ba1f5cd8
--- /dev/null
+++ b/functions/infra/metabase_list_cards.go
@@ -0,0 +1,25 @@
+package infra
+
+import "fmt"
+
+// MetabaseListCards lista preguntas/cards de Metabase.
+// filter: "all", "mine", "fav", "archived", "recent", "popular", "database", "table" (vacio = todas).
+// modelID: ID de database o tabla cuando filter es "database" o "table" (0 = ignorar).
+func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error) {
+ path := "/api/card"
+ sep := "?"
+ if filter != "" {
+ path += sep + "f=" + filter
+ sep = "&"
+ }
+ if modelID > 0 {
+ path += fmt.Sprintf("%smodel_id=%d", sep, modelID)
+ }
+
+ result, err := metabaseRequestList("GET", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return nil, fmt.Errorf("metabase list cards: %w", err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_list_cards.md b/functions/infra/metabase_list_cards.md
new file mode 100644
index 00000000..91656b15
--- /dev/null
+++ b/functions/infra/metabase_list_cards.md
@@ -0,0 +1,63 @@
+---
+name: metabase_list_cards
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error)"
+description: "Lista preguntas/cards de Metabase con filtro opcional. Retorna array de cards. Endpoint: GET /api/card."
+tags: [metabase, card, question, list, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_list_cards.go"
+---
+
+## Ejemplo
+
+```go
+// Listar todas las cards
+cards, err := MetabaseListCards(client, "all", 0)
+
+// Solo mis preguntas
+cards, err := MetabaseListCards(client, "mine", 0)
+
+// Cards de una database especifica
+cards, err := MetabaseListCards(client, "database", 1)
+
+// Cards archivadas
+cards, err := MetabaseListCards(client, "archived", 0)
+```
+
+## Notas
+
+### Parametros para un LLM
+
+| Parametro | Tipo | Requerido | Descripcion |
+|-----------|------|-----------|-------------|
+| client | MetabaseClient | si | Cliente autenticado |
+| filter | string | no | "all", "mine", "fav", "archived", "recent", "popular", "database", "table". Vacio = todas |
+| modelID | int | no | ID de database/tabla. Solo aplica con filter "database" o "table". 0 = ignorar |
+
+No tiene paginacion con offset/limit. Retorna todas las cards que coinciden.
+
+### Campos principales de cada card
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| id | float64 | ID numerico de la card |
+| name | string | Nombre de la pregunta |
+| description | string | Descripcion |
+| display | string | Tipo de visualizacion (table, bar, line, pie, etc.) |
+| collection_id | float64 | ID de la coleccion/carpeta |
+| database_id | float64 | ID de la database |
+| creator_id | float64 | ID del creador |
+| archived | bool | Esta archivada |
+| dataset_query | map | Query de la card (native o structured) |
diff --git a/functions/infra/metabase_list_dashboards.go b/functions/infra/metabase_list_dashboards.go
new file mode 100644
index 00000000..b541f39e
--- /dev/null
+++ b/functions/infra/metabase_list_dashboards.go
@@ -0,0 +1,19 @@
+package infra
+
+import "fmt"
+
+// MetabaseListDashboards lista dashboards de Metabase.
+// filter: "all", "mine" o "archived" (vacio = todas).
+func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error) {
+ path := "/api/dashboard"
+ if filter != "" {
+ path += "?f=" + filter
+ }
+
+ result, err := metabaseRequestList("GET", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return nil, fmt.Errorf("metabase list dashboards: %w", err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_list_dashboards.md b/functions/infra/metabase_list_dashboards.md
new file mode 100644
index 00000000..4c588e51
--- /dev/null
+++ b/functions/infra/metabase_list_dashboards.md
@@ -0,0 +1,57 @@
+---
+name: metabase_list_dashboards
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error)"
+description: "Lista dashboards de Metabase con filtro opcional. Retorna array de dashboards resumidos (sin dashcards). Endpoint: GET /api/dashboard."
+tags: [metabase, dashboard, list, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_list_dashboards.go"
+---
+
+## Ejemplo
+
+```go
+// Listar todos los dashboards
+dashboards, err := MetabaseListDashboards(client, "all")
+
+// Solo mis dashboards
+dashboards, err := MetabaseListDashboards(client, "mine")
+
+// Dashboards archivados
+dashboards, err := MetabaseListDashboards(client, "archived")
+```
+
+## Notas
+
+### Parametros para un LLM
+
+| Parametro | Tipo | Requerido | Descripcion |
+|-----------|------|-----------|-------------|
+| client | MetabaseClient | si | Cliente autenticado |
+| filter | string | no | "all", "mine", "archived". Vacio = todas |
+
+Retorna dashboards resumidos (sin cards). Para ver las cards de un dashboard, usar MetabaseGetDashboard.
+
+### Campos principales de cada dashboard
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| id | float64 | ID del dashboard |
+| name | string | Nombre |
+| description | string | Descripcion |
+| collection_id | float64 | Coleccion contenedora |
+| creator_id | float64 | ID del creador |
+| archived | bool | Archivado |
+| created_at | string | Fecha creacion |
diff --git a/functions/infra/metabase_list_users.go b/functions/infra/metabase_list_users.go
new file mode 100644
index 00000000..fe6971bd
--- /dev/null
+++ b/functions/infra/metabase_list_users.go
@@ -0,0 +1,30 @@
+package infra
+
+import "fmt"
+
+// MetabaseListUsers lista usuarios de Metabase con filtros opcionales.
+// status: "active", "deactivated" o "all" (vacio = "active").
+// query: filtro por nombre o email (vacio = sin filtro).
+// limit/offset: paginacion (0 = valores por defecto de Metabase).
+func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error) {
+ path := "/api/user?"
+ if status != "" {
+ path += "status=" + status + "&"
+ }
+ if query != "" {
+ path += "query=" + query + "&"
+ }
+ if limit > 0 {
+ path += fmt.Sprintf("limit=%d&", limit)
+ }
+ if offset > 0 {
+ path += fmt.Sprintf("offset=%d&", offset)
+ }
+
+ result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
+ if err != nil {
+ return nil, fmt.Errorf("metabase list users: %w", err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_list_users.md b/functions/infra/metabase_list_users.md
new file mode 100644
index 00000000..764ef170
--- /dev/null
+++ b/functions/infra/metabase_list_users.md
@@ -0,0 +1,67 @@
+---
+name: metabase_list_users
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error)"
+description: "Lista usuarios de una instancia Metabase con filtros opcionales por estado, nombre/email y paginacion. Endpoint: GET /api/user. Requiere permisos de superusuario."
+tags: [metabase, user, list, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_list_users.go"
+---
+
+## Ejemplo
+
+```go
+client, _ := MetabaseAuth("http://localhost:3000", "admin@example.com", "pass")
+
+// Listar todos los usuarios activos
+users, err := MetabaseListUsers(client, "active", "", 0, 0)
+
+// Buscar usuario por email
+users, err := MetabaseListUsers(client, "", "john@", 10, 0)
+
+// Listar desactivados
+users, err := MetabaseListUsers(client, "deactivated", "", 25, 0)
+```
+
+## Notas
+
+Retorna un map con la estructura paginada de Metabase:
+- `data`: array de objetos usuario (id, email, first_name, last_name, is_superuser, etc.)
+- `total`: numero total de usuarios que coinciden
+- `limit`: tamanio de pagina usado
+- `offset`: offset usado
+
+### Parametros para un LLM
+
+| Parametro | Tipo | Requerido | Descripcion |
+|-----------|------|-----------|-------------|
+| client | MetabaseClient | si | Cliente autenticado |
+| status | string | no | "active" (default), "deactivated", "all" |
+| query | string | no | Filtro por nombre o email |
+| limit | int | no | Tamanio de pagina (0 = default Metabase) |
+| offset | int | no | Offset para paginacion (0 = inicio) |
+
+### Campos del usuario retornado
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| id | float64 | ID numerico del usuario |
+| email | string | Email unico |
+| first_name | string | Nombre |
+| last_name | string | Apellido |
+| is_superuser | bool | Es admin |
+| is_active | bool | Esta activo |
+| common_name | string | Nombre completo |
+| last_login | string | Fecha ultimo login |
diff --git a/functions/infra/metabase_update_card.go b/functions/infra/metabase_update_card.go
new file mode 100644
index 00000000..288bf8d0
--- /dev/null
+++ b/functions/infra/metabase_update_card.go
@@ -0,0 +1,18 @@
+package infra
+
+import "fmt"
+
+// MetabaseUpdateCard actualiza campos de una card/pregunta en Metabase.
+// fields es un map con los campos a actualizar.
+// Campos comunes: name, description, display, dataset_query, visualization_settings,
+// collection_id, archived, enable_embedding.
+func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error) {
+ path := fmt.Sprintf("/api/card/%d", cardID)
+
+ result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
+ if err != nil {
+ return nil, fmt.Errorf("metabase update card %d: %w", cardID, err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_update_card.md b/functions/infra/metabase_update_card.md
new file mode 100644
index 00000000..625d7e53
--- /dev/null
+++ b/functions/infra/metabase_update_card.md
@@ -0,0 +1,69 @@
+---
+name: metabase_update_card
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error)"
+description: "Actualiza campos de una card/pregunta en Metabase. Solo se modifican los campos incluidos en el map. Endpoint: PUT /api/card/:id."
+tags: [metabase, card, question, update, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_update_card.go"
+---
+
+## Ejemplo
+
+```go
+// Cambiar nombre y descripcion
+card, err := MetabaseUpdateCard(client, 42, map[string]any{
+ "name": "Updated Revenue Chart",
+ "description": "Now includes refunds",
+})
+
+// Archivar una card (soft-delete)
+card, err := MetabaseUpdateCard(client, 42, map[string]any{
+ "archived": true,
+})
+
+// Mover a otra coleccion
+card, err := MetabaseUpdateCard(client, 42, map[string]any{
+ "collection_id": 10,
+})
+
+// Cambiar la query SQL
+card, err := MetabaseUpdateCard(client, 42, map[string]any{
+ "dataset_query": map[string]any{
+ "database": 1,
+ "type": "native",
+ "native": map[string]any{"query": "SELECT * FROM users LIMIT 100"},
+ },
+})
+```
+
+## Notas
+
+### Campos actualizables
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| name | string | Nombre de la pregunta |
+| description | string | Descripcion |
+| display | string | Tipo de visualizacion |
+| dataset_query | map | Query SQL o MBQL |
+| visualization_settings | map | Config de visualizacion |
+| collection_id | int | Mover a otra coleccion |
+| archived | bool | Archivar/desarchivar (soft-delete) |
+| enable_embedding | bool | Habilitar embedding publico |
+| embedding_params | map | Parametros de embedding |
+
+Solo incluir los campos que se quieren cambiar.
+Para eliminar permanentemente usar MetabaseDeleteCard. Para soft-delete usar archived: true.
diff --git a/functions/infra/metabase_update_dashboard.go b/functions/infra/metabase_update_dashboard.go
new file mode 100644
index 00000000..79053b72
--- /dev/null
+++ b/functions/infra/metabase_update_dashboard.go
@@ -0,0 +1,23 @@
+package infra
+
+import "fmt"
+
+// MetabaseUpdateDashboard actualiza un dashboard en Metabase.
+// fields puede incluir metadata del dashboard Y/O la lista completa de dashcards y tabs.
+//
+// Para gestionar cards en el dashboard, incluir "dashcards" en fields:
+// - Agregar card: incluirla con ID negativo (ej: -1, -2)
+// - Actualizar card: incluirla con su ID positivo existente
+// - Eliminar card: omitirla del array (el array es el estado deseado completo)
+//
+// Campos comunes: name, description, archived, parameters, dashcards, tabs, collection_id.
+func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error) {
+ path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
+
+ result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
+ if err != nil {
+ return nil, fmt.Errorf("metabase update dashboard %d: %w", dashboardID, err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_update_dashboard.md b/functions/infra/metabase_update_dashboard.md
new file mode 100644
index 00000000..5e384d06
--- /dev/null
+++ b/functions/infra/metabase_update_dashboard.md
@@ -0,0 +1,110 @@
+---
+name: metabase_update_dashboard
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error)"
+description: "Actualiza un dashboard en Metabase incluyendo metadata, cards y tabs. El campo dashcards representa el estado completo deseado: cards nuevas con ID negativo, existentes con ID positivo, omitidas se eliminan. Endpoint: PUT /api/dashboard/:id."
+tags: [metabase, dashboard, update, cards, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_update_dashboard.go"
+---
+
+## Ejemplo
+
+```go
+// Cambiar nombre
+MetabaseUpdateDashboard(client, 1, map[string]any{
+ "name": "Updated Dashboard",
+})
+
+// Agregar una card al dashboard
+// Primero obtener las dashcards existentes
+dash, _ := MetabaseGetDashboard(client, 1)
+existingCards := dash["dashcards"].([]any)
+
+// Construir nuevo array con las existentes + la nueva
+dashcards := make([]map[string]any, 0)
+for _, dc := range existingCards {
+ dashcards = append(dashcards, dc.(map[string]any))
+}
+// Agregar nueva card (ID negativo = nueva)
+dashcards = append(dashcards, map[string]any{
+ "id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0,
+})
+
+MetabaseUpdateDashboard(client, 1, map[string]any{
+ "dashcards": dashcards,
+})
+
+// Archivar dashboard (soft-delete)
+MetabaseUpdateDashboard(client, 1, map[string]any{"archived": true})
+```
+
+## Notas
+
+### Gestion de dashcards (IMPORTANTE)
+
+El array `dashcards` representa el **estado completo deseado** del dashboard:
+
+| Accion | Como hacerlo |
+|--------|-------------|
+| Agregar card | Incluir con **ID negativo** (-1, -2, etc.) |
+| Actualizar card | Incluir con su **ID positivo** existente |
+| Eliminar card | **Omitir** del array |
+| No cambiar cards | No incluir el campo dashcards |
+
+**Flujo tipico para agregar una card:**
+1. `MetabaseGetDashboard` para obtener dashcards existentes
+2. Copiar las existentes al nuevo array
+3. Agregar la nueva con ID negativo
+4. Enviar el array completo
+
+### Estructura de una dashcard
+
+```go
+map[string]any{
+ "id": -1, // negativo = nueva, positivo = existente
+ "card_id": 42, // ID de la card/pregunta
+ "size_x": 6, // ancho (1-18)
+ "size_y": 4, // alto
+ "col": 0, // columna (0-based)
+ "row": 0, // fila (0-based)
+ "dashboard_tab_id": nil, // tab (nil = sin tabs)
+ "parameter_mappings": []map[string]any{}, // mapeo de filtros
+ "visualization_settings": map[string]any{}, // settings custom
+}
+```
+
+### Gestion de tabs
+
+```go
+map[string]any{
+ "tabs": []map[string]any{
+ {"id": 1, "name": "Overview"}, // tab existente
+ {"id": -1, "name": "Details"}, // tab nuevo (ID negativo)
+ },
+}
+```
+
+### Campos actualizables
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| name | string | Nombre del dashboard |
+| description | string | Descripcion |
+| archived | bool | Archivar/desarchivar |
+| dashcards | []map | Estado completo de cards |
+| tabs | []map | Tabs del dashboard |
+| parameters | []map | Filtros del dashboard |
+| collection_id | int | Mover a otra coleccion |
diff --git a/functions/infra/metabase_update_user.go b/functions/infra/metabase_update_user.go
new file mode 100644
index 00000000..5bd2677f
--- /dev/null
+++ b/functions/infra/metabase_update_user.go
@@ -0,0 +1,17 @@
+package infra
+
+import "fmt"
+
+// MetabaseUpdateUser actualiza campos de un usuario en Metabase.
+// fields es un map con los campos a actualizar. Campos validos:
+// first_name, last_name, email, is_superuser, group_ids, locale, login_attributes.
+func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error) {
+ path := fmt.Sprintf("/api/user/%d", userID)
+
+ result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
+ if err != nil {
+ return nil, fmt.Errorf("metabase update user %d: %w", userID, err)
+ }
+
+ return result, nil
+}
diff --git a/functions/infra/metabase_update_user.md b/functions/infra/metabase_update_user.md
new file mode 100644
index 00000000..3976f233
--- /dev/null
+++ b/functions/infra/metabase_update_user.md
@@ -0,0 +1,58 @@
+---
+name: metabase_update_user
+kind: function
+lang: go
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error)"
+description: "Actualiza campos de un usuario en Metabase. Solo se modifican los campos incluidos en el map. Requiere permisos de superusuario. Endpoint: PUT /api/user/:id."
+tags: [metabase, user, update, api]
+uses_functions: []
+uses_types: [MetabaseClient_go_infra]
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [fmt]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "functions/infra/metabase_update_user.go"
+---
+
+## Ejemplo
+
+```go
+// Cambiar nombre
+user, err := MetabaseUpdateUser(client, 5, map[string]any{
+ "first_name": "Jane",
+ "last_name": "Smith",
+})
+
+// Promover a admin
+user, err := MetabaseUpdateUser(client, 5, map[string]any{
+ "is_superuser": true,
+})
+
+// Cambiar grupos
+user, err := MetabaseUpdateUser(client, 5, map[string]any{
+ "group_ids": []int{1, 3, 5},
+})
+```
+
+## Notas
+
+### Campos actualizables
+
+| Campo | Tipo | Descripcion |
+|-------|------|-------------|
+| first_name | string | Nombre |
+| last_name | string | Apellido |
+| email | string | Email (debe ser unico) |
+| is_superuser | bool | Permisos de admin |
+| group_ids | []int | IDs de grupos del usuario |
+| locale | string | Locale (ej: "es", "en") |
+| login_attributes | map | Atributos para sandboxing |
+
+Solo incluir los campos que se quieren cambiar. Los demas se mantienen sin modificar.
+Retorna el objeto usuario actualizado.
diff --git a/functions/infra/types.go b/functions/infra/types.go
index 486f79b1..9d515777 100644
--- a/functions/infra/types.go
+++ b/functions/infra/types.go
@@ -20,3 +20,9 @@ type ImageInfo struct {
Size string
Created string
}
+
+// MetabaseClient holds the connection details for a Metabase instance API.
+type MetabaseClient struct {
+ BaseURL string // e.g. "http://localhost:3000"
+ Token string // session token or API key
+}
diff --git a/python/.gitignore b/python/.gitignore
new file mode 100644
index 00000000..c18dd8d8
--- /dev/null
+++ b/python/.gitignore
@@ -0,0 +1 @@
+__pycache__/
diff --git a/python/.python-version b/python/.python-version
new file mode 100644
index 00000000..e4fba218
--- /dev/null
+++ b/python/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/functions/components/.gitkeep b/python/functions/__init__.py
similarity index 100%
rename from functions/components/.gitkeep
rename to python/functions/__init__.py
diff --git a/python/functions/metabase/__init__.py b/python/functions/metabase/__init__.py
new file mode 100644
index 00000000..bccd53c3
--- /dev/null
+++ b/python/functions/metabase/__init__.py
@@ -0,0 +1,11 @@
+from .client import MetabaseClient
+from .users import metabase_list_users, metabase_get_user, metabase_create_user, metabase_update_user, metabase_deactivate_user
+from .cards import metabase_list_cards, metabase_get_card, metabase_create_card, metabase_update_card, metabase_delete_card, metabase_execute_card, metabase_execute_query
+from .dashboards import metabase_list_dashboards, metabase_get_dashboard, metabase_create_dashboard, metabase_update_dashboard, metabase_delete_dashboard
+
+__all__ = [
+ "MetabaseClient",
+ "metabase_list_users", "metabase_get_user", "metabase_create_user", "metabase_update_user", "metabase_deactivate_user",
+ "metabase_list_cards", "metabase_get_card", "metabase_create_card", "metabase_update_card", "metabase_delete_card", "metabase_execute_card", "metabase_execute_query",
+ "metabase_list_dashboards", "metabase_get_dashboard", "metabase_create_dashboard", "metabase_update_dashboard", "metabase_delete_dashboard",
+]
diff --git a/python/functions/metabase/cards.py b/python/functions/metabase/cards.py
new file mode 100644
index 00000000..de22cd3e
--- /dev/null
+++ b/python/functions/metabase/cards.py
@@ -0,0 +1,227 @@
+"""CRUD de cards/preguntas de Metabase y ejecucion de queries."""
+
+from .client import MetabaseClient
+
+
+def metabase_list_cards(
+ client: MetabaseClient,
+ filter: str = "",
+ model_id: int = 0,
+) -> list[dict]:
+ """Lista preguntas/cards de Metabase con filtro opcional.
+
+ Endpoint: GET /api/card. No tiene paginacion offset/limit.
+
+ Args:
+ client: Cliente autenticado.
+ filter: "all", "mine", "fav", "archived", "recent", "popular",
+ "database", "table". Vacio = todas.
+ model_id: ID de database/tabla. Solo aplica con filter "database" o "table".
+
+ Returns:
+ Lista de dicts, cada uno con: id, name, description, display,
+ collection_id, database_id, creator_id, archived, dataset_query.
+
+ Example:
+ >>> cards = metabase_list_cards(client, filter="mine")
+ >>> for c in cards:
+ ... print(c["id"], c["name"], c["display"])
+ """
+ params = {}
+ if filter:
+ params["f"] = filter
+ if model_id > 0:
+ params["model_id"] = model_id
+ return client.request("GET", "/api/card", params=params)
+
+
+def metabase_get_card(client: MetabaseClient, card_id: int) -> dict:
+ """Obtiene los detalles completos de una card/pregunta.
+
+ Endpoint: GET /api/card/:id.
+
+ Args:
+ client: Cliente autenticado.
+ card_id: ID de la card.
+
+ Returns:
+ Dict con: id, name, description, display, dataset_query,
+ visualization_settings, collection_id, database_id, archived,
+ creator, created_at, updated_at.
+
+ Example:
+ >>> card = metabase_get_card(client, 42)
+ >>> print(card["name"], card["display"])
+ >>> print(card["dataset_query"]["native"]["query"]) # SQL
+ """
+ return client.request("GET", f"/api/card/{card_id}")
+
+
+def metabase_create_card(
+ client: MetabaseClient,
+ name: str,
+ dataset_query: dict,
+ display: str = "table",
+ collection_id: int = 0,
+ description: str = "",
+) -> dict:
+ """Crea una nueva card/pregunta en Metabase.
+
+ Endpoint: POST /api/card.
+
+ Args:
+ client: Cliente autenticado.
+ name: Nombre de la pregunta.
+ dataset_query: Query de la card. Estructura:
+ SQL nativo: {"database": 1, "type": "native", "native": {"query": "SELECT ..."}}
+ MBQL: {"database": 1, "type": "query", "query": {"source-table": 4, ...}}
+ display: Tipo de visualizacion: "table", "bar", "line", "pie", "scalar",
+ "area", "row", "combo", "funnel", "scatter", "waterfall", etc.
+ collection_id: ID de coleccion destino. 0 = root.
+ description: Descripcion opcional.
+
+ Returns:
+ Dict con la card creada.
+
+ Example:
+ >>> card = metabase_create_card(client, "Revenue by Month", {
+ ... "database": 1,
+ ... "type": "native",
+ ... "native": {"query": "SELECT date_trunc('month', created_at), SUM(total) FROM orders GROUP BY 1"},
+ ... }, display="line", description="Monthly revenue trend")
+ """
+ body: dict = {
+ "name": name,
+ "dataset_query": dataset_query,
+ "display": display,
+ "visualization_settings": {},
+ }
+ if collection_id > 0:
+ body["collection_id"] = collection_id
+ if description:
+ body["description"] = description
+ return client.request("POST", "/api/card", json=body)
+
+
+def metabase_update_card(client: MetabaseClient, card_id: int, **fields) -> dict:
+ """Actualiza campos de una card/pregunta en Metabase.
+
+ Endpoint: PUT /api/card/:id. Solo se modifican los campos pasados.
+
+ Args:
+ client: Cliente autenticado.
+ card_id: ID de la card.
+ **fields: Campos a actualizar. Validos:
+ name (str), description (str), display (str),
+ dataset_query (dict), visualization_settings (dict),
+ collection_id (int), archived (bool),
+ enable_embedding (bool), embedding_params (dict).
+
+ Returns:
+ Dict con la card actualizada.
+
+ Example:
+ >>> metabase_update_card(client, 42, name="Updated Name", archived=True)
+ >>> metabase_update_card(client, 42, dataset_query={
+ ... "database": 1, "type": "native",
+ ... "native": {"query": "SELECT * FROM users LIMIT 100"},
+ ... })
+ """
+ return client.request("PUT", f"/api/card/{card_id}", json=fields)
+
+
+def metabase_delete_card(client: MetabaseClient, card_id: int) -> None:
+ """Elimina permanentemente una card/pregunta.
+
+ Endpoint: DELETE /api/card/:id. IRREVERSIBLE.
+ Para soft-delete preferir: metabase_update_card(client, card_id, archived=True)
+
+ Args:
+ client: Cliente autenticado.
+ card_id: ID de la card a eliminar.
+
+ Example:
+ >>> metabase_delete_card(client, 42)
+ >>> # Preferir soft-delete: metabase_update_card(client, 42, archived=True)
+ """
+ client.request("DELETE", f"/api/card/{card_id}")
+
+
+def metabase_execute_card(
+ client: MetabaseClient,
+ card_id: int,
+ parameters: list[dict] | None = None,
+) -> dict:
+ """Ejecuta la query de una card/pregunta guardada.
+
+ Endpoint: POST /api/card/:id/query.
+
+ Args:
+ client: Cliente autenticado.
+ card_id: ID de la card a ejecutar.
+ parameters: Parametros para queries parametrizadas. Cada parametro:
+ {"type": "category", "target": ["variable", ["template-tag", "tag"]], "value": "val"}
+
+ Returns:
+ Dict con resultados:
+ - status: "completed" o "failed"
+ - row_count: numero de filas
+ - running_time: tiempo en ms
+ - data.columns: nombres de columnas
+ - data.rows: filas de datos (lista de listas)
+ - data.cols: metadata de columnas
+ - data.native_form.query: SQL ejecutado
+
+ Example:
+ >>> result = metabase_execute_card(client, 42)
+ >>> for row in result["data"]["rows"]:
+ ... print(row)
+ >>> # Con parametros:
+ >>> result = metabase_execute_card(client, 42, parameters=[
+ ... {"type": "category", "target": ["variable", ["template-tag", "status"]], "value": "active"},
+ ... ])
+ """
+ body = {}
+ if parameters:
+ body["parameters"] = parameters
+ return client.request("POST", f"/api/card/{card_id}/query", json=body or None)
+
+
+def metabase_execute_query(
+ client: MetabaseClient,
+ database_id: int,
+ sql: str,
+ max_results: int = 0,
+) -> dict:
+ """Ejecuta una query SQL ad-hoc sin guardarla como card.
+
+ Endpoint: POST /api/dataset. Util para exploracion rapida y consultas
+ que no necesitan persistirse.
+
+ Args:
+ client: Cliente autenticado.
+ database_id: ID de la database en Metabase.
+ sql: Query SQL a ejecutar.
+ max_results: Limite de filas. 0 = default 2000.
+
+ Returns:
+ Dict con misma estructura que metabase_execute_card:
+ data.columns, data.rows, row_count, running_time, status.
+
+ Example:
+ >>> result = metabase_execute_query(client, 1, "SELECT * FROM users LIMIT 10")
+ >>> print(f"{result['row_count']} filas en {result['running_time']}ms")
+ >>> for row in result["data"]["rows"]:
+ ... print(row)
+ """
+ body: dict = {
+ "database": database_id,
+ "type": "native",
+ "native": {"query": sql},
+ }
+ if max_results > 0:
+ body["constraints"] = {
+ "max-results": max_results,
+ "max-results-bare-rows": max_results,
+ }
+ return client.request("POST", "/api/dataset", json=body)
diff --git a/python/functions/metabase/client.py b/python/functions/metabase/client.py
new file mode 100644
index 00000000..a3b4ceaf
--- /dev/null
+++ b/python/functions/metabase/client.py
@@ -0,0 +1,87 @@
+"""Cliente base para la API REST de Metabase."""
+
+import httpx
+
+
+class MetabaseClient:
+ """Cliente HTTP para una instancia Metabase.
+
+ Attributes:
+ base_url: URL base sin trailing slash (ej: "http://localhost:3000").
+ token: Session token o API key.
+ _http: Cliente httpx reutilizable con headers de auth.
+ """
+
+ def __init__(self, base_url: str, token: str) -> None:
+ self.base_url = base_url.rstrip("/")
+ self.token = token
+ self._http = httpx.Client(
+ base_url=self.base_url,
+ headers={
+ "Content-Type": "application/json",
+ "X-Metabase-Session": token,
+ },
+ timeout=30.0,
+ )
+
+ def request(self, method: str, path: str, **kwargs) -> dict | list | None:
+ """Ejecuta una peticion HTTP contra la API de Metabase.
+
+ Args:
+ method: HTTP method (GET, POST, PUT, DELETE).
+ path: Ruta relativa (ej: "/api/user").
+ **kwargs: Argumentos extra para httpx (json, params, etc.).
+
+ Returns:
+ Respuesta deserializada como dict/list, o None si el body esta vacio.
+
+ Raises:
+ httpx.HTTPStatusError: Si el status code no es 2xx.
+ """
+ resp = self._http.request(method, path, **kwargs)
+ resp.raise_for_status()
+ if not resp.content:
+ return None
+ return resp.json()
+
+ def close(self) -> None:
+ """Cierra el cliente HTTP."""
+ self._http.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.close()
+
+
+def metabase_auth(base_url: str, email: str, password: str) -> MetabaseClient:
+ """Autentica contra Metabase con email y password.
+
+ Crea una sesion via POST /api/session y retorna un MetabaseClient
+ con el session token listo para usar. El token expira en 14 dias
+ por defecto (configurable con MAX_SESSION_AGE en Metabase).
+
+ Args:
+ base_url: URL base de la instancia (ej: "http://localhost:3000").
+ email: Email del usuario Metabase.
+ password: Password del usuario.
+
+ Returns:
+ MetabaseClient autenticado con session token.
+
+ Raises:
+ httpx.HTTPStatusError: Si las credenciales son invalidas (401)
+ o hay rate limiting.
+
+ Example:
+ >>> client = metabase_auth("http://localhost:3000", "admin@example.com", "pass")
+ >>> # client listo para usar con todas las funciones CRUD
+ """
+ resp = httpx.post(
+ f"{base_url.rstrip('/')}/api/session",
+ json={"username": email, "password": password},
+ )
+ resp.raise_for_status()
+ token = resp.json()["id"]
+ return MetabaseClient(base_url, token)
diff --git a/python/functions/metabase/dashboards.py b/python/functions/metabase/dashboards.py
new file mode 100644
index 00000000..f298c7ae
--- /dev/null
+++ b/python/functions/metabase/dashboards.py
@@ -0,0 +1,143 @@
+"""CRUD de dashboards de Metabase."""
+
+from .client import MetabaseClient
+
+
+def metabase_list_dashboards(
+ client: MetabaseClient,
+ filter: str = "",
+) -> list[dict]:
+ """Lista dashboards de Metabase con filtro opcional.
+
+ Endpoint: GET /api/dashboard. Retorna dashboards resumidos (sin dashcards).
+
+ Args:
+ client: Cliente autenticado.
+ filter: "all", "mine" o "archived". Vacio = todas.
+
+ Returns:
+ Lista de dicts con: id, name, description, collection_id,
+ creator_id, archived, created_at.
+
+ Example:
+ >>> dashboards = metabase_list_dashboards(client, filter="mine")
+ >>> for d in dashboards:
+ ... print(d["id"], d["name"])
+ """
+ params = {}
+ if filter:
+ params["f"] = filter
+ return client.request("GET", "/api/dashboard", params=params)
+
+
+def metabase_get_dashboard(client: MetabaseClient, dashboard_id: int) -> dict:
+ """Obtiene un dashboard completo incluyendo sus cards.
+
+ Endpoint: GET /api/dashboard/:id.
+
+ Args:
+ client: Cliente autenticado.
+ dashboard_id: ID del dashboard.
+
+ Returns:
+ Dict con: id, name, description, dashcards (lista de cards posicionadas),
+ parameters (filtros), tabs, collection_id, archived.
+
+ Cada dashcard tiene: id, card_id, card (objeto completo), size_x, size_y,
+ col, row, dashboard_tab_id, parameter_mappings, visualization_settings.
+
+ Example:
+ >>> dash = metabase_get_dashboard(client, 1)
+ >>> for dc in dash["dashcards"]:
+ ... print(f"Card {dc['card_id']} at ({dc['col']}, {dc['row']})")
+ """
+ return client.request("GET", f"/api/dashboard/{dashboard_id}")
+
+
+def metabase_create_dashboard(
+ client: MetabaseClient,
+ name: str,
+ description: str = "",
+ collection_id: int = 0,
+) -> dict:
+ """Crea un nuevo dashboard vacio en Metabase.
+
+ Endpoint: POST /api/dashboard.
+ Para agregar cards usar metabase_update_dashboard con dashcards.
+
+ Args:
+ client: Cliente autenticado.
+ name: Nombre del dashboard.
+ description: Descripcion opcional.
+ collection_id: Coleccion destino. 0 = root.
+
+ Returns:
+ Dict con el dashboard creado.
+
+ Example:
+ >>> dash = metabase_create_dashboard(client, "Sales Overview", "KPIs de ventas")
+ >>> # Agregar cards:
+ >>> metabase_update_dashboard(client, dash["id"], dashcards=[
+ ... {"id": -1, "card_id": 42, "size_x": 6, "size_y": 4, "col": 0, "row": 0},
+ ... ])
+ """
+ body: dict = {"name": name}
+ if description:
+ body["description"] = description
+ if collection_id > 0:
+ body["collection_id"] = collection_id
+ return client.request("POST", "/api/dashboard", json=body)
+
+
+def metabase_update_dashboard(client: MetabaseClient, dashboard_id: int, **fields) -> dict:
+ """Actualiza un dashboard incluyendo metadata, cards y tabs.
+
+ Endpoint: PUT /api/dashboard/:id.
+
+ El campo dashcards representa el ESTADO COMPLETO DESEADO del dashboard:
+ - Agregar card: incluirla con ID negativo (-1, -2, etc.)
+ - Actualizar card existente: incluirla con su ID positivo
+ - Eliminar card: omitirla del array
+
+ Args:
+ client: Cliente autenticado.
+ dashboard_id: ID del dashboard.
+ **fields: Campos a actualizar. Validos:
+ name (str), description (str), archived (bool),
+ dashcards (list[dict]), tabs (list[dict]),
+ parameters (list[dict]), collection_id (int).
+
+ Returns:
+ Dict con el dashboard actualizado.
+
+ Example:
+ >>> # Cambiar nombre
+ >>> metabase_update_dashboard(client, 1, name="Updated Name")
+ >>>
+ >>> # Agregar card (primero obtener existentes)
+ >>> dash = metabase_get_dashboard(client, 1)
+ >>> cards = list(dash["dashcards"])
+ >>> cards.append({"id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0})
+ >>> metabase_update_dashboard(client, 1, dashcards=cards)
+ >>>
+ >>> # Archivar (soft-delete)
+ >>> metabase_update_dashboard(client, 1, archived=True)
+ """
+ return client.request("PUT", f"/api/dashboard/{dashboard_id}", json=fields)
+
+
+def metabase_delete_dashboard(client: MetabaseClient, dashboard_id: int) -> None:
+ """Elimina permanentemente un dashboard.
+
+ Endpoint: DELETE /api/dashboard/:id. IRREVERSIBLE.
+ Para soft-delete preferir: metabase_update_dashboard(client, id, archived=True)
+
+ Args:
+ client: Cliente autenticado.
+ dashboard_id: ID del dashboard a eliminar.
+
+ Example:
+ >>> metabase_delete_dashboard(client, 1)
+ >>> # Preferir: metabase_update_dashboard(client, 1, archived=True)
+ """
+ client.request("DELETE", f"/api/dashboard/{dashboard_id}")
diff --git a/python/functions/metabase/metabase_auth.md b/python/functions/metabase/metabase_auth.md
new file mode 100644
index 00000000..2b3ef8f9
--- /dev/null
+++ b/python/functions/metabase/metabase_auth.md
@@ -0,0 +1,46 @@
+---
+name: metabase_auth
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_auth(base_url: str, email: str, password: str) -> MetabaseClient"
+description: "Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias. Endpoint: POST /api/session."
+tags: [metabase, auth, session, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/client.py"
+---
+
+## Ejemplo
+
+```python
+from functions.metabase import metabase_auth
+
+client = metabase_auth("http://localhost:3000", "admin@example.com", "pass")
+# client listo para usar con todas las funciones CRUD
+
+# Alternativa con API key:
+from functions.metabase import MetabaseClient
+client = MetabaseClient("http://localhost:3000", "mb_api_key_xxxxx")
+```
+
+## Notas
+
+Dos formas de obtener un client:
+- `metabase_auth()`: login con email/password, obtiene session token via POST /api/session
+- `MetabaseClient(base_url, api_key)`: constructor directo con API key (recomendado para automatizacion)
+
+El client es un context manager: `with metabase_auth(...) as client:`
+
+Errores comunes:
+- 401: credenciales invalidas
+- Rate limiting en intentos fallidos de login
diff --git a/python/functions/metabase/metabase_create_card.md b/python/functions/metabase/metabase_create_card.md
new file mode 100644
index 00000000..c99fc347
--- /dev/null
+++ b/python/functions/metabase/metabase_create_card.md
@@ -0,0 +1,35 @@
+---
+name: metabase_create_card
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_create_card(client: MetabaseClient, name: str, dataset_query: dict, display: str = 'table', collection_id: int = 0, description: str = '') -> dict"
+description: "Crea una card/pregunta en Metabase con query SQL nativa o MBQL. Endpoint: POST /api/card."
+tags: [metabase, card, question, create, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/cards.py"
+---
+
+## Ejemplo
+
+```python
+card = metabase_create_card(client, "Revenue", {
+ "database": 1, "type": "native",
+ "native": {"query": "SELECT SUM(total) FROM orders"},
+}, display="scalar")
+```
+
+## Notas
+
+dataset_query SQL nativo: `{"database": id, "type": "native", "native": {"query": "..."}}`
+dataset_query MBQL: `{"database": id, "type": "query", "query": {"source-table": id, ...}}`
diff --git a/python/functions/metabase/metabase_create_dashboard.md b/python/functions/metabase/metabase_create_dashboard.md
new file mode 100644
index 00000000..d715227e
--- /dev/null
+++ b/python/functions/metabase/metabase_create_dashboard.md
@@ -0,0 +1,32 @@
+---
+name: metabase_create_dashboard
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_create_dashboard(client: MetabaseClient, name: str, description: str = '', collection_id: int = 0) -> dict"
+description: "Crea dashboard vacio en Metabase. Para agregar cards usar metabase_update_dashboard con dashcards. Endpoint: POST /api/dashboard."
+tags: [metabase, dashboard, create, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/dashboards.py"
+---
+
+## Ejemplo
+
+```python
+dash = metabase_create_dashboard(client, "Sales Overview", "KPIs")
+# Agregar cards con metabase_update_dashboard
+```
+
+## Notas
+
+Se crea vacio. Agregar cards con metabase_update_dashboard(dashcards=[...]).
diff --git a/python/functions/metabase/metabase_create_user.md b/python/functions/metabase/metabase_create_user.md
new file mode 100644
index 00000000..688d0890
--- /dev/null
+++ b/python/functions/metabase/metabase_create_user.md
@@ -0,0 +1,32 @@
+---
+name: metabase_create_user
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_create_user(client: MetabaseClient, first_name: str, last_name: str, email: str, password: str = '', group_ids: list[int] | None = None) -> dict"
+description: "Crea un nuevo usuario en Metabase. Sin password envia invitacion por email. Requiere superusuario. Endpoint: POST /api/user."
+tags: [metabase, user, create, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/users.py"
+---
+
+## Ejemplo
+
+```python
+user = metabase_create_user(client, "John", "Doe", "john@example.com", "pass123")
+user = metabase_create_user(client, "Jane", "Smith", "jane@example.com", group_ids=[1, 3])
+```
+
+## Notas
+
+Email debe ser unico. Error 400 si ya existe.
diff --git a/python/functions/metabase/metabase_deactivate_user.md b/python/functions/metabase/metabase_deactivate_user.md
new file mode 100644
index 00000000..21ccd678
--- /dev/null
+++ b/python/functions/metabase/metabase_deactivate_user.md
@@ -0,0 +1,31 @@
+---
+name: metabase_deactivate_user
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_deactivate_user(client: MetabaseClient, user_id: int) -> None"
+description: "Desactiva (soft-delete) un usuario en Metabase. Reactivar con PUT /api/user/:id/reactivate. Endpoint: DELETE /api/user/:id."
+tags: [metabase, user, delete, deactivate, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/users.py"
+---
+
+## Ejemplo
+
+```python
+metabase_deactivate_user(client, 5)
+```
+
+## Notas
+
+Soft-delete. El usuario se puede reactivar.
diff --git a/python/functions/metabase/metabase_delete_card.md b/python/functions/metabase/metabase_delete_card.md
new file mode 100644
index 00000000..eea33cf3
--- /dev/null
+++ b/python/functions/metabase/metabase_delete_card.md
@@ -0,0 +1,32 @@
+---
+name: metabase_delete_card
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_delete_card(client: MetabaseClient, card_id: int) -> None"
+description: "Elimina permanentemente una card/pregunta. IRREVERSIBLE. Preferir archived=True. Endpoint: DELETE /api/card/:id."
+tags: [metabase, card, question, delete, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/cards.py"
+---
+
+## Ejemplo
+
+```python
+metabase_delete_card(client, 42)
+# Preferir: metabase_update_card(client, 42, archived=True)
+```
+
+## Notas
+
+IRREVERSIBLE. Preferir soft-delete con metabase_update_card(archived=True).
diff --git a/python/functions/metabase/metabase_delete_dashboard.md b/python/functions/metabase/metabase_delete_dashboard.md
new file mode 100644
index 00000000..c4bedb14
--- /dev/null
+++ b/python/functions/metabase/metabase_delete_dashboard.md
@@ -0,0 +1,32 @@
+---
+name: metabase_delete_dashboard
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_delete_dashboard(client: MetabaseClient, dashboard_id: int) -> None"
+description: "Elimina permanentemente un dashboard. IRREVERSIBLE. Preferir archived=True. Endpoint: DELETE /api/dashboard/:id."
+tags: [metabase, dashboard, delete, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/dashboards.py"
+---
+
+## Ejemplo
+
+```python
+metabase_delete_dashboard(client, 1)
+# Preferir: metabase_update_dashboard(client, 1, archived=True)
+```
+
+## Notas
+
+IRREVERSIBLE. Preferir soft-delete con metabase_update_dashboard(archived=True).
diff --git a/python/functions/metabase/metabase_execute_card.md b/python/functions/metabase/metabase_execute_card.md
new file mode 100644
index 00000000..0c2dbad7
--- /dev/null
+++ b/python/functions/metabase/metabase_execute_card.md
@@ -0,0 +1,34 @@
+---
+name: metabase_execute_card
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_execute_card(client: MetabaseClient, card_id: int, parameters: list[dict] | None = None) -> dict"
+description: "Ejecuta la query de una card guardada y retorna resultados con columnas y filas. Soporta parametros. Endpoint: POST /api/card/:id/query."
+tags: [metabase, card, question, execute, query, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/cards.py"
+---
+
+## Ejemplo
+
+```python
+result = metabase_execute_card(client, 42)
+for row in result["data"]["rows"]:
+ print(row)
+```
+
+## Notas
+
+Respuesta: status, row_count, running_time, data.columns, data.rows, data.cols.
+Limite default: 2000 filas. Para ad-hoc sin card usar metabase_execute_query.
diff --git a/python/functions/metabase/metabase_execute_query.md b/python/functions/metabase/metabase_execute_query.md
new file mode 100644
index 00000000..5480c36f
--- /dev/null
+++ b/python/functions/metabase/metabase_execute_query.md
@@ -0,0 +1,32 @@
+---
+name: metabase_execute_query
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_execute_query(client: MetabaseClient, database_id: int, sql: str, max_results: int = 0) -> dict"
+description: "Ejecuta query SQL ad-hoc contra Metabase sin guardarla como card. Util para exploracion rapida. Endpoint: POST /api/dataset."
+tags: [metabase, query, execute, sql, dataset, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/cards.py"
+---
+
+## Ejemplo
+
+```python
+result = metabase_execute_query(client, 1, "SELECT * FROM users LIMIT 10")
+print(f"{result['row_count']} filas en {result['running_time']}ms")
+```
+
+## Notas
+
+Misma respuesta que metabase_execute_card. Default 2000 filas, override con max_results.
diff --git a/python/functions/metabase/metabase_get_card.md b/python/functions/metabase/metabase_get_card.md
new file mode 100644
index 00000000..f568546d
--- /dev/null
+++ b/python/functions/metabase/metabase_get_card.md
@@ -0,0 +1,32 @@
+---
+name: metabase_get_card
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_get_card(client: MetabaseClient, card_id: int) -> dict"
+description: "Obtiene detalles completos de una card/pregunta de Metabase incluyendo query, visualizacion y metadata. Endpoint: GET /api/card/:id."
+tags: [metabase, card, question, get, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/cards.py"
+---
+
+## Ejemplo
+
+```python
+card = metabase_get_card(client, 42)
+print(card["name"], card["display"])
+```
+
+## Notas
+
+Error 404 si no existe.
diff --git a/python/functions/metabase/metabase_get_dashboard.md b/python/functions/metabase/metabase_get_dashboard.md
new file mode 100644
index 00000000..073d21df
--- /dev/null
+++ b/python/functions/metabase/metabase_get_dashboard.md
@@ -0,0 +1,33 @@
+---
+name: metabase_get_dashboard
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_get_dashboard(client: MetabaseClient, dashboard_id: int) -> dict"
+description: "Obtiene dashboard completo con dashcards (cards posicionadas), tabs y parametros. Endpoint: GET /api/dashboard/:id."
+tags: [metabase, dashboard, get, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/dashboards.py"
+---
+
+## Ejemplo
+
+```python
+dash = metabase_get_dashboard(client, 1)
+for dc in dash["dashcards"]:
+ print(f"Card {dc['card_id']} at ({dc['col']}, {dc['row']})")
+```
+
+## Notas
+
+Cada dashcard tiene: id, card_id, card, size_x, size_y, col, row, dashboard_tab_id, parameter_mappings.
diff --git a/python/functions/metabase/metabase_get_user.md b/python/functions/metabase/metabase_get_user.md
new file mode 100644
index 00000000..8dcda611
--- /dev/null
+++ b/python/functions/metabase/metabase_get_user.md
@@ -0,0 +1,32 @@
+---
+name: metabase_get_user
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_get_user(client: MetabaseClient, user_id: int) -> dict"
+description: "Obtiene los detalles de un usuario de Metabase por su ID. Endpoint: GET /api/user/:id."
+tags: [metabase, user, get, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/users.py"
+---
+
+## Ejemplo
+
+```python
+user = metabase_get_user(client, 1)
+print(user["email"], user["is_superuser"])
+```
+
+## Notas
+
+Error 404 si el usuario no existe.
diff --git a/python/functions/metabase/metabase_list_cards.md b/python/functions/metabase/metabase_list_cards.md
new file mode 100644
index 00000000..cb903582
--- /dev/null
+++ b/python/functions/metabase/metabase_list_cards.md
@@ -0,0 +1,32 @@
+---
+name: metabase_list_cards
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_list_cards(client: MetabaseClient, filter: str = '', model_id: int = 0) -> list[dict]"
+description: "Lista preguntas/cards de Metabase. Filtros: all, mine, fav, archived, recent, popular, database, table. Endpoint: GET /api/card."
+tags: [metabase, card, question, list, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/cards.py"
+---
+
+## Ejemplo
+
+```python
+cards = metabase_list_cards(client, filter="mine")
+cards = metabase_list_cards(client, filter="database", model_id=1)
+```
+
+## Notas
+
+No tiene paginacion offset/limit. Retorna todas las cards que coinciden.
diff --git a/python/functions/metabase/metabase_list_dashboards.md b/python/functions/metabase/metabase_list_dashboards.md
new file mode 100644
index 00000000..ae87396e
--- /dev/null
+++ b/python/functions/metabase/metabase_list_dashboards.md
@@ -0,0 +1,33 @@
+---
+name: metabase_list_dashboards
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_list_dashboards(client: MetabaseClient, filter: str = '') -> list[dict]"
+description: "Lista dashboards de Metabase. Filtros: all, mine, archived. Retorna resumen sin dashcards. Endpoint: GET /api/dashboard."
+tags: [metabase, dashboard, list, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/dashboards.py"
+---
+
+## Ejemplo
+
+```python
+dashboards = metabase_list_dashboards(client, filter="mine")
+for d in dashboards:
+ print(d["id"], d["name"])
+```
+
+## Notas
+
+Para ver cards de un dashboard usar metabase_get_dashboard.
diff --git a/python/functions/metabase/metabase_list_users.md b/python/functions/metabase/metabase_list_users.md
new file mode 100644
index 00000000..8a8361f5
--- /dev/null
+++ b/python/functions/metabase/metabase_list_users.md
@@ -0,0 +1,33 @@
+---
+name: metabase_list_users
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_list_users(client: MetabaseClient, status: str = '', query: str = '', limit: int = 0, offset: int = 0) -> dict"
+description: "Lista usuarios de Metabase con filtros opcionales por estado, nombre/email y paginacion. Endpoint: GET /api/user."
+tags: [metabase, user, list, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/users.py"
+---
+
+## Ejemplo
+
+```python
+users = metabase_list_users(client, status="active", query="john@")
+for u in users["data"]:
+ print(u["email"], u["first_name"])
+```
+
+## Notas
+
+Retorna dict paginado con data, total, limit, offset.
diff --git a/python/functions/metabase/metabase_update_card.md b/python/functions/metabase/metabase_update_card.md
new file mode 100644
index 00000000..8c046148
--- /dev/null
+++ b/python/functions/metabase/metabase_update_card.md
@@ -0,0 +1,31 @@
+---
+name: metabase_update_card
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_update_card(client: MetabaseClient, card_id: int, **fields) -> dict"
+description: "Actualiza campos de una card/pregunta via kwargs. Campos: name, description, display, dataset_query, collection_id, archived. Endpoint: PUT /api/card/:id."
+tags: [metabase, card, question, update, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/cards.py"
+---
+
+## Ejemplo
+
+```python
+metabase_update_card(client, 42, name="New Name", archived=True)
+```
+
+## Notas
+
+Soft-delete con `archived=True`. Para delete permanente usar metabase_delete_card.
diff --git a/python/functions/metabase/metabase_update_dashboard.md b/python/functions/metabase/metabase_update_dashboard.md
new file mode 100644
index 00000000..17e1379b
--- /dev/null
+++ b/python/functions/metabase/metabase_update_dashboard.md
@@ -0,0 +1,39 @@
+---
+name: metabase_update_dashboard
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_update_dashboard(client: MetabaseClient, dashboard_id: int, **fields) -> dict"
+description: "Actualiza dashboard incluyendo metadata, cards y tabs via kwargs. dashcards es el estado completo deseado: nuevas con ID negativo, existentes con positivo, omitidas se eliminan. Endpoint: PUT /api/dashboard/:id."
+tags: [metabase, dashboard, update, cards, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/dashboards.py"
+---
+
+## Ejemplo
+
+```python
+# Agregar card
+dash = metabase_get_dashboard(client, 1)
+cards = list(dash["dashcards"])
+cards.append({"id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0})
+metabase_update_dashboard(client, 1, dashcards=cards)
+
+# Archivar
+metabase_update_dashboard(client, 1, archived=True)
+```
+
+## Notas
+
+dashcards = estado completo. ID negativo = nueva, positivo = existente, omitida = eliminada.
+Campos: name, description, archived, dashcards, tabs, parameters, collection_id.
diff --git a/python/functions/metabase/metabase_update_user.md b/python/functions/metabase/metabase_update_user.md
new file mode 100644
index 00000000..78e0f4c1
--- /dev/null
+++ b/python/functions/metabase/metabase_update_user.md
@@ -0,0 +1,32 @@
+---
+name: metabase_update_user
+kind: function
+lang: py
+domain: infra
+version: "1.0.0"
+purity: impure
+signature: "def metabase_update_user(client: MetabaseClient, user_id: int, **fields) -> dict"
+description: "Actualiza campos de un usuario en Metabase via keyword arguments. Campos: first_name, last_name, email, is_superuser, group_ids, locale. Endpoint: PUT /api/user/:id."
+tags: [metabase, user, update, api, python]
+uses_functions: []
+uses_types: []
+returns: []
+returns_optional: false
+error_type: "error_go_core"
+imports: [httpx]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "python/functions/metabase/users.py"
+---
+
+## Ejemplo
+
+```python
+metabase_update_user(client, 5, first_name="Jane", is_superuser=True)
+metabase_update_user(client, 5, group_ids=[1, 3, 5])
+```
+
+## Notas
+
+Solo se modifican los campos pasados como kwargs.
diff --git a/python/functions/metabase/users.py b/python/functions/metabase/users.py
new file mode 100644
index 00000000..4133b63b
--- /dev/null
+++ b/python/functions/metabase/users.py
@@ -0,0 +1,153 @@
+"""CRUD de usuarios de Metabase."""
+
+from .client import MetabaseClient
+
+
+def metabase_list_users(
+ client: MetabaseClient,
+ status: str = "",
+ query: str = "",
+ limit: int = 0,
+ offset: int = 0,
+) -> dict:
+ """Lista usuarios de Metabase con filtros opcionales.
+
+ Endpoint: GET /api/user. Requiere permisos de superusuario.
+
+ Args:
+ client: Cliente autenticado.
+ status: "active" (default), "deactivated" o "all".
+ query: Filtro por nombre o email.
+ limit: Tamanio de pagina (0 = default Metabase).
+ offset: Offset para paginacion.
+
+ Returns:
+ Dict con estructura paginada:
+ - data: lista de usuarios (id, email, first_name, last_name, is_superuser, etc.)
+ - total: numero total de usuarios que coinciden
+ - limit: tamanio de pagina usado
+ - offset: offset usado
+
+ Example:
+ >>> users = metabase_list_users(client, status="active", query="john@")
+ >>> for u in users["data"]:
+ ... print(u["email"], u["first_name"])
+ """
+ params = {}
+ if status:
+ params["status"] = status
+ if query:
+ params["query"] = query
+ if limit > 0:
+ params["limit"] = limit
+ if offset > 0:
+ params["offset"] = offset
+ return client.request("GET", "/api/user", params=params)
+
+
+def metabase_get_user(client: MetabaseClient, user_id: int) -> dict:
+ """Obtiene un usuario de Metabase por su ID.
+
+ Endpoint: GET /api/user/:id.
+
+ Args:
+ client: Cliente autenticado.
+ user_id: ID numerico del usuario.
+
+ Returns:
+ Dict con datos del usuario: id, email, first_name, last_name,
+ is_superuser, is_active, common_name, date_joined, last_login,
+ group_ids, locale.
+
+ Raises:
+ httpx.HTTPStatusError: 404 si el usuario no existe.
+
+ Example:
+ >>> user = metabase_get_user(client, 1)
+ >>> print(user["email"], user["is_superuser"])
+ """
+ return client.request("GET", f"/api/user/{user_id}")
+
+
+def metabase_create_user(
+ client: MetabaseClient,
+ first_name: str,
+ last_name: str,
+ email: str,
+ password: str = "",
+ group_ids: list[int] | None = None,
+) -> dict:
+ """Crea un nuevo usuario en Metabase.
+
+ Endpoint: POST /api/user. Requiere permisos de superusuario.
+
+ Args:
+ client: Cliente autenticado con permisos admin.
+ first_name: Nombre del usuario.
+ last_name: Apellido del usuario.
+ email: Email unico del usuario.
+ password: Password. Vacio = Metabase envia invitacion por email.
+ group_ids: IDs de grupos a asignar. None = solo grupo default.
+
+ Returns:
+ Dict con el usuario creado (mismos campos que metabase_get_user).
+
+ Raises:
+ httpx.HTTPStatusError: 400 si el email ya existe.
+
+ Example:
+ >>> user = metabase_create_user(client, "John", "Doe", "john@example.com", "pass123")
+ >>> print(user["id"])
+ """
+ body: dict = {
+ "first_name": first_name,
+ "last_name": last_name,
+ "email": email,
+ }
+ if password:
+ body["password"] = password
+ if group_ids:
+ body["group_ids"] = group_ids
+ return client.request("POST", "/api/user", json=body)
+
+
+def metabase_update_user(client: MetabaseClient, user_id: int, **fields) -> dict:
+ """Actualiza campos de un usuario en Metabase.
+
+ Endpoint: PUT /api/user/:id. Requiere permisos de superusuario.
+ Solo se modifican los campos pasados como keyword arguments.
+
+ Args:
+ client: Cliente autenticado con permisos admin.
+ user_id: ID del usuario a actualizar.
+ **fields: Campos a actualizar. Validos:
+ first_name (str), last_name (str), email (str),
+ is_superuser (bool), group_ids (list[int]),
+ locale (str), login_attributes (dict).
+
+ Returns:
+ Dict con el usuario actualizado.
+
+ Example:
+ >>> user = metabase_update_user(client, 5, first_name="Jane", is_superuser=True)
+ >>> user = metabase_update_user(client, 5, group_ids=[1, 3, 5])
+ """
+ return client.request("PUT", f"/api/user/{user_id}", json=fields)
+
+
+def metabase_deactivate_user(client: MetabaseClient, user_id: int) -> None:
+ """Desactiva (soft-delete) un usuario en Metabase.
+
+ Endpoint: DELETE /api/user/:id. Requiere permisos de superusuario.
+ El usuario no se elimina permanentemente, solo se marca como inactivo.
+ Para reactivar: PUT /api/user/:id/reactivate.
+
+ Args:
+ client: Cliente autenticado con permisos admin.
+ user_id: ID del usuario a desactivar.
+
+ Example:
+ >>> metabase_deactivate_user(client, 5)
+ >>> # Para ver desactivados: metabase_list_users(client, status="deactivated")
+ """
+ client.request("DELETE", f"/api/user/{user_id}")
diff --git a/python/pyproject.toml b/python/pyproject.toml
new file mode 100644
index 00000000..03b6a2b3
--- /dev/null
+++ b/python/pyproject.toml
@@ -0,0 +1,9 @@
+[project]
+name = "fn-registry-python"
+version = "0.1.0"
+description = "Funciones Python del fn-registry: Metabase API, ML, utilidades"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "httpx",
+]
diff --git a/python/uv.lock b/python/uv.lock
new file mode 100644
index 00000000..4ab33db3
--- /dev/null
+++ b/python/uv.lock
@@ -0,0 +1,91 @@
+version = 1
+revision = 3
+requires-python = ">=3.12"
+
+[[package]]
+name = "anyio"
+version = "4.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.2.25"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
+]
+
+[[package]]
+name = "fn-registry-python"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "httpx" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "httpx" }]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
diff --git a/registry/indexer.go b/registry/indexer.go
index 6aee63bf..9a85f2ea 100644
--- a/registry/indexer.go
+++ b/registry/indexer.go
@@ -19,6 +19,9 @@ type IndexResult struct {
// and populates the database. It uses two passes:
// 1. Parse all entries and collect known IDs
// 2. Validate references against known IDs, then insert valid entries
+//
+// Scans functions/ and types/ at the root level, plus any language-specific
+// directories (e.g. python/functions/, python/types/).
func Index(db *DB, root string) (*IndexResult, error) {
if err := db.Purge(); err != nil {
return nil, fmt.Errorf("purging database: %w", err)
@@ -26,39 +29,50 @@ func Index(db *DB, root string) (*IndexResult, error) {
result := &IndexResult{}
- // Pass 1: parse everything
+ // Pass 1: parse everything from all source directories
var functions []*Function
var types []*Type
- functionsDir := filepath.Join(root, "functions")
- if _, err := os.Stat(functionsDir); err == nil {
- filepath.Walk(functionsDir, func(path string, info os.FileInfo, err error) error {
- if err != nil || info.IsDir() || !strings.HasSuffix(path, ".md") {
- return nil
- }
- f, err := ParseFunctionMD(path)
+ // Directories to scan for functions and types.
+ // Base dirs + language-specific dirs discovered automatically.
+ funcDirs := []string{filepath.Join(root, "functions")}
+ typeDirs := []string{filepath.Join(root, "types")}
+
+ // Discover language-specific directories (e.g. python/functions/, python/types/)
+ entries, _ := os.ReadDir(root)
+ for _, e := range entries {
+ if !e.IsDir() {
+ continue
+ }
+ langFuncs := filepath.Join(root, e.Name(), "functions")
+ if fi, err := os.Stat(langFuncs); err == nil && fi.IsDir() {
+ funcDirs = append(funcDirs, langFuncs)
+ }
+ langTypes := filepath.Join(root, e.Name(), "types")
+ if fi, err := os.Stat(langTypes); err == nil && fi.IsDir() {
+ typeDirs = append(typeDirs, langTypes)
+ }
+ }
+
+ for _, dir := range funcDirs {
+ walkMD(dir, func(path string) {
+ f, err := ParseFunctionMD(path, root)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", path, err))
- return nil
+ return
}
functions = append(functions, f)
- return nil
})
}
- typesDir := filepath.Join(root, "types")
- if _, err := os.Stat(typesDir); err == nil {
- filepath.Walk(typesDir, func(path string, info os.FileInfo, err error) error {
- if err != nil || info.IsDir() || !strings.HasSuffix(path, ".md") {
- return nil
- }
- t, err := ParseTypeMD(path)
+ for _, dir := range typeDirs {
+ walkMD(dir, func(path string) {
+ t, err := ParseTypeMD(path, root)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", path, err))
- return nil
+ return
}
types = append(types, t)
- return nil
})
}
@@ -99,3 +113,17 @@ func Index(db *DB, root string) (*IndexResult, error) {
return result, nil
}
+
+// walkMD walks a directory recursively and calls fn for each .md file found.
+func walkMD(dir string, fn func(path string)) {
+ if _, err := os.Stat(dir); err != nil {
+ return
+ }
+ filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ if err != nil || info.IsDir() || !strings.HasSuffix(path, ".md") {
+ return nil
+ }
+ fn(path)
+ return nil
+ })
+}
diff --git a/registry/migrations/003_documentation.sql b/registry/migrations/003_documentation.sql
new file mode 100644
index 00000000..d7f4be6b
--- /dev/null
+++ b/registry/migrations/003_documentation.sql
@@ -0,0 +1,103 @@
+-- Add documentation fields to functions and types.
+-- examples: extracted code blocks from ## Ejemplo
+-- notes: extracted text from ## Notas
+-- documentation: remaining body text from .md
+-- code: source code from the referenced .go/.py/.tsx file
+
+ALTER TABLE functions ADD COLUMN notes TEXT NOT NULL DEFAULT '';
+ALTER TABLE functions ADD COLUMN documentation TEXT NOT NULL DEFAULT '';
+ALTER TABLE functions ADD COLUMN code TEXT NOT NULL DEFAULT '';
+
+ALTER TABLE types ADD COLUMN examples TEXT NOT NULL DEFAULT '';
+ALTER TABLE types ADD COLUMN notes TEXT NOT NULL DEFAULT '';
+ALTER TABLE types ADD COLUMN documentation TEXT NOT NULL DEFAULT '';
+ALTER TABLE types ADD COLUMN code TEXT NOT NULL DEFAULT '';
+
+-- Rebuild FTS for functions: add examples, notes, documentation, code
+DROP TRIGGER IF EXISTS functions_ai;
+DROP TRIGGER IF EXISTS functions_ad;
+DROP TRIGGER IF EXISTS functions_au;
+
+INSERT INTO functions_fts(functions_fts) VALUES('rebuild');
+DROP TABLE IF EXISTS functions_fts;
+
+CREATE VIRTUAL TABLE functions_fts USING fts5(
+ id,
+ name,
+ description,
+ tags,
+ signature,
+ domain,
+ example,
+ notes,
+ documentation,
+ code,
+ content='functions',
+ content_rowid='rowid'
+);
+
+-- Populate FTS from existing data
+INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
+SELECT rowid, id, name, description, tags, signature, domain, example, notes, documentation, code
+FROM functions;
+
+CREATE TRIGGER functions_ai AFTER INSERT ON functions BEGIN
+ INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
+ VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.signature, new.domain, new.example, new.notes, new.documentation, new.code);
+END;
+
+CREATE TRIGGER functions_ad AFTER DELETE ON functions BEGIN
+ INSERT INTO functions_fts(functions_fts, rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
+ VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.signature, old.domain, old.example, old.notes, old.documentation, old.code);
+END;
+
+CREATE TRIGGER functions_au AFTER UPDATE ON functions BEGIN
+ INSERT INTO functions_fts(functions_fts, rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
+ VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.signature, old.domain, old.example, old.notes, old.documentation, old.code);
+ INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
+ VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.signature, new.domain, new.example, new.notes, new.documentation, new.code);
+END;
+
+-- Rebuild FTS for types: add examples, notes, documentation, code
+DROP TRIGGER IF EXISTS types_ai;
+DROP TRIGGER IF EXISTS types_ad;
+DROP TRIGGER IF EXISTS types_au;
+
+INSERT INTO types_fts(types_fts) VALUES('rebuild');
+DROP TABLE IF EXISTS types_fts;
+
+CREATE VIRTUAL TABLE types_fts USING fts5(
+ id,
+ name,
+ description,
+ tags,
+ domain,
+ examples,
+ notes,
+ documentation,
+ code,
+ content='types',
+ content_rowid='rowid'
+);
+
+-- Populate FTS from existing data
+INSERT INTO types_fts(rowid, id, name, description, tags, domain, examples, notes, documentation, code)
+SELECT rowid, id, name, description, tags, domain, examples, notes, documentation, code
+FROM types;
+
+CREATE TRIGGER types_ai AFTER INSERT ON types BEGIN
+ INSERT INTO types_fts(rowid, id, name, description, tags, domain, examples, notes, documentation, code)
+ VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain, new.examples, new.notes, new.documentation, new.code);
+END;
+
+CREATE TRIGGER types_ad AFTER DELETE ON types BEGIN
+ INSERT INTO types_fts(types_fts, rowid, id, name, description, tags, domain, examples, notes, documentation, code)
+ VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain, old.examples, old.notes, old.documentation, old.code);
+END;
+
+CREATE TRIGGER types_au AFTER UPDATE ON types BEGIN
+ INSERT INTO types_fts(types_fts, rowid, id, name, description, tags, domain, examples, notes, documentation, code)
+ VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain, old.examples, old.notes, old.documentation, old.code);
+ INSERT INTO types_fts(rowid, id, name, description, tags, domain, examples, notes, documentation, code)
+ VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain, new.examples, new.notes, new.documentation, new.code);
+END;
diff --git a/registry/models.go b/registry/models.go
index bf3b60a5..53dafa70 100644
--- a/registry/models.go
+++ b/registry/models.go
@@ -47,6 +47,9 @@ type Function struct {
ErrorType string `json:"error_type"`
Imports []string `json:"imports"`
Example string `json:"example"`
+ Notes string `json:"notes"`
+ Documentation string `json:"documentation"`
+ Code string `json:"code"`
Tested bool `json:"tested"`
Tests []string `json:"tests"`
TestFilePath string `json:"test_file_path"`
@@ -82,9 +85,13 @@ type Type struct {
Description string `json:"description"`
Tags []string `json:"tags"`
UsesTypes []string `json:"uses_types"`
- FilePath string `json:"file_path"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
+ Examples string `json:"examples"`
+ Notes string `json:"notes"`
+ Documentation string `json:"documentation"`
+ Code string `json:"code"`
+ FilePath string `json:"file_path"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
}
// ProposalKind classifies a proposal.
diff --git a/registry/parser.go b/registry/parser.go
index e4212667..a5875173 100644
--- a/registry/parser.go
+++ b/registry/parser.go
@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"os"
+ "path/filepath"
"strings"
"gopkg.in/yaml.v3"
@@ -73,7 +74,8 @@ func extractFrontmatter(data []byte) ([]byte, []byte, error) {
}
// ParseFunctionMD parses a function .md file into a Function.
-func ParseFunctionMD(path string) (*Function, error) {
+// root is the registry root directory, used to resolve file_path for code reading.
+func ParseFunctionMD(path string, root string) (*Function, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
@@ -99,7 +101,7 @@ func ParseFunctionMD(path string) (*Function, error) {
return nil, fmt.Errorf("%s: description is required", path)
}
- example := extractExample(body)
+ sections := extractSections(body)
f := &Function{
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
@@ -118,6 +120,9 @@ func ParseFunctionMD(path string) (*Function, error) {
ReturnsOptional: raw.ReturnsOptional,
ErrorType: raw.ErrorType,
Imports: raw.Imports,
+ Example: sections.example,
+ Notes: sections.notes,
+ Documentation: sections.documentation,
Tested: raw.Tested,
Tests: raw.Tests,
TestFilePath: raw.TestFilePath,
@@ -129,21 +134,25 @@ func ParseFunctionMD(path string) (*Function, error) {
Variant: raw.Variant,
}
- if example != "" && f.Example == "" {
- f.Example = example
+ if root != "" && raw.FilePath != "" {
+ codePath := filepath.Join(root, raw.FilePath)
+ if codeData, err := os.ReadFile(codePath); err == nil {
+ f.Code = string(codeData)
+ }
}
return f, nil
}
// ParseTypeMD parses a type .md file into a Type.
-func ParseTypeMD(path string) (*Type, error) {
+// root is the registry root directory, used to resolve file_path for code reading.
+func ParseTypeMD(path string, root string) (*Type, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}
- fm, _, err := extractFrontmatter(data)
+ fm, body, err := extractFrontmatter(data)
if err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
@@ -160,49 +169,96 @@ func ParseTypeMD(path string) (*Type, error) {
return nil, fmt.Errorf("%s: description is required", path)
}
+ sections := extractSections(body)
+
t := &Type{
- ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
- Name: raw.Name,
- Lang: raw.Lang,
- Domain: raw.Domain,
- Version: raw.Version,
- Algebraic: Algebraic(raw.Algebraic),
- Definition: strings.TrimSpace(raw.Definition),
- Description: raw.Description,
- Tags: raw.Tags,
- UsesTypes: raw.UsesTypes,
- FilePath: raw.FilePath,
+ ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
+ Name: raw.Name,
+ Lang: raw.Lang,
+ Domain: raw.Domain,
+ Version: raw.Version,
+ Algebraic: Algebraic(raw.Algebraic),
+ Definition: strings.TrimSpace(raw.Definition),
+ Description: raw.Description,
+ Tags: raw.Tags,
+ UsesTypes: raw.UsesTypes,
+ Examples: sections.example,
+ Notes: sections.notes,
+ Documentation: sections.documentation,
+ FilePath: raw.FilePath,
+ }
+
+ if root != "" && raw.FilePath != "" {
+ codePath := filepath.Join(root, raw.FilePath)
+ if codeData, err := os.ReadFile(codePath); err == nil {
+ t.Code = string(codeData)
+ }
}
return t, nil
}
-// extractExample pulls the first code block after an "## Ejemplo" heading.
-func extractExample(body []byte) string {
+// bodySections holds the extracted sections from a .md body.
+type bodySections struct {
+ example string // content under ## Ejemplo
+ notes string // content under ## Notas
+ documentation string // everything else
+}
+
+// extractSections splits the markdown body into named sections.
+// Known sections (## Ejemplo, ## Notas) are extracted separately.
+// All other content (including unknown ## headings) goes into documentation.
+func extractSections(body []byte) bodySections {
lines := strings.Split(string(body), "\n")
- inExample := false
- inCode := false
- var code []string
+ var s bodySections
+
+ type section struct {
+ name string
+ lines []string
+ }
+
+ var current *section
+ var sections []section
for _, line := range lines {
trimmed := strings.TrimSpace(line)
- if strings.HasPrefix(trimmed, "## Ejemplo") {
- inExample = true
- continue
- }
- if inExample && !inCode && strings.HasPrefix(trimmed, "```") {
- inCode = true
- continue
- }
- if inCode {
- if strings.HasPrefix(trimmed, "```") {
- return strings.Join(code, "\n")
+ if strings.HasPrefix(trimmed, "## ") {
+ if current != nil {
+ sections = append(sections, *current)
}
- code = append(code, line)
+ current = §ion{name: trimmed}
+ continue
}
- if inExample && !inCode && strings.HasPrefix(trimmed, "##") {
- break
+ if current != nil {
+ current.lines = append(current.lines, line)
+ } else {
+ // Content before any ## heading goes to documentation
+ sections = append(sections, section{name: "_preamble", lines: []string{line}})
}
}
- return ""
+ if current != nil {
+ sections = append(sections, *current)
+ }
+
+ var docParts []string
+ for _, sec := range sections {
+ content := strings.TrimSpace(strings.Join(sec.lines, "\n"))
+ if content == "" && sec.name == "_preamble" {
+ continue
+ }
+ switch {
+ case strings.HasPrefix(sec.name, "## Ejemplo"):
+ s.example = content
+ case strings.HasPrefix(sec.name, "## Notas"):
+ s.notes = content
+ case sec.name == "_preamble":
+ docParts = append(docParts, content)
+ default:
+ // Unknown sections go to documentation with their heading
+ docParts = append(docParts, sec.name+"\n\n"+content)
+ }
+ }
+ s.documentation = strings.TrimSpace(strings.Join(docParts, "\n\n"))
+
+ return s
}
diff --git a/registry/parser_test.go b/registry/parser_test.go
index f393c13f..f44c05bd 100644
--- a/registry/parser_test.go
+++ b/registry/parser_test.go
@@ -79,7 +79,7 @@ imports: [react]
tested: false
tests: []
test_file_path: ""
-file_path: "functions/components/DataTable.tsx"
+file_path: "frontend/functions/ui/data_table.tsx"
props:
- name: data
type: "T[]"
@@ -105,7 +105,7 @@ func writeTempFile(t *testing.T, dir, name, content string) string {
func TestParseFunctionMD(t *testing.T) {
path := writeTempFile(t, t.TempDir(), "filter_slice.md", functionMD)
- f, err := ParseFunctionMD(path)
+ f, err := ParseFunctionMD(path, "")
if err != nil {
t.Fatal(err)
}
@@ -130,7 +130,7 @@ func TestParseFunctionMD(t *testing.T) {
func TestParseTypeMD(t *testing.T) {
path := writeTempFile(t, t.TempDir(), "ohlcv.md", typeMD)
- typ, err := ParseTypeMD(path)
+ typ, err := ParseTypeMD(path, "")
if err != nil {
t.Fatal(err)
}
@@ -149,7 +149,7 @@ func TestParseTypeMD(t *testing.T) {
func TestParseComponentMD(t *testing.T) {
path := writeTempFile(t, t.TempDir(), "DataTable.md", componentMD)
- f, err := ParseFunctionMD(path)
+ f, err := ParseFunctionMD(path, "")
if err != nil {
t.Fatal(err)
}
@@ -174,7 +174,7 @@ func TestParseComponentMD(t *testing.T) {
func TestParseMissingFrontmatter(t *testing.T) {
path := writeTempFile(t, t.TempDir(), "bad.md", "# No frontmatter here\n")
- _, err := ParseFunctionMD(path)
+ _, err := ParseFunctionMD(path, "")
if err == nil {
t.Error("expected error for missing frontmatter")
}
diff --git a/registry/store.go b/registry/store.go
index 91b75801..dd063fc2 100644
--- a/registry/store.go
+++ b/registry/store.go
@@ -82,19 +82,22 @@ func (db *DB) InsertFunction(f *Function) error {
description, tags, uses_functions, uses_types, returns,
returns_optional, error_type, imports, example, tested,
tests, test_file_path, file_path, created_at, updated_at,
- props, emits, has_state, framework, variant
+ props, emits, has_state, framework, variant,
+ notes, documentation, code
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?, ?,
- ?, ?, ?, ?, ?
+ ?, ?, ?, ?, ?,
+ ?, ?, ?
)`,
f.ID, f.Name, string(f.Kind), f.Lang, f.Domain, f.Version, string(f.Purity), f.Signature,
f.Description, marshalStrings(f.Tags), marshalStrings(f.UsesFunctions), marshalStrings(f.UsesTypes), marshalStrings(f.Returns),
f.ReturnsOptional, f.ErrorType, marshalStrings(f.Imports), f.Example, f.Tested,
marshalStrings(f.Tests), f.TestFilePath, f.FilePath, f.CreatedAt.Format(time.RFC3339), now,
marshalProps(f.Props), marshalStrings(f.Emits), hasState, f.Framework, marshalStrings(f.Variant),
+ f.Notes, f.Documentation, f.Code,
)
return err
}
@@ -115,11 +118,13 @@ func (db *DB) InsertType(t *Type) error {
INSERT OR REPLACE INTO types (
id, name, lang, domain, version, algebraic,
definition, description, tags, uses_types,
- file_path, created_at, updated_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ file_path, created_at, updated_at,
+ examples, notes, documentation, code
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
t.ID, t.Name, t.Lang, t.Domain, t.Version, string(t.Algebraic),
t.Definition, t.Description, marshalStrings(t.Tags), marshalStrings(t.UsesTypes),
t.FilePath, t.CreatedAt.Format(time.RFC3339), now,
+ t.Examples, t.Notes, t.Documentation, t.Code,
)
return err
}
@@ -270,6 +275,7 @@ func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Functio
&f.ReturnsOptional, &f.ErrorType, &importsJSON, &f.Example, &f.Tested,
&testsJSON, &f.TestFilePath, &f.FilePath, &createdAt, &updatedAt,
&propsJSON, &emitsJSON, &hasState, &f.Framework, &variantJSON,
+ &f.Notes, &f.Documentation, &f.Code,
)
if err != nil {
return nil, fmt.Errorf("scanning function: %w", err)
@@ -308,6 +314,7 @@ func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error
&t.ID, &t.Name, &t.Lang, &t.Domain, &t.Version, &t.Algebraic,
&t.Definition, &t.Description, &tagsJSON, &usesTypJSON,
&t.FilePath, &createdAt, &updatedAt,
+ &t.Examples, &t.Notes, &t.Documentation, &t.Code,
)
if err != nil {
return nil, fmt.Errorf("scanning type: %w", err)
diff --git a/types/infra/metabase_client.go b/types/infra/metabase_client.go
new file mode 100644
index 00000000..0bcbb4d0
--- /dev/null
+++ b/types/infra/metabase_client.go
@@ -0,0 +1,7 @@
+package infra
+
+// MetabaseClient holds the connection details for a Metabase instance API.
+type MetabaseClient struct {
+ BaseURL string // e.g. "http://localhost:3000"
+ Token string // session token or API key
+}
diff --git a/types/infra/metabase_client.md b/types/infra/metabase_client.md
new file mode 100644
index 00000000..be7b1765
--- /dev/null
+++ b/types/infra/metabase_client.md
@@ -0,0 +1,24 @@
+---
+name: MetabaseClient
+lang: go
+domain: infra
+version: "1.0.0"
+algebraic: product
+definition: |
+ type MetabaseClient struct {
+ BaseURL string
+ Token string
+ }
+description: "Cliente para la API REST de Metabase. Contiene la URL base de la instancia y el token de autenticacion (session token o API key)."
+tags: [metabase, api, client, infra]
+uses_types: []
+file_path: "types/infra/metabase_client.go"
+---
+
+## Notas
+
+Tipo producto con dos campos obligatorios:
+- `BaseURL`: URL base de la instancia Metabase sin trailing slash (ej: `http://localhost:3000`)
+- `Token`: token de sesion obtenido con `MetabaseAuth()` o una API key creada en el admin UI de Metabase
+
+El token se envia como header `X-Metabase-Session` en session tokens o `x-api-key` en API keys. Las funciones del registry usan `X-Metabase-Session` por defecto.