diff --git a/README.md b/README.md index 6f318446..f05d3ae8 100644 --- a/README.md +++ b/README.md @@ -515,6 +515,30 @@ Supported model families: > since vectors are not cross-compatible between models. The prompt format is > automatically adjusted for each model family. +### OpenAI Embeddings (Optional) + +As an alternative to local embedding models, you can use OpenAI's API for faster, more reliable embeddings: + +```yaml +# ~/.config/qmd/index.yml +embedding: + provider: openai + openai: + api_key: sk-... # Optional, falls back to QMD_OPENAI_API_KEY or OPENAI_API_KEY env var + model: text-embedding-3-small # Optional, this is the default + expansion_model: gpt-4o-mini # Optional, model for query expansion/reranking + base_url: https://api.openai.com/v1 # Optional, for OpenAI-compatible APIs (Ollama, vLLM, etc.) +``` + +Benefits: +- **~10x faster** than local CPU inference +- **No GPU required** - works on any machine +- **More reliable** - no local model loading issues +- **Cost:** ~$0.02 per 1M tokens (very cheap) +- **OpenAI-compatible** - works with Ollama, vLLM, Azure, and other compatible APIs via `base_url` + +When using OpenAI embeddings, query expansion and reranking use the OpenAI API instead of local models. + ## Installation ```sh diff --git a/bun.lock b/bun.lock index a96f0964..7cfea15b 100644 --- a/bun.lock +++ b/bun.lock @@ -3,14 +3,16 @@ "configVersion": 1, "workspaces": { "": { - "name": "2025-12-07-bm25-q", + "name": "@tobilu/qmd", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", "better-sqlite3": "12.8.0", "fast-glob": "3.3.3", "node-llama-cpp": "3.18.1", + "openai": "^4.77.0", "picomatch": "4.0.4", "sqlite-vec": "0.1.9", + "tiktoken": "^1.0.22", "web-tree-sitter": "0.26.7", "yaml": "2.8.3", "zod": "4.2.1", @@ -37,59 +39,59 @@ }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], - "@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.6", "", {}, "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA=="], @@ -153,57 +155,57 @@ "@reflink/reflink-win32-x64-msvc": ["@reflink/reflink-win32-x64-msvc@0.1.19", "", { "os": "win32", "cpu": "x64" }, "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w=="], - "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.2", "", {}, "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g=="], + "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.4", "", {}, "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA=="], "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], @@ -213,7 +215,9 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -229,9 +233,13 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -245,6 +253,8 @@ "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.8.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ=="], @@ -253,7 +263,7 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -277,7 +287,7 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -291,6 +301,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], @@ -301,7 +313,7 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -313,6 +325,8 @@ "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -337,7 +351,9 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -347,7 +363,9 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], @@ -359,7 +377,7 @@ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="], + "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -381,6 +399,12 @@ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -401,7 +425,7 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -413,13 +437,17 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hono": ["hono@4.12.10", "", {}, "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w=="], + "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], "http-errors": ["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" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -451,7 +479,7 @@ "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], @@ -501,18 +529,22 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], - "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], "node-api-headers": ["node-api-headers@1.8.0", "", {}, "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], "node-llama-cpp": ["node-llama-cpp@3.18.1", "", { "dependencies": { "@huggingface/jinja": "^0.5.6", "async-retry": "^1.3.3", "bytes": "^3.1.2", "chalk": "^5.6.2", "chmodrp": "^1.0.2", "cmake-js": "^8.0.0", "cross-spawn": "^7.0.6", "env-var": "^7.5.0", "filenamify": "^6.0.0", "fs-extra": "^11.3.4", "ignore": "^7.0.4", "ipull": "^3.9.5", "is-unicode-supported": "^2.1.0", "lifecycle-utils": "^3.1.1", "log-symbols": "^7.0.1", "nanoid": "^5.1.6", "node-addon-api": "^8.6.0", "ora": "^9.3.0", "pretty-ms": "^9.3.0", "proper-lockfile": "^4.1.2", "semver": "^7.7.1", "simple-git": "^3.33.0", "slice-ansi": "^8.0.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.2.0", "validate-npm-package-name": "^7.0.2", "which": "^6.0.1", "yargs": "^17.7.2" }, "optionalDependencies": { "@node-llama-cpp/linux-arm64": "3.18.1", "@node-llama-cpp/linux-armv7l": "3.18.1", "@node-llama-cpp/linux-x64": "3.18.1", "@node-llama-cpp/linux-x64-cuda": "3.18.1", "@node-llama-cpp/linux-x64-cuda-ext": "3.18.1", "@node-llama-cpp/linux-x64-vulkan": "3.18.1", "@node-llama-cpp/mac-arm64-metal": "3.18.1", "@node-llama-cpp/mac-x64": "3.18.1", "@node-llama-cpp/win-arm64": "3.18.1", "@node-llama-cpp/win-x64": "3.18.1", "@node-llama-cpp/win-x64-cuda": "3.18.1", "@node-llama-cpp/win-x64-cuda-ext": "3.18.1", "@node-llama-cpp/win-x64-vulkan": "3.18.1" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"], "bin": { "node-llama-cpp": "dist/cli/cli.js", "nlc": "dist/cli/cli.js" } }, "sha512-w0zfuy/IKS2fhrbed5SylZDXJHTVz4HnkwZ4UrFPgSNwJab3QIPwIl4lyCKHHy9flLrtxsAuV5kXfH3HZ6bb8w=="], @@ -527,6 +559,8 @@ "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="], + "ora": ["ora@9.3.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.1", "string-width": "^8.1.0" } }, "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw=="], "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], @@ -535,7 +569,7 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + "path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -547,7 +581,7 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], @@ -559,9 +593,9 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -585,7 +619,7 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + "rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -595,11 +629,11 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], @@ -665,12 +699,14 @@ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], - "tar": ["tar@7.5.10", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw=="], + "tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tiktoken": ["tiktoken@1.0.22", "", {}, "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], @@ -687,6 +723,8 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tree-sitter-go": ["tree-sitter-go@0.23.4", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-iQaHEs4yMa/hMo/ZCGqLfG61F0miinULU1fFh+GZreCRtKylFLtvn798ocCZjO2r/ungNZgAY1s1hPFyAwkc7w=="], "tree-sitter-javascript": ["tree-sitter-javascript@0.23.1", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA=="], @@ -705,7 +743,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], @@ -725,8 +763,14 @@ "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + "web-tree-sitter": ["web-tree-sitter@0.26.7", "", {}, "sha512-KiZhelTvBA/ziUHEO7Emb75cGVAq8iGZNabYaZm53Zpy50NsXyOW+xSHlwHt5CVg/TRPZBfeVLTTobF0LjFJ1w=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -755,11 +799,9 @@ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cmake-js/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "ipull/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "ipull/lifecycle-utils": ["lifecycle-utils@2.1.0", "", {}, "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA=="], @@ -767,13 +809,9 @@ "ipull/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - "ipull/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "node-llama-cpp/node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], + "openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], @@ -785,18 +823,8 @@ "stdout-update/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "stdout-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -813,9 +841,11 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ipull/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="], - "stdout-update/string-width/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/package.json b/package.json index 0ec04c9c..3cf3e79b 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,10 @@ "better-sqlite3": "12.8.0", "fast-glob": "3.3.3", "node-llama-cpp": "3.18.1", + "openai": "^4.77.0", "picomatch": "4.0.4", "sqlite-vec": "0.1.9", + "tiktoken": "^1.0.22", "web-tree-sitter": "0.26.7", "yaml": "2.8.3", "zod": "4.2.1" diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index 50ae7648..a388ad01 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -78,7 +78,7 @@ import { type ReindexResult, type ChunkStrategy, } from "../store.js"; -import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js"; +import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, getDefaultEmbeddingLLM, getEmbeddingConfig, withLLMSession, pullModels, setEmbeddingConfig, isUsingOpenAI, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js"; import { formatSearchResults, formatDocuments, @@ -98,6 +98,7 @@ import { listAllContexts, setConfigIndexName, loadConfig, + getEmbeddingConfig as getEmbeddingConfigFromYaml, } from "../collections.js"; import { getEmbeddedQmdSkillContent, getEmbeddedQmdSkillFiles } from "../embedded-skills.js"; @@ -454,8 +455,14 @@ async function showStatus(): Promise { console.log(`\n${c.dim}No collections. Run 'qmd collection add .' to index markdown files.${c.reset}`); } - // Models - { + // Models / Provider info + if (isUsingOpenAI()) { + const embCfg = getEmbeddingConfig(); + console.log(`\n${c.bold}Provider${c.reset}`); + console.log(` Mode: ${c.green}OpenAI-compatible${c.reset}`); + console.log(` Base URL: ${embCfg.openai?.baseURL || process.env.QMD_OPENAI_BASE_URL || '(default)'}`); + console.log(` Embed model: ${embCfg.openai?.embedModel || 'text-embedding-3-small'}`); + } else { // hf:org/repo/file.gguf → https://huggingface.co/org/repo const hfLink = (uri: string) => { const match = uri.match(/^hf:([^/]+\/[^/]+)\//); @@ -465,38 +472,37 @@ async function showStatus(): Promise { console.log(` Embedding: ${hfLink(DEFAULT_EMBED_MODEL_URI)}`); console.log(` Reranking: ${hfLink(DEFAULT_RERANK_MODEL_URI)}`); console.log(` Generation: ${hfLink(DEFAULT_GENERATE_MODEL_URI)}`); - } - // Device / GPU info - console.log(`\n${c.bold}Device${c.reset}`); - try { - const llm = getDefaultLlamaCpp(); - const device = await llm.getDeviceInfo({ allowBuild: false }); - if (device.gpu) { - console.log(` GPU: ${c.green}${device.gpu}${c.reset} (offloading: ${device.gpuOffloading ? 'yes' : 'no'})`); - if (device.gpuDevices.length > 0) { - // Deduplicate and count GPUs - const counts = new Map(); - for (const name of device.gpuDevices) { - counts.set(name, (counts.get(name) || 0) + 1); + // Device / GPU info (local mode only — skip in OpenAI mode to avoid triggering compilation) + console.log(`\n${c.bold}Device${c.reset}`); + try { + const llm = getDefaultLlamaCpp(); + const device = await llm.getDeviceInfo({ allowBuild: false }); + if (device.gpu) { + console.log(` GPU: ${c.green}${device.gpu}${c.reset} (offloading: ${device.gpuOffloading ? 'yes' : 'no'})`); + if (device.gpuDevices.length > 0) { + const counts = new Map(); + for (const name of device.gpuDevices) { + counts.set(name, (counts.get(name) || 0) + 1); + } + const deviceStr = Array.from(counts.entries()) + .map(([name, count]) => count > 1 ? `${count}× ${name}` : name) + .join(', '); + console.log(` Devices: ${deviceStr}`); } - const deviceStr = Array.from(counts.entries()) - .map(([name, count]) => count > 1 ? `${count}× ${name}` : name) - .join(', '); - console.log(` Devices: ${deviceStr}`); + if (device.vram) { + console.log(` VRAM: ${formatBytes(device.vram.free)} free / ${formatBytes(device.vram.total)} total`); + } + } else { + console.log(` GPU: ${c.yellow}none${c.reset} (running on CPU — models will be slow)`); + console.log(` ${c.dim}Tip: Install CUDA, Vulkan, or Metal support for GPU acceleration.${c.reset}`); } - if (device.vram) { - console.log(` VRAM: ${formatBytes(device.vram.free)} free / ${formatBytes(device.vram.total)} total`); + console.log(` CPU: ${device.cpuCores} math cores`); + } catch (error) { + console.log(` Status: ${c.dim}skipped${c.reset} (status probe does not build llama.cpp backends)`); + if (error instanceof Error && error.message) { + console.log(` ${c.dim}${error.message}${c.reset}`); } - } else { - console.log(` GPU: ${c.yellow}none${c.reset} (running on CPU — models will be slow)`); - console.log(` ${c.dim}Tip: Install CUDA, Vulkan, or Metal support for GPU acceleration.${c.reset}`); - } - console.log(` CPU: ${device.cpuCores} math cores`); - } catch (error) { - console.log(` Status: ${c.dim}skipped${c.reset} (status probe does not build llama.cpp backends)`); - if (error instanceof Error && error.message) { - console.log(` ${c.dim}${error.message}${c.reset}`); } } @@ -1704,34 +1710,37 @@ async function vectorIndex( const startTime = Date.now(); - const result = await generateEmbeddings(storeInstance, { - force, - model, - maxDocsPerBatch: batchOptions?.maxDocsPerBatch, - maxBatchBytes: batchOptions?.maxBatchBytes, - chunkStrategy: batchOptions?.chunkStrategy, - onProgress: (info) => { - if (info.totalBytes === 0) return; - const percent = (info.bytesProcessed / info.totalBytes) * 100; - progress.set(percent); - - const elapsed = (Date.now() - startTime) / 1000; - const bytesPerSec = info.bytesProcessed / elapsed; - const remainingBytes = info.totalBytes - info.bytesProcessed; - const etaSec = remainingBytes / bytesPerSec; - - const bar = renderProgressBar(percent); - const percentStr = percent.toFixed(0).padStart(3); - const throughput = `${formatBytes(bytesPerSec)}/s`; - const eta = elapsed > 2 ? formatETA(etaSec) : "..."; - const errStr = info.errors > 0 ? ` ${c.yellow}${info.errors} err${c.reset}` : ""; - - if (isTTY) process.stderr.write(`\r${c.cyan}${bar}${c.reset} ${c.bold}${percentStr}%${c.reset} ${c.dim}${info.chunksEmbedded}/${info.totalChunks}${c.reset}${errStr} ${c.dim}${throughput} ETA ${eta}${c.reset} `); - }, - }); + let result: Awaited>; + try { + result = await generateEmbeddings(storeInstance, { + force, + model, + maxDocsPerBatch: batchOptions?.maxDocsPerBatch, + maxBatchBytes: batchOptions?.maxBatchBytes, + chunkStrategy: batchOptions?.chunkStrategy, + onProgress: (info) => { + if (info.totalBytes === 0) return; + const percent = (info.bytesProcessed / info.totalBytes) * 100; + progress.set(percent); - progress.clear(); - cursor.show(); + const elapsed = (Date.now() - startTime) / 1000; + const bytesPerSec = info.bytesProcessed / elapsed; + const remainingBytes = info.totalBytes - info.bytesProcessed; + const etaSec = remainingBytes / bytesPerSec; + + const bar = renderProgressBar(percent); + const percentStr = percent.toFixed(0).padStart(3); + const throughput = `${formatBytes(bytesPerSec)}/s`; + const eta = elapsed > 2 ? formatETA(etaSec) : "..."; + const errStr = info.errors > 0 ? ` ${c.yellow}${info.errors} err${c.reset}` : ""; + + if (isTTY) process.stderr.write(`\r${c.cyan}${bar}${c.reset} ${c.bold}${percentStr}%${c.reset} ${c.dim}${info.chunksEmbedded}/${info.totalChunks}${c.reset}${errStr} ${c.dim}${throughput} ETA ${eta}${c.reset} `); + }, + }); + } finally { + progress.clear(); + cursor.show(); + } const totalTimeSec = result.durationMs / 1000; @@ -2235,10 +2244,8 @@ function search(query: string, opts: OutputOptions): void { // Use large limit for --all, otherwise fetch more than needed and let outputResults filter const fetchLimit = opts.all ? 100000 : Math.max(50, opts.limit * 2); - const results = filterByCollections( - searchFTS(db, query, fetchLimit, singleCollection), - collectionNames - ); + // Pass collections directly to searchFTS (it now supports arrays) + const results = searchFTS(db, query, fetchLimit, collectionNames.length > 0 ? collectionNames : undefined); // Add context to results const resultsWithContext = results.map(r => ({ @@ -2286,7 +2293,7 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string = checkIndexHealth(store.db); - await withLLMSession(async () => { + const llmSession = async () => { let results = await vectorSearchQuery(store, query, { collection: singleCollection, limit: opts.all ? 500 : (opts.limit || 10), @@ -2324,7 +2331,15 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string = context: r.context, docid: r.docid, })), query, { ...opts, limit: results.length }); - }, { maxDuration: 10 * 60 * 1000, name: 'vectorSearch' }); + }; + + if (isUsingOpenAI()) { + await llmSession(); + } else { + await withLLMSession(async () => llmSession(), + { maxDuration: 10 * 60 * 1000, name: 'vectorSearch' } + ); + } } async function querySearch(query: string, opts: OutputOptions, _embedModel: string = DEFAULT_EMBED_MODEL, _rerankModel: string = DEFAULT_RERANK_MODEL): Promise { @@ -2342,7 +2357,7 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri // Intent can come from --intent flag or from intent: line in query document const intent = opts.intent || parsed?.intent; - await withLLMSession(async () => { + const querySession = async () => { let results; if (parsed) { @@ -2462,7 +2477,15 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri docid: r.docid, explain: r.explain, })), displayQuery, { ...opts, limit: results.length }); - }, { maxDuration: 10 * 60 * 1000, name: 'querySearch' }); + }; + + if (isUsingOpenAI()) { + await querySession(); + } else { + await withLLMSession(async () => querySession(), + { maxDuration: 10 * 60 * 1000, name: 'querySearch' } + ); + } } // Parse CLI arguments using util.parseArgs @@ -2847,6 +2870,31 @@ if (isMain) { process.exit(cli.values.help ? 0 : 1); } + // Load embedding configuration. + // Priority: YAML config > env vars > default (local). + // Setting QMD_OPENAI_BASE_URL alone is enough to activate OpenAI mode. + const embeddingYamlConfig = getEmbeddingConfigFromYaml(); + const useOpenAI = embeddingYamlConfig.provider === 'openai' + || !!process.env.QMD_OPENAI_BASE_URL + || process.env.QMD_OPENAI === '1'; + + if (useOpenAI) { + setEmbeddingConfig({ + provider: 'openai', + openai: { + apiKey: embeddingYamlConfig.openai?.api_key || process.env.QMD_OPENAI_API_KEY, + embedModel: embeddingYamlConfig.openai?.model || process.env.QMD_OPENAI_EMBED_MODEL, + expansionModel: embeddingYamlConfig.openai?.expansion_model, + rerankModel: embeddingYamlConfig.openai?.rerank_model, + baseURL: embeddingYamlConfig.openai?.base_url || process.env.QMD_OPENAI_BASE_URL, + chatBaseURL: embeddingYamlConfig.openai?.chat_base_url, + chatApiKey: embeddingYamlConfig.openai?.chat_api_key, + rerankBaseURL: embeddingYamlConfig.openai?.rerank_base_url, + rerankApiKey: embeddingYamlConfig.openai?.rerank_api_key, + }, + }); + } + switch (cli.command) { case "context": { const subcommand = cli.args[0]; diff --git a/src/collections.ts b/src/collections.ts index e68ff65b..ff948218 100644 --- a/src/collections.ts +++ b/src/collections.ts @@ -42,6 +42,24 @@ export interface ModelsConfig { generate?: string; } +/** + * Embedding provider configuration (optional in config file) + */ +export interface EmbeddingProviderConfig { + provider?: 'local' | 'openai'; // Default: 'local' + openai?: { + api_key?: string; // Falls back to QMD_OPENAI_API_KEY / OPENAI_API_KEY env var + model?: string; // Default: 'text-embedding-3-small' + expansion_model?: string; // Default: 'gpt-4o-mini' + rerank_model?: string; // Default: falls back to expansion_model + base_url?: string; // Base URL for embeddings (OpenAI-compatible) + chat_base_url?: string; // Separate base URL for expansion (falls back to base_url) + chat_api_key?: string; // Separate API key for chat endpoint (falls back to api_key) + rerank_base_url?: string; // Separate base URL for reranking (falls back to chat_base_url) + rerank_api_key?: string; // Separate API key for rerank endpoint (falls back to chat_api_key) + }; +} + /** * The complete configuration file structure */ @@ -51,6 +69,7 @@ export interface CollectionConfig { editor_uri_template?: string; // Alias for editor_uri collections: Record; // Collection name -> config models?: ModelsConfig; + embedding?: EmbeddingProviderConfig; // Optional embedding provider settings } /** @@ -510,3 +529,12 @@ export function isValidCollectionName(name: string): boolean { // Allow alphanumeric, hyphens, underscores return /^[a-zA-Z0-9_-]+$/.test(name); } + +/** + * Get embedding configuration from config file + * Returns default (local) config if not specified + */ +export function getEmbeddingConfig(): EmbeddingProviderConfig { + const config = loadConfig(); + return config.embedding || { provider: 'local' }; +} diff --git a/src/db.ts b/src/db.ts index 5fe7ab47..0ca4380d 100644 --- a/src/db.ts +++ b/src/db.ts @@ -69,6 +69,7 @@ export interface Database { exec(sql: string): void; prepare(sql: string): Statement; loadExtension(path: string): void; + transaction(fn: () => T): () => T; close(): void; } diff --git a/src/llm.ts b/src/llm.ts index 7cccc3fa..6c60c034 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -4,16 +4,21 @@ * Provides embeddings, text generation, and reranking using local GGUF models. */ -import { - getLlama, - resolveModelFile, - LlamaChatSession, - LlamaLogLevel, - type Llama, - type LlamaModel, - type LlamaEmbeddingContext, - type Token as LlamaToken, +import type { + Llama, + LlamaModel, + LlamaEmbeddingContext, + Token as LlamaToken, } from "node-llama-cpp"; + +// Lazy-load node-llama-cpp runtime to avoid triggering cmake builds in OpenAI-only mode +let _nodeLlamaCpp: typeof import("node-llama-cpp") | null = null; +async function loadNodeLlamaCpp() { + if (!_nodeLlamaCpp) { + _nodeLlamaCpp = await import("node-llama-cpp"); + } + return _nodeLlamaCpp; +} import { homedir } from "os"; import { join } from "path"; import { existsSync, mkdirSync, statSync, unlinkSync, readdirSync, readFileSync, writeFileSync, openSync, readSync, closeSync } from "fs"; @@ -344,6 +349,7 @@ export async function pullModels( } } + const { resolveModelFile } = await loadNodeLlamaCpp(); const path = await resolveModelFile(model, cacheDir); validateGgufFile(path, model); const sizeBytes = existsSync(path) ? statSync(path).size : 0; @@ -372,6 +378,16 @@ export interface LLM { */ embed(text: string, options?: EmbedOptions): Promise; + /** + * Get embeddings for multiple texts in a batch + */ + embedBatch(texts: string[]): Promise<(EmbeddingResult | null)[]>; + + /** + * Get the model name used for embeddings + */ + getModelName(): string; + /** * Generate text completion */ @@ -514,6 +530,13 @@ export class LlamaCpp implements LLM { return this.embedModelUri; } + /** + * Get the model name used for embeddings + */ + getModelName(): string { + return this.embedModelUri; + } + /** * Reset the inactivity timer. Called after each model operation. * When timer fires, models are unloaded to free memory (if no active sessions). @@ -619,10 +642,11 @@ export class LlamaCpp implements LLM { if (!this.llama) { const gpuMode = resolveLlamaGpuMode(); + const nodeLlama = await loadNodeLlamaCpp(); const loadLlama = async (gpu: LlamaGpuMode) => - await getLlama({ + await nodeLlama.getLlama({ build: allowBuild ? "autoAttempt" : "never", - logLevel: LlamaLogLevel.error, + logLevel: nodeLlama.LlamaLogLevel.error, gpu, skipDownload: !allowBuild, }); @@ -660,7 +684,7 @@ export class LlamaCpp implements LLM { */ private async resolveModel(modelUri: string): Promise { this.ensureModelCacheDir(); - // resolveModelFile handles HF URIs and downloads to the cache dir + const { resolveModelFile } = await loadNodeLlamaCpp(); const modelPath = await resolveModelFile(modelUri, this.modelCacheDir); validateGgufFile(modelPath, modelUri); return modelPath; @@ -1077,6 +1101,7 @@ export class LlamaCpp implements LLM { await this.ensureGenerateModel(); // Create fresh context -> sequence -> session for each call + const { LlamaChatSession } = await loadNodeLlamaCpp(); const context = await this.generateModel!.createContext(); const sequence = context.getSequence(); const session = new LlamaChatSession({ contextSequence: sequence }); @@ -1154,6 +1179,7 @@ export class LlamaCpp implements LLM { : `/no_think Expand this search query: ${query}`; // Create a bounded context for expansion to prevent large default VRAM allocations. + const { LlamaChatSession } = await loadNodeLlamaCpp(); const genContext = await this.generateModel!.createContext({ contextSize: this.expandContextSize, }); @@ -1663,3 +1689,130 @@ export async function disposeDefaultLlamaCpp(): Promise { defaultLlamaCpp = null; } } + +// ============================================================================= +// OpenAI Embedding Support +// ============================================================================= + +import { OpenAIEmbedding, type OpenAIConfig } from "./openai-llm.js"; + +/** + * Embedding provider configuration + */ +export type EmbeddingProvider = 'local' | 'openai'; + +export type EmbeddingConfig = { + provider: EmbeddingProvider; + openai?: OpenAIConfig; +}; + +// Default embedding config: use local llama-cpp +let embeddingConfig: EmbeddingConfig = { provider: 'local' }; +let openAIEmbedding: OpenAIEmbedding | null = null; + +/** + * Set the embedding configuration. Call before using embeddings. + */ +export function setEmbeddingConfig(config: EmbeddingConfig): void { + embeddingConfig = config; + // Reset OpenAI instance if config changes + openAIEmbedding = null; +} + +/** + * Get the current embedding configuration + */ +export function getEmbeddingConfig(): EmbeddingConfig { + return embeddingConfig; +} + +/** + * Check if using OpenAI for embeddings + */ +export function isUsingOpenAI(): boolean { + return embeddingConfig.provider === 'openai'; +} + +/** + * Get the appropriate LLM for embeddings based on config. + * Returns OpenAI embedding client if configured, otherwise local LlamaCpp. + */ +export function getDefaultEmbeddingLLM(): LLM { + if (embeddingConfig.provider === 'openai') { + if (!openAIEmbedding) { + openAIEmbedding = new OpenAIEmbedding(embeddingConfig.openai); + } + return openAIEmbedding; + } + return getDefaultLlamaCpp(); +} + +/** + * Lightweight ILLMSession wrapper for OpenAI — no LlamaCpp session manager needed. + */ +class OpenAILLMSession implements ILLMSession { + private llm: OpenAIEmbedding; + private abortController = new AbortController(); + private released = false; + private maxDurationTimer: ReturnType | null = null; + + constructor(llm: OpenAIEmbedding, options: LLMSessionOptions = {}) { + this.llm = llm; + const maxDuration = options.maxDuration ?? 10 * 60 * 1000; + if (maxDuration > 0) { + this.maxDurationTimer = setTimeout(() => { + this.abortController.abort(new Error("OpenAI session exceeded max duration")); + }, maxDuration); + this.maxDurationTimer.unref(); + } + } + + get isValid(): boolean { return !this.released && !this.abortController.signal.aborted; } + get signal(): AbortSignal { return this.abortController.signal; } + + release(): void { + if (this.released) return; + this.released = true; + if (this.maxDurationTimer) { clearTimeout(this.maxDurationTimer); this.maxDurationTimer = null; } + this.abortController.abort(new Error("Session released")); + } + + async embed(text: string, options?: EmbedOptions): Promise { + return this.llm.embed(text, options); + } + + async embedBatch(texts: string[], _options?: EmbedOptions): Promise<(EmbeddingResult | null)[]> { + return this.llm.embedBatch(texts); + } + + async expandQuery(query: string, options?: { context?: string; includeLexical?: boolean }): Promise { + return this.llm.expandQuery(query, options); + } + + async rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise { + return this.llm.rerank(query, documents, options); + } +} + +/** + * Execute a function with the correct session type based on embedding provider. + * OpenAI mode: lightweight wrapper, no node-llama-cpp loaded. + * Local mode: full LlamaCpp session with resource management. + */ +export async function withEmbeddingSession( + fn: (session: ILLMSession, modelName: string) => Promise, + options?: LLMSessionOptions & { storeLlm?: LlamaCpp } +): Promise { + if (isUsingOpenAI()) { + const llm = getDefaultEmbeddingLLM() as OpenAIEmbedding; + const session = new OpenAILLMSession(llm, options); + try { + return await fn(session, llm.getModelName()); + } finally { + session.release(); + } + } + + const llm = options?.storeLlm ?? getDefaultLlamaCpp(); + return withLLMSessionForLlm(llm, (session) => fn(session, llm.embedModelName), options); +} diff --git a/src/openai-llm.ts b/src/openai-llm.ts new file mode 100644 index 00000000..822502b7 --- /dev/null +++ b/src/openai-llm.ts @@ -0,0 +1,395 @@ +/** + * openai-llm.ts - OpenAI API embeddings for QMD + * + * Provides embedding generation using OpenAI's API instead of local models. + * Much faster and more reliable than local llama-cpp, costs ~$0.02/1M tokens. + */ + +import OpenAI from 'openai'; +import { get_encoding } from 'tiktoken'; +import type { + LLM, + EmbedOptions, + EmbeddingResult, + GenerateOptions, + GenerateResult, + RerankOptions, + RerankResult, + RerankDocument, + ModelInfo, + Queryable +} from './llm.js'; + +export type OpenAIConfig = { + apiKey?: string; + embedModel?: string; + expansionModel?: string; + rerankModel?: string; + baseURL?: string; + chatBaseURL?: string; + chatApiKey?: string; + rerankBaseURL?: string; + rerankApiKey?: string; + maxInputTokens?: number; +}; + +// Lazy tiktoken encoder (cl100k_base covers most OpenAI-compatible models) +let _enc: ReturnType | null = null; +function getEncoder() { + if (!_enc) _enc = get_encoding('cl100k_base'); + return _enc; +} + +function resolveMaxInputTokens(config?: number): number { + if (config !== undefined) return config; + const env = parseInt(process.env.QMD_OPENAI_MAX_INPUT_TOKENS ?? '', 10); + return Number.isFinite(env) && env > 0 ? env : 512; +} + +/** + * Truncate text to at most maxTokens tokens using tiktoken. + * Returns the original string if it's already within the limit. + */ +function truncateToTokenLimit(text: string, maxTokens: number): string { + const enc = getEncoder(); + const tokens = enc.encode(text); + if (tokens.length <= maxTokens) return text; + return new TextDecoder().decode(enc.decode(tokens.slice(0, maxTokens))); +} + +const DEFAULT_EMBED_MODEL = 'text-embedding-3-small'; +const DEFAULT_EXPANSION_MODEL = 'gpt-4o-mini'; + +// Retry configuration +const MAX_RETRIES = 5; +const BASE_DELAY_MS = 1000; +const MAX_DELAY_MS = 60000; + +/** + * Sleep for a given number of milliseconds + */ +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Retry a function with exponential backoff + jitter + * Handles rate limits (429) and transient errors (5xx) + */ +async function withRetry( + fn: () => Promise, + options: { maxRetries?: number; context?: string } = {} +): Promise { + const maxRetries = options.maxRetries ?? MAX_RETRIES; + const context = options.context ?? 'operation'; + + let lastError: unknown; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error: unknown) { + lastError = error; + + // Check if we should retry + const isRateLimit = error instanceof Error && + ('status' in error && (error as { status: number }).status === 429); + const isServerError = error instanceof Error && + ('status' in error && (error as { status: number }).status >= 500); + const isRetryable = isRateLimit || isServerError; + + if (!isRetryable || attempt === maxRetries) { + throw error; + } + + // Calculate delay with exponential backoff + jitter + const exponentialDelay = BASE_DELAY_MS * Math.pow(2, attempt); + const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter + const delay = Math.min(exponentialDelay + jitter, MAX_DELAY_MS); + + // Check for Retry-After header hint + let retryAfter = 0; + if (error instanceof Error && 'headers' in error) { + const headers = (error as { headers?: { get?: (k: string) => string | null } }).headers; + const retryAfterHeader = headers?.get?.('retry-after'); + if (retryAfterHeader) { + retryAfter = parseInt(retryAfterHeader, 10) * 1000; + } + } + + const finalDelay = Math.max(delay, retryAfter); + console.warn(`[OpenAI] ${context} failed (attempt ${attempt + 1}/${maxRetries + 1}), ` + + `retrying in ${Math.round(finalDelay / 1000)}s...`); + + await sleep(finalDelay); + } + } + + throw lastError; +} + +/** + * OpenAI LLM implementation - primarily for embeddings + */ +export class OpenAIEmbedding implements LLM { + private client: OpenAI; + private chatClient: OpenAI; + private rerankClient: OpenAI; + private embedModel: string; + private expansionModel: string; + private rerankModel: string; + private maxInputTokens: number; + + constructor(config: OpenAIConfig = {}) { + const apiKey = config.apiKey || process.env.QMD_OPENAI_API_KEY || process.env.OPENAI_API_KEY; + const baseURL = config.baseURL || process.env.QMD_OPENAI_BASE_URL; + + this.client = new OpenAI({ apiKey, baseURL }); + + const chatApiKey = config.chatApiKey || process.env.QMD_OPENAI_CHAT_API_KEY || apiKey; + const chatBaseURL = config.chatBaseURL || process.env.QMD_OPENAI_CHAT_BASE_URL || baseURL; + this.chatClient = (chatBaseURL !== baseURL || chatApiKey !== apiKey) + ? new OpenAI({ apiKey: chatApiKey, baseURL: chatBaseURL }) + : this.client; + + // Rerank client: falls back to chat client, then base client + const rerankApiKey = config.rerankApiKey || process.env.QMD_OPENAI_RERANK_API_KEY || chatApiKey; + const rerankBaseURL = config.rerankBaseURL || process.env.QMD_OPENAI_RERANK_BASE_URL || chatBaseURL; + this.rerankClient = (rerankBaseURL !== chatBaseURL || rerankApiKey !== chatApiKey) + ? new OpenAI({ apiKey: rerankApiKey, baseURL: rerankBaseURL }) + : this.chatClient; + + this.embedModel = config.embedModel || DEFAULT_EMBED_MODEL; + this.expansionModel = config.expansionModel || DEFAULT_EXPANSION_MODEL; + this.rerankModel = config.rerankModel || config.expansionModel || DEFAULT_EXPANSION_MODEL; + this.maxInputTokens = resolveMaxInputTokens(config.maxInputTokens); + } + + async embed(text: string, options?: EmbedOptions): Promise { + const input = truncateToTokenLimit(text, this.maxInputTokens); + return withRetry(async () => { + const response = await this.client.embeddings.create({ + model: this.embedModel, + input, + }); + const data = response.data[0]; + if (!data) { + throw new Error('No embedding data returned from OpenAI'); + } + return { + embedding: data.embedding, + model: this.embedModel, + }; + }, { context: 'embed' }); + } + + getModelName(): string { + return this.embedModel; + } + + async embedBatch(texts: string[]): Promise<(EmbeddingResult | null)[]> { + const inputs = texts.map(t => truncateToTokenLimit(t, this.maxInputTokens)); + return withRetry(async () => { + // OpenAI supports batch embedding natively + const response = await this.client.embeddings.create({ + model: this.embedModel, + input: inputs, + }); + return response.data.map(item => ({ + embedding: item.embedding, + model: this.embedModel, + })); + }, { context: `embedBatch(${texts.length} texts)` }); + } + + // Stub implementations for other LLM interface methods + async generate(prompt: string, options?: GenerateOptions): Promise { + // Not implemented - use local model for generation + console.warn('OpenAIEmbedding.generate() not implemented, use local model'); + return null; + } + + async modelExists(model: string): Promise { + return { + name: model, + exists: model === this.embedModel, + }; + } + + async expandQuery(query: string, options?: { context?: string, includeLexical?: boolean }): Promise { + const includeLexical = options?.includeLexical ?? true; + + try { + const response = await withRetry(() => this.chatClient.chat.completions.create({ + model: this.expansionModel, + messages: [ + { + role: 'system', + content: `You are a search query expander. Given a search query, generate expanded versions for different search backends. + +Output format (one per line): +lex: +vec: +hyde: + +Generate 1-2 of each type. Be concise. Include the original query terms.` + }, + { + role: 'user', + content: query + } + ], + temperature: 0.7, + max_tokens: 300, + }), { context: 'expandQuery' }); + + const content = response.choices[0]?.message?.content || ''; + const lines = content.trim().split('\n'); + + const queryables: Queryable[] = []; + for (const line of lines) { + const colonIdx = line.indexOf(':'); + if (colonIdx === -1) continue; + + const type = line.slice(0, colonIdx).trim().toLowerCase(); + if (type !== 'lex' && type !== 'vec' && type !== 'hyde') continue; + + const text = line.slice(colonIdx + 1).trim(); + if (!text) continue; + + queryables.push({ type: type as 'lex' | 'vec' | 'hyde', text }); + } + + // Filter lex if not requested + const filtered = includeLexical ? queryables : queryables.filter(q => q.type !== 'lex'); + + if (filtered.length > 0) return filtered; + + // Fallback if parsing failed + const fallback: Queryable[] = [ + { type: 'vec', text: query }, + { type: 'hyde', text: `Information about ${query}` }, + ]; + if (includeLexical) fallback.unshift({ type: 'lex', text: query }); + return fallback; + + } catch (error) { + console.error('OpenAI query expansion error:', error); + // Fallback to original query + const fallback: Queryable[] = [{ type: 'vec', text: query }]; + if (includeLexical) fallback.unshift({ type: 'lex', text: query }); + return fallback; + } + } + + async rerank(query: string, documents: RerankDocument[], options?: RerankOptions): Promise { + if (documents.length === 0) { + return { results: [], model: `${this.rerankModel}-rerank` }; + } + + // For very small sets, skip the API call — not worth the latency + if (documents.length <= 2) { + return { + results: documents.map((doc, index) => ({ + file: doc.file, + score: 1 - (index * 0.01), + index, + })), + model: 'passthrough', + }; + } + + try { + // Truncate documents for the prompt — 500 chars each is enough for relevance judgment + const truncated = documents.map((doc, i) => + `[${i}] ${doc.title ? doc.title + ': ' : ''}${doc.text.slice(0, 500).replace(/\n+/g, ' ')}` + ); + + const response = await withRetry(() => this.rerankClient.chat.completions.create({ + model: this.rerankModel, + messages: [ + { + role: 'system', + content: `You are a document relevance ranker. Given a search query and numbered documents, rank them by relevance. + +Output ONLY a comma-separated list of document indices, most relevant first. Include ALL indices exactly once. +Example output for 5 documents: 2,0,4,1,3` + }, + { + role: 'user', + content: `Query: ${query}\n\nDocuments:\n${truncated.join('\n\n')}` + } + ], + temperature: 0, + max_tokens: 200, + }), { context: 'rerank' }); + + const content = response.choices[0]?.message?.content?.trim() || ''; + + // Parse the comma-separated indices + const indices = content + .replace(/[^0-9,]/g, '') // Strip anything that isn't a digit or comma + .split(',') + .map(s => parseInt(s.trim(), 10)) + .filter(n => !isNaN(n) && n >= 0 && n < documents.length); + + // Deduplicate while preserving order + const seen = new Set(); + const uniqueIndices: number[] = []; + for (const idx of indices) { + if (!seen.has(idx)) { + seen.add(idx); + uniqueIndices.push(idx); + } + } + + // Add any missing indices at the end (in case the model missed some) + for (let i = 0; i < documents.length; i++) { + if (!seen.has(i)) { + uniqueIndices.push(i); + } + } + + // Convert rank position to score (1.0 for rank 1, decreasing) + const results = uniqueIndices.map((docIndex, rank) => ({ + file: documents[docIndex]!.file, + score: 1.0 - (rank / uniqueIndices.length), + index: docIndex, + })); + + return { + results, + model: `${this.rerankModel}-rerank`, + }; + } catch (error) { + // Fallback: preserve original order if reranking fails + console.warn('[OpenAI] Rerank failed, preserving original order:', + error instanceof Error ? error.message : String(error)); + return { + results: documents.map((doc, index) => ({ + file: doc.file, + score: 1 - (index * 0.01), + index, + })), + model: 'passthrough-fallback', + }; + } + } + + async dispose(): Promise { + // No resources to dispose for API client + } +} + +// Singleton instance +let defaultOpenAI: OpenAIEmbedding | null = null; + +export function getDefaultOpenAI(config?: OpenAIConfig): OpenAIEmbedding { + if (!defaultOpenAI) { + defaultOpenAI = new OpenAIEmbedding(config); + } + return defaultOpenAI; +} + +export function setDefaultOpenAI(llm: OpenAIEmbedding | null): void { + defaultOpenAI = llm; +} diff --git a/src/store.ts b/src/store.ts index 16a55b7d..780c5d95 100644 --- a/src/store.ts +++ b/src/store.ts @@ -18,12 +18,16 @@ import { createHash } from "crypto"; import { readFileSync, realpathSync, statSync, mkdirSync } from "node:fs"; // Note: node:path resolve is not imported — we export our own cross-platform resolve() import fastGlob from "fast-glob"; +import { encoding_for_model } from "tiktoken"; import { LlamaCpp, getDefaultLlamaCpp, + getDefaultEmbeddingLLM, + isUsingOpenAI, formatQueryForEmbedding, formatDocForEmbedding, withLLMSessionForLlm, + withEmbeddingSession, type RerankDocument, type ILLMSession, } from "./llm.js"; @@ -1125,8 +1129,8 @@ export type Store = { toVirtualPath: (absolutePath: string) => string | null; // Search - searchFTS: (query: string, limit?: number, collectionName?: string) => SearchResult[]; - searchVec: (query: string, model: string, limit?: number, collectionName?: string, session?: ILLMSession, precomputedEmbedding?: number[]) => Promise; + searchFTS: (query: string, limit?: number, collections?: string | string[]) => SearchResult[]; + searchVec: (query: string, model: string, limit?: number, collections?: string | string[], session?: ILLMSession, precomputedEmbedding?: number[]) => Promise; // Query expansion & reranking expandQuery: (query: string, model?: string, intent?: string) => Promise; @@ -1429,12 +1433,7 @@ export async function generateEmbeddings( const totalDocs = docsToEmbed.length; const startTime = Date.now(); - // Use store's LlamaCpp or global singleton, wrapped in a session - const llm = getLlm(store); - const embedModelUri = llm.embedModelName; - - // Create a session manager for this llm instance - const result = await withLLMSessionForLlm(llm, async (session) => { + const result = await withEmbeddingSession(async (session, embedModelUri) => { let chunksEmbedded = 0; let errors = 0; let bytesProcessed = 0; @@ -1444,7 +1443,6 @@ export async function generateEmbeddings( const batches = buildEmbeddingBatches(docsToEmbed, maxDocsPerBatch, maxBatchBytes); for (const batchMeta of batches) { - // Abort early if session has been invalidated if (!session.isValid) { console.warn(`⚠ Session expired — skipping remaining document batches`); break; @@ -1490,7 +1488,7 @@ export async function generateEmbeddings( if (!vectorTableInitialized) { const firstChunk = batchChunks[0]!; const firstText = formatDocForEmbedding(firstChunk.text, firstChunk.title, embedModelUri); - const firstResult = await session.embed(firstText, { model }); + const firstResult = await session.embed(firstText); if (!firstResult) { throw new Error("Failed to get embedding dimensions from first chunk"); } @@ -1502,7 +1500,6 @@ export async function generateEmbeddings( let batchChunkBytesProcessed = 0; for (let batchStart = 0; batchStart < batchChunks.length; batchStart += BATCH_SIZE) { - // Abort early if session has been invalidated (e.g. max duration exceeded) if (!session.isValid) { const remaining = batchChunks.length - batchStart; errors += remaining; @@ -1510,7 +1507,6 @@ export async function generateEmbeddings( break; } - // Abort early if error rate is too high (>80% of processed chunks failed) const processed = chunksEmbedded + errors; if (processed >= BATCH_SIZE && errors > processed * 0.8) { const remaining = batchChunks.length - batchStart; @@ -1524,7 +1520,7 @@ export async function generateEmbeddings( const texts = chunkBatch.map(chunk => formatDocForEmbedding(chunk.text, chunk.title, embedModelUri)); try { - const embeddings = await session.embedBatch(texts, { model }); + const embeddings = await session.embedBatch(texts); for (let i = 0; i < chunkBatch.length; i++) { const chunk = chunkBatch[i]!; const embedding = embeddings[i]; @@ -1537,8 +1533,6 @@ export async function generateEmbeddings( batchChunkBytesProcessed += chunk.bytes; } } catch { - // Batch failed — try individual embeddings as fallback - // But skip if session is already invalid (avoids N doomed retries) if (!session.isValid) { errors += chunkBatch.length; batchChunkBytesProcessed += chunkBatch.reduce((sum, c) => sum + c.bytes, 0); @@ -1546,7 +1540,7 @@ export async function generateEmbeddings( for (const chunk of chunkBatch) { try { const text = formatDocForEmbedding(chunk.text, chunk.title, embedModelUri); - const result = await session.embed(text, { model }); + const result = await session.embed(text); if (result) { insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(result.embedding), model, now); chunksEmbedded++; @@ -1578,7 +1572,7 @@ export async function generateEmbeddings( } return { chunksEmbedded, errors }; - }, { maxDuration: 30 * 60 * 1000, name: 'generateEmbeddings' }); + }, { maxDuration: 30 * 60 * 1000, name: 'generateEmbeddings', storeLlm: store.llm ?? undefined }); return { docsProcessed: totalDocs, @@ -1639,8 +1633,8 @@ export function createStore(dbPath?: string): Store { toVirtualPath: (absolutePath: string) => toVirtualPath(db, absolutePath), // Search - searchFTS: (query: string, limit?: number, collectionName?: string) => searchFTS(db, query, limit, collectionName), - searchVec: (query: string, model: string, limit?: number, collectionName?: string, session?: ILLMSession, precomputedEmbedding?: number[]) => searchVec(db, query, model, limit, collectionName, session, precomputedEmbedding), + searchFTS: (query: string, limit?: number, collections?: string | string[]) => searchFTS(db, query, limit, collections), + searchVec: (query: string, model: string, limit?: number, collections?: string | string[], session?: ILLMSession, precomputedEmbedding?: number[]) => searchVec(db, query, model, limit, collections, session, precomputedEmbedding), // Query expansion & reranking expandQuery: (query: string, model?: string, intent?: string) => expandQuery(query, model, db, intent, store.llm), @@ -2267,6 +2261,17 @@ export async function chunkDocumentAsync( * When filepath and chunkStrategy are provided, uses AST-aware break points * for supported code files. */ +// Cached tiktoken encoder for OpenAI tokenization +let tiktokenEncoder: ReturnType | null = null; + +function getTiktokenEncoder() { + if (!tiktokenEncoder) { + // Use cl100k_base which is used by text-embedding-3-small + tiktokenEncoder = encoding_for_model("gpt-4"); + } + return tiktokenEncoder; +} + export async function chunkDocumentByTokens( content: string, maxTokens: number = CHUNK_SIZE_TOKENS, @@ -2276,6 +2281,12 @@ export async function chunkDocumentByTokens( chunkStrategy: ChunkStrategy = "regex", signal?: AbortSignal ): Promise<{ text: string; pos: number; tokens: number }[]> { + // Use tiktoken for OpenAI (fast, no model loading) + // Use llama-cpp for local models (accurate to the model) + if (isUsingOpenAI()) { + return chunkWithTiktoken(content, maxTokens, overlapTokens); + } + const llm = getDefaultLlamaCpp(); // Use moderate chars/token estimate (prose ~4, code ~2, mixed ~3) @@ -2357,6 +2368,86 @@ export async function chunkDocumentByTokens( return results; } +/** + * Chunk using tiktoken (fast, for OpenAI embeddings) + */ +function chunkWithTiktoken( + content: string, + maxTokens: number, + overlapTokens: number +): { text: string; pos: number; tokens: number }[] { + const encoder = getTiktokenEncoder(); + // Allow all special tokens in documents (they might contain code examples, etc.) + const allTokens = encoder.encode(content, "all"); + const totalTokens = allTokens.length; + + if (totalTokens <= maxTokens) { + return [{ text: content, pos: 0, tokens: totalTokens }]; + } + + const chunks: { text: string; pos: number; tokens: number }[] = []; + const step = maxTokens - overlapTokens; + const decoder = new TextDecoder(); + let tokenPos = 0; + + while (tokenPos < totalTokens) { + const chunkEnd = Math.min(tokenPos + maxTokens, totalTokens); + const chunkTokens = allTokens.slice(tokenPos, chunkEnd); + let chunkText = decoder.decode(encoder.decode(chunkTokens)); + + // Find a good break point if not at end of document + if (chunkEnd < totalTokens) { + chunkText = findGoodBreakPoint(chunkText); + } + + // Approximate character position + const avgCharsPerToken = content.length / totalTokens; + const charPos = Math.floor(tokenPos * avgCharsPerToken); + chunks.push({ text: chunkText, pos: charPos, tokens: chunkTokens.length }); + + if (chunkEnd >= totalTokens) break; + tokenPos += step; + } + + return chunks; +} + +/** + * Find a good break point in text (paragraph, sentence, or line) + */ +function findGoodBreakPoint(text: string): string { + const searchStart = Math.floor(text.length * 0.7); + const searchSlice = text.slice(searchStart); + + let breakOffset = -1; + const paragraphBreak = searchSlice.lastIndexOf('\n\n'); + if (paragraphBreak >= 0) { + breakOffset = paragraphBreak + 2; + } else { + const sentenceEnd = Math.max( + searchSlice.lastIndexOf('. '), + searchSlice.lastIndexOf('.\n'), + searchSlice.lastIndexOf('? '), + searchSlice.lastIndexOf('?\n'), + searchSlice.lastIndexOf('! '), + searchSlice.lastIndexOf('!\n') + ); + if (sentenceEnd >= 0) { + breakOffset = sentenceEnd + 2; + } else { + const lineBreak = searchSlice.lastIndexOf('\n'); + if (lineBreak >= 0) { + breakOffset = lineBreak + 1; + } + } + } + + if (breakOffset >= 0) { + return text.slice(0, searchStart + breakOffset); + } + return text; +} + // ============================================================================= // Fuzzy matching // ============================================================================= @@ -3021,7 +3112,7 @@ export function validateLexQuery(query: string): string | null { return null; } -export function searchFTS(db: Database, query: string, limit: number = 20, collectionName?: string): SearchResult[] { +export function searchFTS(db: Database, query: string, limit: number = 20, collections?: string | string[]): SearchResult[] { const ftsQuery = buildFTS5Query(query); if (!ftsQuery) return []; @@ -3035,7 +3126,7 @@ export function searchFTS(db: Database, query: string, limit: number = 20, colle // When filtering by collection, fetch extra candidates from the FTS index // since some will be filtered out. Without a collection filter we can // fetch exactly the requested limit. - const ftsLimit = collectionName ? limit * 10 : limit; + const ftsLimit = collections ? limit * 10 : limit; let sql = ` WITH fts_matches AS ( @@ -3058,9 +3149,16 @@ export function searchFTS(db: Database, query: string, limit: number = 20, colle WHERE d.active = 1 `; - if (collectionName) { - sql += ` AND d.collection = ?`; - params.push(String(collectionName)); + if (collections) { + const collArray = Array.isArray(collections) ? collections : [collections]; + if (collArray.length === 1 && collArray[0]) { + sql += ` AND d.collection = ?`; + params.push(collArray[0]); + } else if (collArray.length > 1) { + const valid = collArray.filter(Boolean); + sql += ` AND d.collection IN (${valid.map(() => '?').join(',')})`; + params.push(...valid); + } } // bm25 lower is better; sort ascending. @@ -3096,7 +3194,7 @@ export function searchFTS(db: Database, query: string, limit: number = 20, colle // Vector Search // ============================================================================= -export async function searchVec(db: Database, query: string, model: string, limit: number = 20, collectionName?: string, session?: ILLMSession, precomputedEmbedding?: number[]): Promise { +export async function searchVec(db: Database, query: string, model: string, limit: number = 20, collections?: string | string[], session?: ILLMSession, precomputedEmbedding?: number[]): Promise { const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get(); if (!tableExists) return []; @@ -3139,9 +3237,16 @@ export async function searchVec(db: Database, query: string, model: string, limi `; const params: string[] = [...hashSeqs]; - if (collectionName) { - docSql += ` AND d.collection = ?`; - params.push(collectionName); + if (collections) { + const collArray = Array.isArray(collections) ? collections : [collections]; + if (collArray.length === 1 && collArray[0]) { + docSql += ` AND d.collection = ?`; + params.push(collArray[0]); + } else if (collArray.length > 1) { + const valid = collArray.filter(Boolean); + docSql += ` AND d.collection IN (${valid.map(() => '?').join(',')})`; + params.push(...valid); + } } const docRows = db.prepare(docSql).all(...params) as { @@ -3189,6 +3294,15 @@ export async function searchVec(db: Database, query: string, model: string, limi async function getEmbedding(text: string, model: string, isQuery: boolean, session?: ILLMSession, llmOverride?: LlamaCpp): Promise { // Format text using the appropriate prompt template const formattedText = isQuery ? formatQueryForEmbedding(text, model) : formatDocForEmbedding(text, undefined, model); + + // Always use OpenAI when configured, regardless of session + if (isUsingOpenAI()) { + const llm = llmOverride ?? getDefaultEmbeddingLLM(); + const result = await llm.embed(formattedText, { model, isQuery }); + return result?.embedding || null; + } + + // Use session if available, otherwise local default model const result = session ? await session.embed(formattedText, { model, isQuery }) : await (llmOverride ?? getDefaultLlamaCpp()).embed(formattedText, { model, isQuery }); @@ -3256,6 +3370,17 @@ export function insertEmbedding( // ============================================================================= export async function expandQuery(query: string, model: string = DEFAULT_QUERY_MODEL, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise { + // Use OpenAI query expansion when configured (fast, ~200ms via gpt-4o-mini) + if (isUsingOpenAI()) { + const openaiLLM = getDefaultEmbeddingLLM(); + const results = await openaiLLM.expandQuery(query, { context: intent, includeLexical: true }); + const expanded = results + .filter(r => r.text !== query) + .map(r => ({ type: r.type, query: r.text })); + if (expanded.length > 0) return expanded; + return [{ type: 'lex' as const, query }]; + } + // Check cache first — stored as JSON preserving types const cacheKey = getCacheKey("expandQuery", { query, model, ...(intent && { intent }) }); const cached = getCachedResult(db, cacheKey); @@ -3273,8 +3398,7 @@ export async function expandQuery(query: string, model: string = DEFAULT_QUERY_M } } - const llm = llmOverride ?? getDefaultLlamaCpp(); - // Note: LlamaCpp uses hardcoded model, model parameter is ignored + const llm = isUsingOpenAI() ? getDefaultEmbeddingLLM() : (llmOverride ?? getDefaultLlamaCpp()); const results = await llm.expandQuery(query, { intent }); // Map Queryable[] → ExpandedQuery[] (same shape, decoupled from llm.ts internals). @@ -3295,8 +3419,21 @@ export async function expandQuery(query: string, model: string = DEFAULT_QUERY_M // ============================================================================= export async function rerank(query: string, documents: { file: string; text: string }[], model: string = DEFAULT_RERANK_MODEL, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise<{ file: string; score: number }[]> { + // Use OpenAI-based reranking when in OpenAI mode + if (isUsingOpenAI()) { + const embeddingLLM = getDefaultEmbeddingLLM(); + const rerankDocs: RerankDocument[] = documents.map((doc) => ({ + file: doc.file, + text: doc.text.slice(0, 4000), + })); + const result = await embeddingLLM.rerank(query, rerankDocs); + return result.results.map((r) => ({ file: r.file, score: r.score })); + } + // Prepend intent to rerank query so the reranker scores with domain context - const rerankQuery = intent ? `${intent}\n\n${query}` : query; + const rerankQuery = intent ? `${intent} + +${query}` : query; const cachedResults: Map = new Map(); const uncachedDocsByChunk: Map = new Map(); @@ -4087,8 +4224,8 @@ export async function hybridQuery( } // Batch embed all vector queries in a single call - const llm = getLlm(store); - const textsToEmbed = vecQueries.map(q => formatQueryForEmbedding(q.text, llm.embedModelName)); + const llm = getDefaultEmbeddingLLM(); + const textsToEmbed = vecQueries.map(q => formatQueryForEmbedding(q.text, llm.getModelName())); hooks?.onEmbedStart?.(textsToEmbed.length); const embedStart = Date.now(); const embeddings = await llm.embedBatch(textsToEmbed); @@ -4470,8 +4607,9 @@ export async function structuredSearch( s.type === 'vec' || s.type === 'hyde' ); if (vecSearches.length > 0) { - const llm = getLlm(store); - const textsToEmbed = vecSearches.map(s => formatQueryForEmbedding(s.query, llm.embedModelName)); + const llm = isUsingOpenAI() ? getDefaultEmbeddingLLM() : getLlm(store); + const modelName = isUsingOpenAI() ? llm.getModelName() : (llm as LlamaCpp).embedModelName; + const textsToEmbed = vecSearches.map(s => formatQueryForEmbedding(s.query, modelName)); hooks?.onEmbedStart?.(textsToEmbed.length); const embedStart = Date.now(); const embeddings = await llm.embedBatch(textsToEmbed); diff --git a/test/qmd-manager.test.ts b/test/qmd-manager.test.ts new file mode 100644 index 00000000..a779e690 --- /dev/null +++ b/test/qmd-manager.test.ts @@ -0,0 +1,281 @@ +/** + * qmd-manager.test.ts — Unit tests for QmdManager + * + * QmdManager is a high-level orchestrator that wraps QMDStore with: + * - configurable similarity thresholds (minScore, limit) + * - pluggable similarity engine (LlamaCpp) + * - pluggable storage (SQLite Database) + * - inactivity timer for automatic resource cleanup + * + * All three dependencies (timer, storage, similarity engine) are injected + * so tests run fully in-memory without touching the filesystem or GPU. + * + * Run with: bun test qmd-manager.test.ts + */ + +import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; +import type { QmdManager, QmdManagerOptions } from "../src/qmd-manager.js"; + +// ============================================================================= +// Dependency Mocks +// ============================================================================= + +/** + * Mock LlamaCpp (similarity engine). + * Mirrors the shape used by the real LlamaCpp in src/llm.ts: + * - embed() → number[][] + * - rerank() → scored candidates + * - dispose() + */ +function makeMockSimilarityEngine() { + return { + embed: vi.fn().mockResolvedValue([[0.1, 0.2, 0.3]]), + rerank: vi.fn().mockResolvedValue([]), + dispose: vi.fn().mockResolvedValue(undefined), + // Inactivity-timer hook — checked by QmdManager before auto-dispose + canUnload: vi.fn().mockReturnValue(true), + }; +} + +/** + * Mock SQLite Database (storage layer). + * Mirrors the minimal interface QmdManager uses from src/db.ts. + */ +function makeMockStorage() { + return { + prepare: vi.fn().mockReturnValue({ + get: vi.fn().mockReturnValue(undefined), + all: vi.fn().mockReturnValue([]), + run: vi.fn(), + }), + exec: vi.fn(), + close: vi.fn(), + }; +} + +/** + * Mock timer — replaces the real inactivity setTimeout/clearTimeout so + * tests can advance time without waiting. + * + * The returned object exposes `fire()` to manually trigger the callback, + * which lets tests assert cleanup behaviour without `vi.useFakeTimers`. + */ +function makeMockTimer() { + let callback: (() => void) | null = null; + let scheduled = false; + + return { + schedule: vi.fn((fn: () => void, _ms: number) => { + callback = fn; + scheduled = true; + }), + cancel: vi.fn(() => { + callback = null; + scheduled = false; + }), + /** Manually trigger the scheduled callback (simulates timeout firing). */ + fire() { + if (callback) callback(); + }, + get isScheduled() { + return scheduled; + }, + }; +} + +// ============================================================================= +// Factory Helper +// ============================================================================= + +/** + * Instantiate a QmdManager with fully-mocked dependencies. + * + * @param overrides Partial QmdManagerOptions merged on top of safe defaults. + * + * Default thresholds: + * minScore = 0.0 (accept all results) + * limit = 10 + * inactivityTimeoutMs = 300_000 (5 min) + */ +async function createTestManager(overrides: Partial = {}): Promise<{ + manager: QmdManager; + similarityEngine: ReturnType; + storage: ReturnType; + timer: ReturnType; +}> { + const similarityEngine = overrides.similarityEngine ?? makeMockSimilarityEngine(); + const storage = overrides.storage ?? makeMockStorage(); + const timer = overrides.timer ?? makeMockTimer(); + + const { QmdManager } = await import("../src/qmd-manager.js"); + + const manager = new QmdManager({ + minScore: 0.0, + limit: 10, + inactivityTimeoutMs: 300_000, + ...overrides, + similarityEngine: similarityEngine as any, + storage: storage as any, + timer: timer as any, + }); + + return { manager, similarityEngine, storage, timer }; +} + +// ============================================================================= +// Lifecycle +// ============================================================================= + +describe("QmdManager — lifecycle", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("constructs without error given valid options", async () => { + const { manager } = await createTestManager(); + expect(manager).toBeDefined(); + }); + + test("close() disposes the similarity engine", async () => { + const { manager, similarityEngine } = await createTestManager(); + await manager.close(); + expect(similarityEngine.dispose).toHaveBeenCalledOnce(); + }); + + test("close() closes the storage", async () => { + const { manager, storage } = await createTestManager(); + await manager.close(); + expect(storage.close).toHaveBeenCalledOnce(); + }); + + test("close() cancels any pending inactivity timer", async () => { + const { manager, timer } = await createTestManager(); + await manager.close(); + expect(timer.cancel).toHaveBeenCalled(); + }); +}); + +// ============================================================================= +// Inactivity Timer +// ============================================================================= + +describe("QmdManager — inactivity timer", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("schedules inactivity timer after construction when timeout > 0", async () => { + const { timer } = await createTestManager({ inactivityTimeoutMs: 5_000 }); + expect(timer.schedule).toHaveBeenCalledWith(expect.any(Function), 5_000); + }); + + test("does not schedule timer when inactivityTimeoutMs is 0", async () => { + const { timer } = await createTestManager({ inactivityTimeoutMs: 0 }); + expect(timer.schedule).not.toHaveBeenCalled(); + }); + + test("timer fires → calls dispose on similarity engine when idle", async () => { + const { timer, similarityEngine } = await createTestManager({ inactivityTimeoutMs: 5_000 }); + // Simulate the timer firing while nothing is in-flight + similarityEngine.canUnload.mockReturnValue(true); + timer.fire(); + expect(similarityEngine.dispose).toHaveBeenCalled(); + }); + + test("timer fires → skips dispose when similarity engine is busy", async () => { + const { timer, similarityEngine } = await createTestManager({ inactivityTimeoutMs: 5_000 }); + similarityEngine.canUnload.mockReturnValue(false); + timer.fire(); + expect(similarityEngine.dispose).not.toHaveBeenCalled(); + }); +}); + +// ============================================================================= +// Threshold Options +// ============================================================================= + +describe("QmdManager — threshold options", () => { + test("accepts default threshold options", async () => { + const { manager } = await createTestManager(); + expect(manager.options.minScore).toBe(0.0); + expect(manager.options.limit).toBe(10); + }); + + test("accepts custom minScore threshold", async () => { + const { manager } = await createTestManager({ minScore: 0.75 }); + expect(manager.options.minScore).toBe(0.75); + }); + + test("accepts custom result limit", async () => { + const { manager } = await createTestManager({ limit: 25 }); + expect(manager.options.limit).toBe(25); + }); + + test("rejects minScore outside [0, 1]", async () => { + await expect(createTestManager({ minScore: -0.1 })).rejects.toThrow(); + await expect(createTestManager({ minScore: 1.1 })).rejects.toThrow(); + }); + + test("rejects limit <= 0", async () => { + await expect(createTestManager({ limit: 0 })).rejects.toThrow(); + await expect(createTestManager({ limit: -1 })).rejects.toThrow(); + }); +}); + +// ============================================================================= +// Search Integration (wired through mocked dependencies) +// ============================================================================= + +describe("QmdManager — search", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("search() calls similarity engine embed with the query", async () => { + const { manager, similarityEngine } = await createTestManager(); + await manager.search("authentication flow"); + expect(similarityEngine.embed).toHaveBeenCalledWith( + expect.stringContaining("authentication flow"), + ); + }); + + test("search() filters results below minScore threshold", async () => { + const { manager, similarityEngine } = await createTestManager({ minScore: 0.8 }); + // Mock returns a mix of high and low-score docs + similarityEngine.rerank.mockResolvedValue([ + { path: "high.md", score: 0.9 }, + { path: "low.md", score: 0.5 }, + ]); + + const results = await manager.search("query"); + expect(results.every((r) => r.score >= 0.8)).toBe(true); + }); + + test("search() respects the limit option", async () => { + const { manager, similarityEngine } = await createTestManager({ limit: 3 }); + similarityEngine.rerank.mockResolvedValue([ + { path: "a.md", score: 0.9 }, + { path: "b.md", score: 0.85 }, + { path: "c.md", score: 0.8 }, + { path: "d.md", score: 0.75 }, + ]); + + const results = await manager.search("query"); + expect(results.length).toBeLessThanOrEqual(3); + }); + + test("search() returns empty array when similarity engine finds nothing", async () => { + const { manager, similarityEngine } = await createTestManager(); + similarityEngine.rerank.mockResolvedValue([]); + const results = await manager.search("unknown topic"); + expect(results).toEqual([]); + }); + + test("search() resets the inactivity timer after each call", async () => { + const { manager, timer } = await createTestManager({ inactivityTimeoutMs: 5_000 }); + const callsBefore = (timer.schedule as ReturnType).mock.calls.length; + await manager.search("query"); + const callsAfter = (timer.schedule as ReturnType).mock.calls.length; + expect(callsAfter).toBeGreaterThan(callsBefore); + }); +});