diff --git a/.env.example b/.env.example index ddf1556..3970ea2 100644 --- a/.env.example +++ b/.env.example @@ -2,13 +2,6 @@ # Copy this file to .env and fill in your values: # cp .env.example .env -# ======================== -# REQUIRED -# ======================== - -# Your Anthropic API key (starts with sk-ant-) -ANTHROPIC_API_KEY= - # ======================== # OPTIONAL: Slack # ======================== diff --git a/CLAUDE.md b/CLAUDE.md index f5d3cd7..29f9603 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -163,7 +163,7 @@ MCP flow: External client -> /mcp endpoint -> bearer auth -> MCP Server -> tool **Docker (recommended for new installs):** ```bash git clone https://github.com/ghostwright/phantom.git && cd phantom -cp .env.example .env # add ANTHROPIC_API_KEY + Slack tokens +cp .env.example .env # add Slack tokens (auth handled by Agent SDK) docker compose up -d ``` diff --git a/README.md b/README.md index 81dd0c8..dced0e7 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ This is what happens when you give an AI its own computer. ```bash curl -fsSL https://raw.githubusercontent.com/ghostwright/phantom/main/docker-compose.user.yaml -o docker-compose.yaml curl -fsSL https://raw.githubusercontent.com/ghostwright/phantom/main/.env.example -o .env -# Edit .env - add your ANTHROPIC_API_KEY, Slack tokens, and OWNER_SLACK_USER_ID +# Edit .env - add your Slack tokens and OWNER_SLACK_USER_ID docker compose up -d ``` @@ -294,7 +294,7 @@ docker exec phantom-ollama ollama pull nomic-embed-text bun run phantom init --yes # Set your API key -export ANTHROPIC_API_KEY=sk-ant-... +# Auth is handled by the Claude Agent SDK (OAuth/Claude MAX) — no API key needed # Start bun run phantom start diff --git a/bun.lock b/bun.lock index bdf0674..4a3bf1d 100644 --- a/bun.lock +++ b/bun.lock @@ -6,9 +6,9 @@ "name": "phantom", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.77", - "@anthropic-ai/sdk": "^0.80.0", "@modelcontextprotocol/sdk": "^1.28.0", "@slack/bolt": "^4.6.0", + "clawmem": "file:../ClawMem", "croner": "^10.0.1", "imapflow": "^1.2.18", "nodemailer": "^8.0.4", @@ -29,10 +29,6 @@ "packages": { "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.84", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-rvp3kZJM4IgDBE1zwj30H3N0bI3pYRF28tDJoyAVuWTLiWls7diNVCyFz7GeXZEAYYD87lCBE3vnQplLLluNHg=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.80.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g=="], - - "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], @@ -53,6 +49,8 @@ "@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=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], @@ -85,10 +83,60 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], + + "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.28.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw=="], + "@node-llama-cpp/linux-arm64": ["@node-llama-cpp/linux-arm64@3.18.1", "", { "os": "linux", "cpu": [ "x64", "arm64", ] }, "sha512-rXMgZxUay78FOJV/fJ67apYP9eElH5jd4df5YRKPlLhLHHchuOSyDn+qtyW/L/EnPzpogoLkmULqCkdXU39XsQ=="], + + "@node-llama-cpp/linux-armv7l": ["@node-llama-cpp/linux-armv7l@3.18.1", "", { "os": "linux", "cpu": [ "arm", "x64", ] }, "sha512-BrJL2cGo0pN5xd5nw+CzTn2rFMpz9MJyZZPUY81ptGkF2uIuXT2hdCVh56i9ImQrTwBfq1YcZL/l/Qe/1+HR/Q=="], + + "@node-llama-cpp/linux-x64": ["@node-llama-cpp/linux-x64@3.18.1", "", { "os": "linux", "cpu": "x64" }, "sha512-tRmWcsyvAcqJHQHXHsaOkx6muGbcirA9nRdNgH6n7bjGUw4VuoBD3dChyNF3/Ktt7ohB9kz+XhhyZjbDHpXyMA=="], + + "@node-llama-cpp/linux-x64-cuda": ["@node-llama-cpp/linux-x64-cuda@3.18.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qOaYP4uwsUoBHQ/7xSOvyJIuXapS57Al+Sudgi00f96ldNZLKe1vuSGptAi5LTM2lIj66PKm6h8PlRWctwsZ2g=="], + + "@node-llama-cpp/linux-x64-cuda-ext": ["@node-llama-cpp/linux-x64-cuda-ext@3.18.1", "", { "os": "linux", "cpu": "x64" }, "sha512-VqyKhAVHPCpFzh0f1koCBgpThL+04QOXwv0oDQ8s8YcpfMMOXQlBhTB0plgTh0HrPExoObfTS4ohkrbyGgmztQ=="], + + "@node-llama-cpp/linux-x64-vulkan": ["@node-llama-cpp/linux-x64-vulkan@3.18.1", "", { "os": "linux", "cpu": "x64" }, "sha512-SIaNTK5pUPhwJD0gmiQfHa8OrRctVMmnqu+slJrz2Mzgg/XrwFndJlS9hvc+jSjTXCouwf7sYeQaaJWvQgBh/A=="], + + "@node-llama-cpp/mac-arm64-metal": ["@node-llama-cpp/mac-arm64-metal@3.18.1", "", { "os": "darwin", "cpu": [ "x64", "arm64", ] }, "sha512-cyZTdsUMlvuRlGmkkoBbN3v/DT6NuruEqoQYd9CqIrPyLa1xLNBTSKIZ9SgRnw23iCOj4URfITvRP+2pu63LuQ=="], + + "@node-llama-cpp/mac-x64": ["@node-llama-cpp/mac-x64@3.18.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-GfCPgdltaIpBhEnQ7WfsrRXrZO9r9pBtDUAQMXRuJwOPP5q7xKrQZUXI6J6mpc8tAG0//CTIuGn4hTKoD/8V8w=="], + + "@node-llama-cpp/win-arm64": ["@node-llama-cpp/win-arm64@3.18.1", "", { "os": "win32", "cpu": [ "x64", "arm64", ] }, "sha512-S05YUzBMVSRS5KNbOS26cDYugeQHqogI3uewtTUBVC0tPbTHRSKjsdicmgWru1eNAry399LWWhzOf/3St/qsAw=="], + + "@node-llama-cpp/win-x64": ["@node-llama-cpp/win-x64@3.18.1", "", { "os": "win32", "cpu": "x64" }, "sha512-QLDVphPl+YDI+x/VYYgIV1N9g0GMXk3PqcoopOUG3cBRUtce7FO+YX903YdRJezs4oKbIp8YaO+xYBgeUSqhpA=="], + + "@node-llama-cpp/win-x64-cuda": ["@node-llama-cpp/win-x64-cuda@3.18.1", "", { "os": "win32", "cpu": "x64" }, "sha512-drgJmBhnxGQtB/SLo4sf4PPSuxRv3MdNP0FF6rKPY9TtzEOV293bRQyYEu/JYwvXfVApAIsRaJUTGvCkA9Qobw=="], + + "@node-llama-cpp/win-x64-cuda-ext": ["@node-llama-cpp/win-x64-cuda-ext@3.18.1", "", { "os": "win32", "cpu": "x64" }, "sha512-u0FzJBQsJA355ksKERxwPJhlcWl3ZJSNkU2ZUwDEiKNOCbv3ybvSCIEyDvB63wdtkfVUuCRJWijZnpDZxrCGqg=="], + + "@node-llama-cpp/win-x64-vulkan": ["@node-llama-cpp/win-x64-vulkan@3.18.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PjmxrnPToi7y0zlP7l+hRIhvOmuEv94P6xZ11vjqICEJu8XdAJpvTfPKgDW4W0p0v4+So8ZiZYLUuwIHcsseyQ=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@reflink/reflink": ["@reflink/reflink@0.1.19", "", { "optionalDependencies": { "@reflink/reflink-darwin-arm64": "0.1.19", "@reflink/reflink-darwin-x64": "0.1.19", "@reflink/reflink-linux-arm64-gnu": "0.1.19", "@reflink/reflink-linux-arm64-musl": "0.1.19", "@reflink/reflink-linux-x64-gnu": "0.1.19", "@reflink/reflink-linux-x64-musl": "0.1.19", "@reflink/reflink-win32-arm64-msvc": "0.1.19", "@reflink/reflink-win32-x64-msvc": "0.1.19" } }, "sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA=="], + + "@reflink/reflink-darwin-arm64": ["@reflink/reflink-darwin-arm64@0.1.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA=="], + + "@reflink/reflink-darwin-x64": ["@reflink/reflink-darwin-x64@0.1.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA=="], + + "@reflink/reflink-linux-arm64-gnu": ["@reflink/reflink-linux-arm64-gnu@0.1.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg=="], + + "@reflink/reflink-linux-arm64-musl": ["@reflink/reflink-linux-arm64-musl@0.1.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA=="], + + "@reflink/reflink-linux-x64-gnu": ["@reflink/reflink-linux-x64-gnu@0.1.19", "", { "os": "linux", "cpu": "x64" }, "sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw=="], + + "@reflink/reflink-linux-x64-musl": ["@reflink/reflink-linux-x64-musl@0.1.19", "", { "os": "linux", "cpu": "x64" }, "sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ=="], + + "@reflink/reflink-win32-arm64-msvc": ["@reflink/reflink-win32-arm64-msvc@0.1.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ=="], + + "@reflink/reflink-win32-x64-msvc": ["@reflink/reflink-win32-x64-msvc@0.1.19", "", { "os": "win32", "cpu": "x64" }, "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w=="], + "@slack/bolt": ["@slack/bolt@4.6.0", "", { "dependencies": { "@slack/logger": "^4.0.0", "@slack/oauth": "^3.0.4", "@slack/socket-mode": "^2.0.5", "@slack/types": "^2.18.0", "@slack/web-api": "^7.12.0", "axios": "^1.12.0", "express": "^5.0.0", "path-to-regexp": "^8.1.0", "raw-body": "^3", "tsscmp": "^1.0.6" }, "peerDependencies": { "@types/express": "^5.0.0" } }, "sha512-xPgfUs2+OXSugz54Ky07pA890+Qydk22SYToi8uGpXeHSt1JWwFJkRyd/9Vlg5I1AdfdpGXExDpwnbuN9Q/2dQ=="], "@slack/logger": ["@slack/logger@4.0.1", "", { "dependencies": { "@types/node": ">=18" } }, "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ=="], @@ -105,6 +153,8 @@ "@telegraf/types": ["@telegraf/types@7.1.0", "", {}, "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw=="], + "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.4", "", {}, "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], @@ -147,6 +197,16 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "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=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], @@ -171,8 +231,32 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chmodrp": ["chmodrp@1.0.2", "", {}, "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "clawmem": ["clawmem@file:../ClawMem", { "dependencies": { "@modelcontextprotocol/sdk": "^1.25.1", "gray-matter": "^4.0.3", "node-llama-cpp": "^3.14.5", "sqlite-vec": "^0.1.7-alpha.2", "yaml": "^2.8.2", "zod": "^4.2.1" }, "devDependencies": { "@types/bun": "latest" }, "optionalDependencies": { "sqlite-vec-darwin-arm64": "^0.1.7-alpha.2", "sqlite-vec-darwin-x64": "^0.1.7-alpha.2", "sqlite-vec-linux-x64": "^0.1.7-alpha.2", "sqlite-vec-win32-x64": "^0.1.7-alpha.2" }, "peerDependencies": { "typescript": "^5.9.3" }, "bin": { "clawmem": "./bin/clawmem" } }], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "cmake-js": ["cmake-js@8.0.0", "", { "dependencies": { "debug": "^4.4.3", "fs-extra": "^11.3.3", "node-api-headers": "^1.8.0", "rc": "1.2.8", "semver": "^7.7.3", "tar": "^7.5.6", "url-join": "^4.0.1", "which": "^6.0.0", "yargs": "^17.7.2" }, "bin": { "cmake-js": "bin/cmake-js" } }, "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "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=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -189,6 +273,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "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=="], @@ -199,10 +285,14 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "encoding-japanese": ["encoding-japanese@2.2.0", "", {}, "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A=="], + "env-var": ["env-var@7.5.0", "", {}, "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -211,8 +301,12 @@ "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=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], @@ -227,12 +321,18 @@ "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=="], + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "filename-reserved-regex": ["filename-reserved-regex@3.0.0", "", {}, "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw=="], + + "filenamify": ["filenamify@6.0.0", "", { "dependencies": { "filename-reserved-regex": "^3.0.0" } }, "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ=="], + "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=="], "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], @@ -243,14 +343,24 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + "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=="], @@ -263,42 +373,64 @@ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "imapflow": ["imapflow@1.2.18", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "iconv-lite": "0.7.2", "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1", "nodemailer": "8.0.4", "pino": "10.3.1", "socks": "2.8.7" } }, "sha512-zxYvcG9ckj/UcTRs+ZDT+wJzW8DqkjgWZwc1z4Q28R/4C/1YvJieVETOuR/9ztCXcycURC50PJShMimITvz5wQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "ipull": ["ipull@3.9.5", "", { "dependencies": { "@tinyhttp/content-disposition": "^2.2.0", "async-retry": "^1.3.3", "chalk": "^5.3.0", "ci-info": "^4.0.0", "cli-spinners": "^2.9.2", "commander": "^10.0.0", "eventemitter3": "^5.0.1", "filenamify": "^6.0.0", "fs-extra": "^11.1.1", "is-unicode-supported": "^2.0.0", "lifecycle-utils": "^2.0.1", "lodash.debounce": "^4.0.8", "lowdb": "^7.0.1", "pretty-bytes": "^6.1.0", "pretty-ms": "^8.0.0", "sleep-promise": "^9.1.0", "slice-ansi": "^7.1.0", "stdout-update": "^4.0.1", "strip-ansi": "^7.1.0" }, "optionalDependencies": { "@reflink/reflink": "^0.1.16" }, "bin": { "ipull": "dist/cli/cli.js" } }, "sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA=="], + "is-electron": ["is-electron@2.2.2", "", {}, "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg=="], + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], - "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + "libbase64": ["libbase64@1.3.0", "", {}, "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg=="], "libmime": ["libmime@5.3.7", "", { "dependencies": { "encoding-japanese": "2.2.0", "iconv-lite": "0.6.3", "libbase64": "1.3.0", "libqp": "2.1.1" } }, "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw=="], "libqp": ["libqp@2.1.1", "", {}, "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow=="], + "lifecycle-utils": ["lifecycle-utils@3.1.1", "", {}, "sha512-gNd3OvhFNjHykJE3uGntz7UuPzWlK9phrIdXxU9Adis0+ExkwnZibfxCJWiWWZ+a6VbKiZrb+9D9hCQWd4vjTg=="], + + "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], @@ -313,6 +445,10 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "lowdb": ["lowdb@7.0.1", "", { "dependencies": { "steno": "^4.0.2" } }, "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], @@ -323,14 +459,30 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "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-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-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=="], + "nodemailer": ["nodemailer@8.0.4", "", {}, "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -343,6 +495,10 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "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=="], + "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], @@ -351,6 +507,8 @@ "p-timeout": ["p-timeout@4.1.0", "", {}, "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -367,8 +525,14 @@ "postal-mime": ["postal-mime@2.7.3", "", {}, "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw=="], + "pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="], + + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "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=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], @@ -381,12 +545,18 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resend": ["resend@6.9.4", "", { "dependencies": { "postal-mime": "2.7.3", "svix": "1.86.0" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-/M3dsJzu5OgozqVsA4Psd/1L7EdePgOIIxClas453GOQYFG3VHc2ZyCHZFlvqsc9aZCCd2BJRRqZgWC8D9c7/g=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], "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=="], @@ -401,6 +571,8 @@ "sandwich-stream": ["sandwich-stream@2.0.2", "", {}, "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ=="], + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "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=="], @@ -421,6 +593,14 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-git": ["simple-git@3.33.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng=="], + + "sleep-promise": ["sleep-promise@9.1.0", "", {}, "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA=="], + + "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], @@ -429,12 +609,42 @@ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "sqlite-vec": ["sqlite-vec@0.1.9", "", { "optionalDependencies": { "sqlite-vec-darwin-arm64": "0.1.9", "sqlite-vec-darwin-x64": "0.1.9", "sqlite-vec-linux-arm64": "0.1.9", "sqlite-vec-linux-x64": "0.1.9", "sqlite-vec-windows-x64": "0.1.9" } }, "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA=="], + + "sqlite-vec-darwin-arm64": ["sqlite-vec-darwin-arm64@0.1.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg=="], + + "sqlite-vec-darwin-x64": ["sqlite-vec-darwin-x64@0.1.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA=="], + + "sqlite-vec-linux-arm64": ["sqlite-vec-linux-arm64@0.1.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw=="], + + "sqlite-vec-linux-x64": ["sqlite-vec-linux-x64@0.1.9", "", { "os": "linux", "cpu": "x64" }, "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA=="], + + "sqlite-vec-windows-x64": ["sqlite-vec-windows-x64@0.1.9", "", { "os": "win32", "cpu": "x64" }, "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q=="], + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "stdin-discarder": ["stdin-discarder@0.3.1", "", {}, "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA=="], + + "stdout-update": ["stdout-update@4.0.1", "", { "dependencies": { "ansi-escapes": "^6.2.0", "ansi-styles": "^6.2.1", "string-width": "^7.1.0", "strip-ansi": "^7.1.0" } }, "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ=="], + + "steno": ["steno@4.0.2", "", {}, "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A=="], + + "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "svix": ["svix@1.86.0", "", { "dependencies": { "standardwebhooks": "1.0.0", "uuid": "^10.0.0" } }, "sha512-/HTvXwjLJe1l/MsLXAO1ddCYxElJk4eNR4DzOjDOEmGrPN/3BtBE8perGwMAaJ2sT5T172VkBYzmHcjUfM1JRQ=="], + "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=="], + "telegraf": ["telegraf@4.16.3", "", { "dependencies": { "@telegraf/types": "^7.1.0", "abort-controller": "^3.0.0", "debug": "^4.3.4", "mri": "^1.2.0", "node-fetch": "^2.7.0", "p-timeout": "^4.1.0", "safe-compare": "^1.1.4", "sandwich-stream": "^2.0.2" }, "bin": { "telegraf": "lib/cli.mjs" } }, "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w=="], "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], @@ -443,8 +653,6 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], - "tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], @@ -453,10 +661,16 @@ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -465,24 +679,94 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "clawmem/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "cliui/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=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cmake-js/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + "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=="], + + "ipull/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="], + + "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=="], + "libmime/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "node-llama-cpp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + + "ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + "p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], "p-queue/p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], + "proper-lockfile/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "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=="], + + "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=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "yargs/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=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cmake-js/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + "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=="], + + "node-llama-cpp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/config/memory.yaml b/config/memory.yaml index d7a6d91..8a13c11 100644 --- a/config/memory.yaml +++ b/config/memory.yaml @@ -1,21 +1,16 @@ -# Environment variables QDRANT_URL and OLLAMA_URL override these values. -# In Docker, compose sets QDRANT_URL=http://qdrant:6333 and OLLAMA_URL=http://ollama:11434. -qdrant: - url: "http://localhost:6333" - -ollama: - url: "http://localhost:11434" - model: "nomic-embed-text" +# CLAWMEM_STORE_PATH and CLAWMEM_EMBED_MODEL override these values. +# Optional remote embeddings are configured via CLAWMEM_EMBED_URL and +# CLAWMEM_EMBED_API_KEY; ClawMem reads those directly from the environment. +clawmem: + store_path: "data/clawmem.sqlite" + embed_model: "embedding" + busy_timeout_ms: 5000 collections: episodes: "episodes" semantic_facts: "semantic_facts" procedures: "procedures" -embedding: - dimensions: 768 - batch_size: 32 - context: max_tokens: 50000 episode_limit: 10 diff --git a/docs/deploy-checklist.md b/docs/deploy-checklist.md index 780bb71..ac4be3e 100644 --- a/docs/deploy-checklist.md +++ b/docs/deploy-checklist.md @@ -103,7 +103,6 @@ Format: `U` followed by alphanumeric characters (e.g., `UKWMQ41F0`) Create a file at `.env.` in the Phantom repo root: ``` -ANTHROPIC_API_KEY=sk-ant-your-api-key SLACK_BOT_TOKEN=xoxb-their-bot-token SLACK_APP_TOKEN=xapp-their-app-token OWNER_SLACK_USER_ID=UTHEIR_USER_ID diff --git a/docs/docker-deploy.md b/docs/docker-deploy.md index b44bb36..baa9e70 100644 --- a/docs/docker-deploy.md +++ b/docs/docker-deploy.md @@ -44,7 +44,6 @@ What it does (5 steps, all idempotent): Create `.env.` in the Phantom repo root with: ``` -ANTHROPIC_API_KEY=sk-ant-... SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... OWNER_SLACK_USER_ID=U04ABC123 diff --git a/docs/getting-started.md b/docs/getting-started.md index cbd59de..4ab2ec6 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -68,13 +68,9 @@ curl -fsSL https://raw.githubusercontent.com/ghostwright/phantom/main/.env.examp Open `.env` in your editor and fill in these values: -### Required +### Authentication -``` -ANTHROPIC_API_KEY=sk-ant-your-key-here -``` - -Your Anthropic API key. This is the only value you absolutely must set. +Auth is handled automatically by the Claude Agent SDK (OAuth / Claude MAX). No API key is required. ### Slack (recommended) @@ -105,13 +101,12 @@ Everything else in `.env.example` has sensible defaults. You can leave the rest If you just want the shortest path to a running Phantom with Slack: ``` -ANTHROPIC_API_KEY=sk-ant-your-key-here SLACK_BOT_TOKEN=xoxb-your-bot-token SLACK_APP_TOKEN=xapp-your-app-token OWNER_SLACK_USER_ID=U04ABC123XY ``` -Four lines. That is all Phantom needs. +Three lines. That is all Phantom needs. Auth is handled by the Agent SDK. ## Step 3: Start Phantom @@ -209,7 +204,6 @@ Create your `.env` file: ```bash cat > .env << 'EOF' -ANTHROPIC_API_KEY=sk-ant-your-key-here SLACK_BOT_TOKEN=xoxb-your-bot-token SLACK_APP_TOKEN=xapp-your-app-token OWNER_SLACK_USER_ID=U04ABC123XY diff --git a/docs/security.md b/docs/security.md index 8eda25b..7fd4465 100644 --- a/docs/security.md +++ b/docs/security.md @@ -42,7 +42,7 @@ These are configured automatically by the [app manifest](../slack-app-manifest.y | Variable | Required | Purpose | |----------|----------|---------| -| `ANTHROPIC_API_KEY` | Yes | Claude Opus 4.6 API access | +| ~~`ANTHROPIC_API_KEY`~~ | No | Removed — auth handled by Claude Agent SDK (OAuth) | | `SLACK_BOT_TOKEN` | For Slack | Bot user OAuth token (`xoxb-`) | | `SLACK_APP_TOKEN` | For Slack | App-level token for Socket Mode (`xapp-`) | | `SLACK_CHANNEL_ID` | For Slack | Default channel for intro message on first start | diff --git a/docs/self-evolution.md b/docs/self-evolution.md index 9aa2f86..6323b05 100644 --- a/docs/self-evolution.md +++ b/docs/self-evolution.md @@ -100,7 +100,7 @@ If metrics degrade after an evolution (success rate drops, correction rate incre ## LLM Judges -When the `ANTHROPIC_API_KEY` is available, 6 LLM judges provide higher-quality evolution: +The Agent SDK powers 6 LLM judges that provide higher-quality evolution: | Judge | Model | Strategy | Purpose | |-------|-------|----------|---------| diff --git a/package.json b/package.json index e5dbcd4..54f4708 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,9 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.77", - "@anthropic-ai/sdk": "^0.80.0", "@modelcontextprotocol/sdk": "^1.28.0", "@slack/bolt": "^4.6.0", + "clawmem": "file:../ClawMem", "croner": "^10.0.1", "imapflow": "^1.2.18", "nodemailer": "^8.0.4", diff --git a/src/agent/__tests__/prompt-assembler.test.ts b/src/agent/__tests__/prompt-assembler.test.ts index 8c9a180..d92cdd7 100644 --- a/src/agent/__tests__/prompt-assembler.test.ts +++ b/src/agent/__tests__/prompt-assembler.test.ts @@ -46,8 +46,8 @@ describe("assemblePrompt Docker awareness", () => { expect(prompt).toContain("sibling"); expect(prompt).toContain("ClickHouse, Postgres, Redis"); expect(prompt).toContain("Docker volumes"); - expect(prompt).toContain("http://qdrant:6333"); - expect(prompt).toContain("http://ollama:11434"); + expect(prompt).toContain("ClawMem"); + expect(prompt).toContain("CLAWMEM_EMBED_URL"); }); test("Docker mode warns agent not to modify compose/Dockerfile", () => { diff --git a/src/agent/prompt-assembler.ts b/src/agent/prompt-assembler.ts index 09197e1..00f615c 100644 --- a/src/agent/prompt-assembler.ts +++ b/src/agent/prompt-assembler.ts @@ -218,7 +218,8 @@ function buildEnvironment(config: PhantomConfig): string { lines.push("- Your data (config, memory, web pages, repos) persists in Docker volumes."); lines.push("- To connect to services you create, use their container name as the hostname."); lines.push("- Do NOT modify docker-compose.yaml or Dockerfile. Those are managed by the operator."); - lines.push("- Qdrant is at http://qdrant:6333, Ollama is at http://ollama:11434."); + lines.push("- Persistent memory runs on ClawMem with a local SQLite store in the mounted data volume."); + lines.push("- If remote embeddings are configured, they are provided through CLAWMEM_EMBED_URL."); } return lines.join("\n"); diff --git a/src/cli/__tests__/doctor.test.ts b/src/cli/__tests__/doctor.test.ts index 106f7f7..794d2dd 100644 --- a/src/cli/__tests__/doctor.test.ts +++ b/src/cli/__tests__/doctor.test.ts @@ -30,6 +30,7 @@ describe("phantom doctor", () => { expect(logs.some((l) => l.includes("Phantom Doctor"))).toBe(true); expect(logs.some((l) => l.includes("Bun"))).toBe(true); expect(logs.some((l) => l.includes("Docker"))).toBe(true); + expect(logs.some((l) => l.includes("ClawMem"))).toBe(true); expect(logs.some((l) => l.includes("Config"))).toBe(true); expect(logs.some((l) => l.includes("SQLite"))).toBe(true); }); diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 98bb6db..cac42d0 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -1,5 +1,6 @@ import { existsSync, readFileSync } from "node:fs"; import { parseArgs } from "node:util"; +import { createClawMemStore } from "../memory/clawmem-runtime.ts"; type CheckResult = { name: string; @@ -39,47 +40,29 @@ async function checkDocker(): Promise { } } -async function checkQdrant(): Promise { +async function checkClawMem(): Promise { try { - const resp = await fetch("http://localhost:6333/healthz", { signal: AbortSignal.timeout(3000) }); - if (resp.ok) { - return { name: "Qdrant", status: "ok", message: "Healthy (port 6333)" }; - } - return { name: "Qdrant", status: "fail", message: `HTTP ${resp.status}`, fix: "docker compose up -d qdrant" }; - } catch { + const [{ loadMemoryConfig }] = await Promise.all([import("../memory/config.ts")]); + const config = loadMemoryConfig(); + const store = await createClawMemStore(config.clawmem.store_path, { + busyTimeout: config.clawmem.busy_timeout_ms, + }); + const status = store.getStatus(); + store.close(); + + const vectorState = status.hasVectorIndex ? "vectors ready" : "FTS-only until first embeddings"; return { - name: "Qdrant", - status: "fail", - message: "Not reachable at localhost:6333", - fix: "docker compose up -d qdrant", + name: "ClawMem", + status: "ok", + message: `${config.clawmem.store_path} (${status.totalDocuments} docs, ${vectorState})`, }; - } -} - -async function checkOllama(): Promise { - try { - const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) }); - if (!resp.ok) { - return { name: "Ollama", status: "fail", message: `HTTP ${resp.status}`, fix: "docker compose up -d ollama" }; - } - const data = (await resp.json()) as { models?: Array<{ name: string }> }; - const models = data.models ?? []; - const hasEmbed = models.some((m) => m.name.includes("nomic-embed-text")); - if (!hasEmbed) { - return { - name: "Ollama", - status: "warn", - message: "Running but nomic-embed-text model not pulled", - fix: "docker exec phantom-ollama ollama pull nomic-embed-text", - }; - } - return { name: "Ollama", status: "ok", message: `Healthy, ${models.length} model(s) loaded` }; - } catch { + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); return { - name: "Ollama", + name: "ClawMem", status: "fail", - message: "Not reachable at localhost:11434", - fix: "docker compose up -d ollama", + message: msg, + fix: "Check config/memory.yaml and ensure the local ClawMem dependency is installed", }; } } @@ -188,8 +171,7 @@ export async function runDoctor(args: string[]): Promise { const checks = await Promise.all([ checkBun(), checkDocker(), - checkQdrant(), - checkOllama(), + checkClawMem(), checkConfig(), checkMcpConfig(), checkDatabase(), diff --git a/src/cli/init.ts b/src/cli/init.ts index db315a1..ec9a34d 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -302,9 +302,8 @@ export async function runInit(args: string[]): Promise { } console.log("\nNext steps:"); - console.log(" 1. Set ANTHROPIC_API_KEY in your environment"); - console.log(" 2. Start Docker services: docker compose up -d"); - console.log(" 3. Start Phantom: phantom start"); + console.log(" 1. Start Docker services: docker compose up -d"); + console.log(" 2. Start Phantom: phantom start"); console.log(" 4. Connect from Claude Code:"); console.log(` claude mcp add phantom -- curl -H "Authorization: Bearer ${mcp.adminToken}" https://your-host/mcp`); } diff --git a/src/cli/status.ts b/src/cli/status.ts index 7342d2f..23fbed5 100644 --- a/src/cli/status.ts +++ b/src/cli/status.ts @@ -7,7 +7,7 @@ type HealthResponse = { agent: string; role: { id: string; name: string }; channels: Record; - memory: { qdrant: boolean; ollama: boolean }; + memory: { clawmem: boolean; configured: boolean }; evolution: { generation: number }; onboarding?: string; peers?: Record; @@ -71,8 +71,7 @@ export async function runStatus(args: string[]): Promise { .map(([name]) => name); const channelStr = channelList.length > 0 ? channelList.join(", ") : "none"; - const memoryStr = - data.memory.qdrant && data.memory.ollama ? "ok" : data.memory.qdrant || data.memory.ollama ? "degraded" : "offline"; + const memoryStr = data.memory.clawmem ? "ok" : data.memory.configured ? "offline" : "disabled"; console.log( `${data.agent} | ${data.role.name} | v${data.version} | ` + diff --git a/src/config/schemas.ts b/src/config/schemas.ts index dbce22c..ef07d4f 100644 --- a/src/config/schemas.ts +++ b/src/config/schemas.ts @@ -70,15 +70,11 @@ export const ChannelsConfigSchema = z.object({ export type ChannelsConfig = z.infer; export const MemoryConfigSchema = z.object({ - qdrant: z + clawmem: z .object({ - url: z.string().url().default("http://localhost:6333"), - }) - .default({}), - ollama: z - .object({ - url: z.string().url().default("http://localhost:11434"), - model: z.string().min(1).default("nomic-embed-text"), + store_path: z.string().min(1).default("data/clawmem.sqlite"), + embed_model: z.string().min(1).default("embedding"), + busy_timeout_ms: z.number().int().nonnegative().default(5000), }) .default({}), collections: z @@ -88,12 +84,6 @@ export const MemoryConfigSchema = z.object({ procedures: z.string().min(1).default("procedures"), }) .default({}), - embedding: z - .object({ - dimensions: z.number().int().positive().default(768), - batch_size: z.number().int().positive().default(32), - }) - .default({}), context: z .object({ max_tokens: z.number().int().positive().default(50000), diff --git a/src/core/__tests__/health-status.test.ts b/src/core/__tests__/health-status.test.ts index bb2bc92..0f86188 100644 --- a/src/core/__tests__/health-status.test.ts +++ b/src/core/__tests__/health-status.test.ts @@ -6,29 +6,19 @@ import type { MemoryHealth } from "../../memory/types.ts"; * This must mirror the logic in startServer's /health handler exactly. */ function computeHealthStatus(memory: MemoryHealth): string { - const allHealthy = memory.qdrant && memory.ollama; - const someHealthy = memory.qdrant || memory.ollama; - return allHealthy ? "ok" : someHealthy ? "degraded" : memory.configured ? "down" : "ok"; + return memory.clawmem ? "ok" : memory.configured ? "down" : "ok"; } describe("health status logic", () => { - test("both healthy and configured -> ok", () => { - expect(computeHealthStatus({ qdrant: true, ollama: true, configured: true })).toBe("ok"); + test("clawmem healthy and configured -> ok", () => { + expect(computeHealthStatus({ clawmem: true, configured: true })).toBe("ok"); }); - test("qdrant up, ollama down, configured -> degraded", () => { - expect(computeHealthStatus({ qdrant: true, ollama: false, configured: true })).toBe("degraded"); + test("clawmem down when configured -> down", () => { + expect(computeHealthStatus({ clawmem: false, configured: true })).toBe("down"); }); - test("qdrant down, ollama up, configured -> degraded", () => { - expect(computeHealthStatus({ qdrant: false, ollama: true, configured: true })).toBe("degraded"); - }); - - test("both down when configured -> down (the bug fix)", () => { - expect(computeHealthStatus({ qdrant: false, ollama: false, configured: true })).toBe("down"); - }); - - test("both down when not configured -> ok (memory intentionally not set up)", () => { - expect(computeHealthStatus({ qdrant: false, ollama: false, configured: false })).toBe("ok"); + test("clawmem down when not configured -> ok", () => { + expect(computeHealthStatus({ clawmem: false, configured: false })).toBe("ok"); }); }); diff --git a/src/core/server.ts b/src/core/server.ts index ebab6fb..060b3fd 100644 --- a/src/core/server.ts +++ b/src/core/server.ts @@ -83,14 +83,11 @@ export function startServer(config: PhantomConfig, startedAt: number): ReturnTyp if (url.pathname === "/health") { const memory: MemoryHealth = memoryHealthProvider ? await memoryHealthProvider() - : { qdrant: false, ollama: false, configured: false }; + : { clawmem: false, configured: false }; const channels: Record = channelHealthProvider ? channelHealthProvider() : {}; - const allHealthy = memory.qdrant && memory.ollama; - const someHealthy = memory.qdrant || memory.ollama; - // Both up -> ok. One up -> degraded. Both down + configured -> down. Not configured -> ok. - const status = allHealthy ? "ok" : someHealthy ? "degraded" : memory.configured ? "down" : "ok"; + const status = memory.clawmem ? "ok" : memory.configured ? "down" : "ok"; const evolutionGeneration = evolutionVersionProvider ? evolutionVersionProvider() : 0; const roleInfo = roleInfoProvider ? roleInfoProvider() : null; diff --git a/src/evolution/__tests__/cost-cap.test.ts b/src/evolution/__tests__/cost-cap.test.ts index 24135db..47bafd8 100644 --- a/src/evolution/__tests__/cost-cap.test.ts +++ b/src/evolution/__tests__/cost-cap.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { EvolutionEngine } from "../engine.ts"; import type { SessionSummary } from "../types.ts"; @@ -6,8 +6,6 @@ import type { SessionSummary } from "../types.ts"; const TEST_DIR = "/tmp/phantom-test-cost-cap"; const CONFIG_PATH = `${TEST_DIR}/config/evolution.yaml`; -let savedApiKey: string | undefined; - function setupTestEnv(costCap: number): void { mkdirSync(`${TEST_DIR}/config`, { recursive: true }); mkdirSync(`${TEST_DIR}/phantom-config/meta`, { recursive: true }); @@ -99,16 +97,7 @@ function makeSession(overrides: Partial = {}): SessionSummary { } describe("Cost Cap", () => { - beforeEach(() => { - savedApiKey = process.env.ANTHROPIC_API_KEY; - }); - afterEach(() => { - if (savedApiKey !== undefined) { - process.env.ANTHROPIC_API_KEY = savedApiKey; - } else { - process.env.ANTHROPIC_API_KEY = undefined; - } rmSync(TEST_DIR, { recursive: true, force: true }); }); diff --git a/src/evolution/__tests__/judge-activation.test.ts b/src/evolution/__tests__/judge-activation.test.ts index 59935ce..617c2bc 100644 --- a/src/evolution/__tests__/judge-activation.test.ts +++ b/src/evolution/__tests__/judge-activation.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { EvolutionEngine } from "../engine.ts"; @@ -78,64 +78,30 @@ function setupWithJudgeMode(enabled: "auto" | "always" | "never"): void { writeFileSync(`${TEST_DIR}/phantom-config/meta/golden-suite.jsonl`, "", "utf-8"); } -let savedApiKey: string | undefined; - describe("Judge Activation", () => { - beforeEach(() => { - savedApiKey = process.env.ANTHROPIC_API_KEY; - }); - afterEach(() => { - if (savedApiKey !== undefined) { - process.env.ANTHROPIC_API_KEY = savedApiKey; - } else { - process.env.ANTHROPIC_API_KEY = undefined; - } rmSync(TEST_DIR, { recursive: true, force: true }); }); - test("auto mode enables judges when ANTHROPIC_API_KEY is set", () => { - process.env.ANTHROPIC_API_KEY = "sk-test-key"; + test("auto mode enables judges (Agent SDK always available)", () => { setupWithJudgeMode("auto"); const engine = new EvolutionEngine(CONFIG_PATH); expect(engine.usesLLMJudges()).toBe(true); }); - test("auto mode disables judges when ANTHROPIC_API_KEY is missing", () => { - process.env.ANTHROPIC_API_KEY = undefined; - setupWithJudgeMode("auto"); - const engine = new EvolutionEngine(CONFIG_PATH); - expect(engine.usesLLMJudges()).toBe(false); - }); - - test("never mode disables judges even when API key is set", () => { - process.env.ANTHROPIC_API_KEY = "sk-test-key"; + test("never mode disables judges", () => { setupWithJudgeMode("never"); const engine = new EvolutionEngine(CONFIG_PATH); expect(engine.usesLLMJudges()).toBe(false); }); - test("always mode enables judges regardless of API key", () => { - process.env.ANTHROPIC_API_KEY = undefined; + test("always mode enables judges", () => { setupWithJudgeMode("always"); const engine = new EvolutionEngine(CONFIG_PATH); expect(engine.usesLLMJudges()).toBe(true); }); - test("usesLLMJudges accessor matches resolved state", () => { - process.env.ANTHROPIC_API_KEY = "sk-test-key"; - setupWithJudgeMode("auto"); - const engine = new EvolutionEngine(CONFIG_PATH); - expect(engine.usesLLMJudges()).toBe(true); - - process.env.ANTHROPIC_API_KEY = undefined; - setupWithJudgeMode("auto"); - const engine2 = new EvolutionEngine(CONFIG_PATH); - expect(engine2.usesLLMJudges()).toBe(false); - }); - - test("missing judges section defaults to auto mode", () => { - process.env.ANTHROPIC_API_KEY = undefined; + test("missing judges section defaults to auto (enabled)", () => { mkdirSync(`${TEST_DIR}/config`, { recursive: true }); mkdirSync(`${TEST_DIR}/phantom-config/meta`, { recursive: true }); mkdirSync(`${TEST_DIR}/phantom-config/strategies`, { recursive: true }); @@ -147,6 +113,7 @@ describe("Judge Activation", () => { [ "cadence:", " reflection_interval: 1", + " consolidation_interval: 10", "paths:", ` config_dir: "${TEST_DIR}/phantom-config"`, ` constitution: "${TEST_DIR}/phantom-config/constitution.md"`, @@ -198,8 +165,8 @@ describe("Judge Activation", () => { writeFileSync(`${TEST_DIR}/phantom-config/meta/evolution-log.jsonl`, "", "utf-8"); writeFileSync(`${TEST_DIR}/phantom-config/meta/golden-suite.jsonl`, "", "utf-8"); - // No API key + auto = disabled + // Auto mode + Agent SDK = always enabled const engine = new EvolutionEngine(CONFIG_PATH); - expect(engine.usesLLMJudges()).toBe(false); + expect(engine.usesLLMJudges()).toBe(true); }); }); diff --git a/src/evolution/engine.ts b/src/evolution/engine.ts index c32b4df..486bd9c 100644 --- a/src/evolution/engine.ts +++ b/src/evolution/engine.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import { applyApproved } from "./application.ts"; import { type EvolutionConfig, loadEvolutionConfig } from "./config.ts"; import { recordObservations, runConsolidation } from "./consolidation.ts"; +import { JudgeError } from "./judges/client.ts"; import { ConstitutionChecker } from "./constitution.ts"; import { addCase, loadSuite, pruneSuite } from "./golden-suite.ts"; import { runQualityJudge } from "./judges/quality-judge.ts"; @@ -38,17 +39,17 @@ export class EvolutionEngine { this.checker = new ConstitutionChecker(this.config); this.llmJudgesEnabled = this.resolveJudgeMode(); if (this.llmJudgesEnabled) { - console.log("[evolution] LLM judges enabled (API key detected)"); + console.log("[evolution] LLM judges enabled (Agent SDK)"); } else { - console.log("[evolution] LLM judges disabled (no API key or config override)"); + console.log("[evolution] LLM judges disabled (config override)"); } } private resolveJudgeMode(): boolean { const setting = this.config.judges?.enabled ?? "auto"; if (setting === "never") return false; - if (setting === "always") return true; - return !!process.env.ANTHROPIC_API_KEY; + // Agent SDK uses Claude MAX OAuth — no API key needed, judges always available + return true; } usesLLMJudges(): boolean { @@ -181,6 +182,14 @@ export class EvolutionEngine { } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); console.warn(`[evolution] Quality judge failed (non-blocking): ${msg}`); + // Record cost even on failure so daily cap tracks actual spend + if (error instanceof JudgeError) { + judgeCosts.quality_assessment.calls++; + judgeCosts.quality_assessment.totalUsd += error.costUsd; + judgeCosts.quality_assessment.totalInputTokens += error.inputTokens; + judgeCosts.quality_assessment.totalOutputTokens += error.outputTokens; + this.incrementDailyCost(error.costUsd); + } } } diff --git a/src/evolution/judges/client.ts b/src/evolution/judges/client.ts index 6254e99..ee0441e 100644 --- a/src/evolution/judges/client.ts +++ b/src/evolution/judges/client.ts @@ -1,37 +1,45 @@ -import Anthropic from "@anthropic-ai/sdk"; -import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; -// zod/v4 required: matches schemas.ts for zodOutputFormat compatibility +import { query } from "@anthropic-ai/claude-agent-sdk"; +import { zodToJsonSchema } from "zod-to-json-schema"; +// zod/v4 required: matches schemas.ts for judge output compatibility import type { z } from "zod/v4"; -import { - JUDGE_MAX_TOKENS, - JUDGE_TEMPERATURE, - type JudgeResult, - type MultiJudgeResult, - type VotingStrategy, +import type { + JudgeResult, + MultiJudgeResult, + VotingStrategy, } from "./types.ts"; +// JUDGE_MAX_TOKENS not used — SDK manages token limits internally -let _client: Anthropic | null = null; - -function getClient(): Anthropic { - if (!_client) { - _client = new Anthropic(); +/** + * Error thrown when a judge call fails but still incurred cost. + * Callers should check for this to record spend toward the daily cap. + */ +export class JudgeError extends Error { + costUsd: number; + inputTokens: number; + outputTokens: number; + + constructor(message: string, cost: { costUsd: number; inputTokens: number; outputTokens: number }) { + super(message); + this.name = "JudgeError"; + this.costUsd = cost.costUsd; + this.inputTokens = cost.inputTokens; + this.outputTokens = cost.outputTokens; } - return _client; -} - -// Visible for testing - allows injecting a mock client -export function setClient(client: Anthropic | null): void { - _client = client; } +/** + * Judge availability check. + * With the Agent SDK (Claude MAX OAuth), judges are always available — + * no API key required. + */ export function isJudgeAvailable(): boolean { - return !!process.env.ANTHROPIC_API_KEY; + return true; } /** - * Call a single LLM judge with structured output. - * Uses the raw Anthropic SDK (not the Agent SDK). - * Temperature 0 for deterministic judging. + * Call a single LLM judge with structured output via the Agent SDK. + * Uses the SDK's native outputFormat for schema-constrained generation, + * with tools disabled to prevent judges from executing code. */ export async function callJudge(options: { model: string; @@ -41,32 +49,112 @@ export async function callJudge(options: { schemaName?: string; maxTokens?: number; }): Promise> { - const client = getClient(); const startTime = Date.now(); - const message = await client.messages.parse({ - model: options.model, - max_tokens: options.maxTokens ?? JUDGE_MAX_TOKENS, - temperature: JUDGE_TEMPERATURE, - system: options.systemPrompt, - messages: [{ role: "user", content: options.userMessage }], - output_config: { - // Cast needed: SDK .d.ts references zod v3 types but runtime uses zod/v4 - // biome-ignore lint/suspicious/noExplicitAny: bridging zod v3/v4 type mismatch - format: zodOutputFormat(options.schema as any), + // Cast needed: zod-to-json-schema types reference zod v3 but runtime uses zod/v4 + // biome-ignore lint/suspicious/noExplicitAny: bridging zod v3/v4 type mismatch + const jsonSchema = zodToJsonSchema(options.schema as any) as Record; + + let resultText = ""; + let totalCostUsd = 0; + let inputTokens = 0; + let outputTokens = 0; + let isError = false; + let errorReason = ""; + let structuredOutput: unknown = undefined; + + const queryStream = query({ + prompt: options.userMessage, + options: { + model: options.model, + permissionMode: "bypassPermissions", + allowDangerouslySkipPermissions: true, + systemPrompt: options.systemPrompt, + persistSession: false, + // Disable all tools — judges must only produce text/structured output + tools: [], + // Native structured output via the SDK + outputFormat: { + type: "json_schema" as const, + schema: jsonSchema, + }, + // Limit to 1 turn — judges should produce output in a single pass + maxTurns: 1, }, }); - const parsed = message.parsed_output; - if (!parsed) { - throw new Error(`Judge returned no structured output (stop_reason: ${message.stop_reason})`); + for await (const message of queryStream) { + switch (message.type) { + case "assistant": { + const text = message.message.content + .filter((b: { type: string }) => b.type === "text") + .map((b: { type: string; text?: string }) => b.text ?? "") + .join(""); + if (text) resultText = text; + break; + } + case "result": { + const result = message as unknown as { + subtype: string; + is_error: boolean; + total_cost_usd?: number; + structured_output?: unknown; + errors?: string[]; + modelUsage?: Record< + string, + { + inputTokens: number; + outputTokens: number; + cacheReadInputTokens?: number; + cacheCreationInputTokens?: number; + costUSD: number; + } + >; + }; + + totalCostUsd = result.total_cost_usd ?? 0; + isError = result.is_error; + structuredOutput = result.structured_output; + + if (result.is_error) { + errorReason = result.errors?.join("; ") ?? result.subtype; + } + + if (result.modelUsage) { + for (const usage of Object.values(result.modelUsage)) { + inputTokens += + usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheCreationInputTokens ?? 0); + outputTokens += usage.outputTokens; + } + } + break; + } + } + } + + const costInfo = { costUsd: totalCostUsd, inputTokens, outputTokens }; + + // Check for SDK-level errors before parsing + if (isError) { + throw new JudgeError(`Judge SDK error (${errorReason})`, costInfo); } - const inputTokens = message.usage.input_tokens; - const outputTokens = message.usage.output_tokens; - const costUsd = estimateCost(options.model, inputTokens, outputTokens); + // Prefer SDK structured_output, fall back to parsing assistant text + let parsed: T; + try { + if (structuredOutput != null) { + parsed = options.schema.parse(structuredOutput); + } else { + const cleaned = resultText.replace(/^```(?:json)?\s*\n?|\n?\s*```$/g, "").trim(); + parsed = options.schema.parse(JSON.parse(cleaned)); + } + } catch (parseErr) { + throw new JudgeError( + `Judge output parsing failed: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`, + costInfo, + ); + } - // Extract verdict and confidence from the parsed data if present const data = parsed as Record; const verdict = (data.verdict as "pass" | "fail") ?? "pass"; const confidence = (data.confidence as number) ?? 1.0; @@ -80,7 +168,7 @@ export async function callJudge(options: { model: options.model, inputTokens, outputTokens, - costUsd, + costUsd: totalCostUsd, durationMs: Date.now() - startTime, }; } @@ -160,26 +248,3 @@ export async function multiJudge( } } } - -/** - * Estimate USD cost from token counts. - * Pricing as of March 2026. - */ -function estimateCost(model: string, inputTokens: number, outputTokens: number): number { - let inputPer1M: number; - let outputPer1M: number; - - if (model.includes("opus")) { - inputPer1M = 5.0; - outputPer1M = 25.0; - } else if (model.includes("haiku")) { - inputPer1M = 1.0; - outputPer1M = 5.0; - } else { - // Sonnet default - inputPer1M = 3.0; - outputPer1M = 15.0; - } - - return (inputTokens / 1_000_000) * inputPer1M + (outputTokens / 1_000_000) * outputPer1M; -} diff --git a/src/evolution/reflection.ts b/src/evolution/reflection.ts index bd0d1b8..a23b3c3 100644 --- a/src/evolution/reflection.ts +++ b/src/evolution/reflection.ts @@ -1,5 +1,6 @@ import { matchesCorrectionPattern, matchesDomainFactPattern, matchesPreferencePattern } from "../shared/patterns.ts"; import type { EvolutionConfig } from "./config.ts"; +import { JudgeError } from "./judges/client.ts"; import { extractObservationsWithJudge, toSessionObservations } from "./judges/observation-judge.ts"; import type { JudgeCostEntry } from "./judges/types.ts"; import type { ConfigDelta, CritiqueResult, EvolvedConfig, SessionObservation, SessionSummary } from "./types.ts"; @@ -28,7 +29,12 @@ export async function extractObservationsWithLLM( } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); console.warn(`[evolution] Observation judge failed, falling back to heuristic: ${msg}`); - return { observations: extractObservations(session), judgeCost: null }; + // Record cost even on failure so daily cap tracks actual spend + const judgeCost = + error instanceof JudgeError + ? { calls: 1, totalUsd: error.costUsd, totalInputTokens: error.inputTokens, totalOutputTokens: error.outputTokens } + : null; + return { observations: extractObservations(session), judgeCost }; } } diff --git a/src/mcp/dynamic-handlers.ts b/src/mcp/dynamic-handlers.ts index aaba18b..5a1d626 100644 --- a/src/mcp/dynamic-handlers.ts +++ b/src/mcp/dynamic-handlers.ts @@ -82,19 +82,14 @@ async function executeScriptHandler(path: string, input: Record } async function executeShellHandler(command: string, input: Record): Promise { - const proc = Bun.spawn(["bash", "-c", command], { - stdout: "pipe", - stderr: "pipe", - env: buildSafeEnv(input), - }); + const shell = new Bun.$.Shell().env(buildSafeEnv(input)).nothrow(); + const result = await shell`${{ raw: command }}`; + const stdout = await result.text(); + const stderr = result.stderr.toString(); - const stdout = await new Response(proc.stdout).text(); - const stderr = await new Response(proc.stderr).text(); - await proc.exited; - - if (proc.exitCode !== 0) { + if (result.exitCode !== 0) { return { - content: [{ type: "text", text: `Shell error (exit ${proc.exitCode}): ${stderr || stdout}` }], + content: [{ type: "text", text: `Shell error (exit ${result.exitCode}): ${stderr || stdout}` }], isError: true, }; } diff --git a/src/mcp/resources.ts b/src/mcp/resources.ts index e652704..bf45052 100644 --- a/src/mcp/resources.ts +++ b/src/mcp/resources.ts @@ -36,11 +36,11 @@ function registerHealthResource(server: McpServer, deps: ResourceDependencies): }, async (): Promise => { const memoryHealth = deps.memory - ? await deps.memory.healthCheck().catch(() => ({ qdrant: false, ollama: false })) - : { qdrant: false, ollama: false }; + ? await deps.memory.healthCheck().catch(() => ({ clawmem: false, configured: false })) + : { clawmem: false, configured: false }; const uptimeSeconds = Math.floor((Date.now() - deps.startedAt) / 1000); - const allHealthy = memoryHealth.qdrant && memoryHealth.ollama; + const status = memoryHealth.clawmem ? "ok" : memoryHealth.configured ? "down" : "ok"; return { contents: [ @@ -48,7 +48,7 @@ function registerHealthResource(server: McpServer, deps: ResourceDependencies): uri: "phantom://health", text: JSON.stringify( { - status: allHealthy ? "ok" : "degraded", + status, uptime: uptimeSeconds, version: "0.4.0", agent: deps.config.name, diff --git a/src/memory/__tests__/config.test.ts b/src/memory/__tests__/config.test.ts index 0fff850..e9d6ea3 100644 --- a/src/memory/__tests__/config.test.ts +++ b/src/memory/__tests__/config.test.ts @@ -2,60 +2,59 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { loadMemoryConfig } from "../config.ts"; describe("loadMemoryConfig env overrides", () => { - const origQdrant = process.env.QDRANT_URL; - const origOllama = process.env.OLLAMA_URL; - const origModel = process.env.EMBEDDING_MODEL; + const origStorePath = process.env.CLAWMEM_STORE_PATH; + const origModel = process.env.CLAWMEM_EMBED_MODEL; + const origBusyTimeout = process.env.CLAWMEM_BUSY_TIMEOUT_MS; beforeEach(() => { - process.env.QDRANT_URL = undefined; - process.env.OLLAMA_URL = undefined; - process.env.EMBEDDING_MODEL = undefined; + delete process.env.CLAWMEM_STORE_PATH; + delete process.env.CLAWMEM_EMBED_MODEL; + delete process.env.CLAWMEM_BUSY_TIMEOUT_MS; }); afterEach(() => { - process.env.QDRANT_URL = origQdrant; - process.env.OLLAMA_URL = origOllama; - process.env.EMBEDDING_MODEL = origModel; + process.env.CLAWMEM_STORE_PATH = origStorePath; + process.env.CLAWMEM_EMBED_MODEL = origModel; + process.env.CLAWMEM_BUSY_TIMEOUT_MS = origBusyTimeout; }); test("uses YAML defaults when no env vars set", () => { const config = loadMemoryConfig(); - expect(config.qdrant.url).toBe("http://localhost:6333"); - expect(config.ollama.url).toBe("http://localhost:11434"); - expect(config.ollama.model).toBe("nomic-embed-text"); + expect(config.clawmem.store_path).toBe("data/clawmem.sqlite"); + expect(config.clawmem.embed_model).toBe("embedding"); + expect(config.clawmem.busy_timeout_ms).toBe(5000); }); - test("QDRANT_URL env var overrides YAML config", () => { - process.env.QDRANT_URL = "http://qdrant:6333"; + test("CLAWMEM_STORE_PATH env var overrides YAML config", () => { + process.env.CLAWMEM_STORE_PATH = "tmp/test-memory.sqlite"; const config = loadMemoryConfig(); - expect(config.qdrant.url).toBe("http://qdrant:6333"); + expect(config.clawmem.store_path).toBe("tmp/test-memory.sqlite"); }); - test("OLLAMA_URL env var overrides YAML config", () => { - process.env.OLLAMA_URL = "http://ollama:11434"; + test("CLAWMEM_EMBED_MODEL env var overrides YAML config", () => { + process.env.CLAWMEM_EMBED_MODEL = "text-embedding-3-small"; const config = loadMemoryConfig(); - expect(config.ollama.url).toBe("http://ollama:11434"); + expect(config.clawmem.embed_model).toBe("text-embedding-3-small"); }); - test("EMBEDDING_MODEL env var overrides YAML config", () => { - process.env.EMBEDDING_MODEL = "mxbai-embed-large"; + test("CLAWMEM_BUSY_TIMEOUT_MS env var overrides YAML config", () => { + process.env.CLAWMEM_BUSY_TIMEOUT_MS = "9000"; const config = loadMemoryConfig(); - expect(config.ollama.model).toBe("mxbai-embed-large"); + expect(config.clawmem.busy_timeout_ms).toBe(9000); }); test("env vars override for missing YAML file (defaults path)", () => { - process.env.QDRANT_URL = "http://qdrant:6333"; - process.env.OLLAMA_URL = "http://ollama:11434"; + process.env.CLAWMEM_STORE_PATH = "tmp/missing.sqlite"; + process.env.CLAWMEM_EMBED_MODEL = "voyage-3-large"; const config = loadMemoryConfig("config/nonexistent.yaml"); - expect(config.qdrant.url).toBe("http://qdrant:6333"); - expect(config.ollama.url).toBe("http://ollama:11434"); + expect(config.clawmem.store_path).toBe("tmp/missing.sqlite"); + expect(config.clawmem.embed_model).toBe("voyage-3-large"); }); test("non-memory fields are preserved when env vars set", () => { - process.env.QDRANT_URL = "http://qdrant:6333"; + process.env.CLAWMEM_STORE_PATH = "tmp/test-memory.sqlite"; const config = loadMemoryConfig(); expect(config.collections.episodes).toBe("episodes"); - expect(config.embedding.dimensions).toBe(768); expect(config.context.max_tokens).toBe(50000); }); }); diff --git a/src/memory/__tests__/context-builder.test.ts b/src/memory/__tests__/context-builder.test.ts index 13300c1..185dbee 100644 --- a/src/memory/__tests__/context-builder.test.ts +++ b/src/memory/__tests__/context-builder.test.ts @@ -4,10 +4,8 @@ import { MemoryContextBuilder } from "../context-builder.ts"; import type { MemorySystem } from "../system.ts"; const TEST_CONFIG: MemoryConfig = { - qdrant: { url: "http://localhost:6333" }, - ollama: { url: "http://localhost:11434", model: "nomic-embed-text" }, + clawmem: { store_path: "data/test-clawmem.sqlite", embed_model: "embedding", busy_timeout_ms: 5000 }, collections: { episodes: "episodes", semantic_facts: "semantic_facts", procedures: "procedures" }, - embedding: { dimensions: 768, batch_size: 32 }, context: { max_tokens: 50000, episode_limit: 10, fact_limit: 20, procedure_limit: 5 }, }; @@ -260,9 +258,9 @@ describe("MemoryContextBuilder", () => { test("handles errors from memory system gracefully", async () => { const memory = createMockMemorySystem({ - episodes: Promise.reject(new Error("Qdrant down")), - facts: Promise.reject(new Error("Qdrant down")), - procedure: Promise.reject(new Error("Qdrant down")), + episodes: Promise.reject(new Error("ClawMem unavailable")), + facts: Promise.reject(new Error("ClawMem unavailable")), + procedure: Promise.reject(new Error("ClawMem unavailable")), }); const builder = new MemoryContextBuilder(memory, TEST_CONFIG); diff --git a/src/memory/__tests__/embeddings.test.ts b/src/memory/__tests__/embeddings.test.ts deleted file mode 100644 index ca9f1e3..0000000 --- a/src/memory/__tests__/embeddings.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { afterAll, describe, expect, mock, test } from "bun:test"; -import type { MemoryConfig } from "../../config/types.ts"; -import { EmbeddingClient, textToSparseVector } from "../embeddings.ts"; - -const TEST_CONFIG: MemoryConfig = { - qdrant: { url: "http://localhost:6333" }, - ollama: { url: "http://localhost:11434", model: "nomic-embed-text" }, - collections: { episodes: "episodes", semantic_facts: "semantic_facts", procedures: "procedures" }, - embedding: { dimensions: 768, batch_size: 32 }, - context: { max_tokens: 50000, episode_limit: 10, fact_limit: 20, procedure_limit: 5 }, -}; - -function make768dVector(): number[] { - return Array.from({ length: 768 }, (_, i) => Math.sin(i * 0.01)); -} - -describe("EmbeddingClient", () => { - const originalFetch = globalThis.fetch; - - afterAll(() => { - globalThis.fetch = originalFetch; - }); - - test("embed() returns embedding vector from Ollama response", async () => { - const mockVector = make768dVector(); - - globalThis.fetch = mock(() => - Promise.resolve( - new Response(JSON.stringify({ embeddings: [mockVector] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ), - ) as unknown as typeof fetch; - - const client = new EmbeddingClient(TEST_CONFIG); - const result = await client.embed("test text"); - - expect(result).toEqual(mockVector); - expect(result.length).toBe(768); - }); - - test("embed() throws on HTTP error with helpful message", async () => { - globalThis.fetch = mock(() => - Promise.resolve(new Response("model not found", { status: 404 })), - ) as unknown as typeof fetch; - - const client = new EmbeddingClient(TEST_CONFIG); - await expect(client.embed("test")).rejects.toThrow("Ollama embedding failed (404)"); - }); - - test("embed() throws on empty embeddings response", async () => { - globalThis.fetch = mock(() => - Promise.resolve( - new Response(JSON.stringify({ embeddings: [] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ), - ) as unknown as typeof fetch; - - const client = new EmbeddingClient(TEST_CONFIG); - await expect(client.embed("test")).rejects.toThrow("empty embeddings"); - }); - - test("embedBatch() returns multiple vectors", async () => { - const vec1 = make768dVector(); - const vec2 = make768dVector().map((v) => v + 0.5); - - globalThis.fetch = mock(() => - Promise.resolve( - new Response(JSON.stringify({ embeddings: [vec1, vec2] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ), - ) as unknown as typeof fetch; - - const client = new EmbeddingClient(TEST_CONFIG); - const results = await client.embedBatch(["text one", "text two"]); - - expect(results.length).toBe(2); - expect(results[0]).toEqual(vec1); - expect(results[1]).toEqual(vec2); - }); - - test("embedBatch() throws on mismatched count", async () => { - const vec1 = make768dVector(); - - globalThis.fetch = mock(() => - Promise.resolve( - new Response(JSON.stringify({ embeddings: [vec1] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ), - ) as unknown as typeof fetch; - - const client = new EmbeddingClient(TEST_CONFIG); - await expect(client.embedBatch(["one", "two", "three"])).rejects.toThrow("1 embeddings for 3 inputs"); - }); - - test("isHealthy() returns true when Ollama is up", async () => { - globalThis.fetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ models: [] }), { status: 200 })), - ) as unknown as typeof fetch; - - const client = new EmbeddingClient(TEST_CONFIG); - expect(await client.isHealthy()).toBe(true); - }); - - test("isHealthy() returns false when Ollama is down", async () => { - globalThis.fetch = mock(() => Promise.reject(new Error("ECONNREFUSED"))) as unknown as typeof fetch; - - const client = new EmbeddingClient(TEST_CONFIG); - expect(await client.isHealthy()).toBe(false); - }); -}); - -describe("textToSparseVector", () => { - test("generates sparse vector from text", () => { - const result = textToSparseVector("the quick brown fox jumps over the lazy dog"); - - expect(result.indices.length).toBeGreaterThan(0); - expect(result.values.length).toBe(result.indices.length); - // All values should be positive TF scores - for (const v of result.values) { - expect(v).toBeGreaterThan(0); - expect(v).toBeLessThanOrEqual(1); - } - }); - - test("handles empty text", () => { - const result = textToSparseVector(""); - expect(result.indices.length).toBe(0); - expect(result.values.length).toBe(0); - }); - - test("produces consistent hashes for same tokens", () => { - const a = textToSparseVector("hello world"); - const b = textToSparseVector("hello world"); - expect(a.indices).toEqual(b.indices); - expect(a.values).toEqual(b.values); - }); - - test("produces different hashes for different tokens", () => { - const a = textToSparseVector("hello world"); - const b = textToSparseVector("goodbye moon"); - // At least some indices should differ - const aSet = new Set(a.indices); - const bSet = new Set(b.indices); - const intersection = [...aSet].filter((x) => bSet.has(x)); - expect(intersection.length).toBeLessThan(a.indices.length); - }); - - test("filters single-character tokens", () => { - const result = textToSparseVector("I am a b c test"); - // "I", "a", "b", "c" are single chars and should be filtered - expect(result.indices.length).toBe(2); // "am" and "test" - }); -}); diff --git a/src/memory/__tests__/episodic.test.ts b/src/memory/__tests__/episodic.test.ts deleted file mode 100644 index 1997e21..0000000 --- a/src/memory/__tests__/episodic.test.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { afterAll, describe, expect, mock, test } from "bun:test"; -import type { MemoryConfig } from "../../config/types.ts"; -import { EmbeddingClient } from "../embeddings.ts"; -import { EpisodicStore } from "../episodic.ts"; -import { QdrantClient } from "../qdrant-client.ts"; -import type { Episode } from "../types.ts"; - -const TEST_CONFIG: MemoryConfig = { - qdrant: { url: "http://localhost:6333" }, - ollama: { url: "http://localhost:11434", model: "nomic-embed-text" }, - collections: { episodes: "episodes", semantic_facts: "semantic_facts", procedures: "procedures" }, - embedding: { dimensions: 768, batch_size: 32 }, - context: { max_tokens: 50000, episode_limit: 10, fact_limit: 20, procedure_limit: 5 }, -}; - -function makeTestEpisode(overrides?: Partial): Episode { - return { - id: "ep-001", - type: "task", - summary: "Deployed the staging server", - detail: "User asked to deploy staging. Ran tests, created PR, merged.", - parent_id: null, - session_id: "session-1", - user_id: "user-1", - tools_used: ["Bash", "Write"], - files_touched: ["/deploy.sh"], - outcome: "success", - outcome_detail: "Deployment completed successfully", - lessons: ["Always run tests before deploying"], - started_at: new Date(Date.now() - 3600000).toISOString(), - ended_at: new Date().toISOString(), - duration_seconds: 3600, - importance: 0.8, - access_count: 0, - last_accessed_at: new Date().toISOString(), - decay_rate: 1.0, - ...overrides, - }; -} - -function make768dVector(): number[] { - return Array.from({ length: 768 }, (_, i) => Math.sin(i * 0.01)); -} - -describe("EpisodicStore", () => { - const originalFetch = globalThis.fetch; - - afterAll(() => { - globalThis.fetch = originalFetch; - }); - - test("store() embeds summary and detail, upserts to Qdrant", async () => { - const vec = make768dVector(); - let upsertCalled = false; - let upsertBody: Record | null = null; - - globalThis.fetch = mock((url: string | Request, init?: RequestInit) => { - const urlStr = typeof url === "string" ? url : url.url; - - // Ollama embed - if (urlStr.includes("/api/embed")) { - return Promise.resolve( - new Response(JSON.stringify({ embeddings: [vec, vec] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ); - } - - // Qdrant collection check - if (urlStr.includes("/collections/episodes") && (!init?.method || init?.method === "GET")) { - return Promise.resolve(new Response(JSON.stringify({}), { status: 200 })); - } - - // Qdrant upsert - if (urlStr.includes("/points") && init?.method === "PUT") { - upsertCalled = true; - upsertBody = JSON.parse(init.body as string); - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - } - - // Qdrant payload index - if (urlStr.includes("/index")) { - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - } - - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new EpisodicStore(qdrant, embedder, TEST_CONFIG); - - const episode = makeTestEpisode(); - const id = await store.store(episode); - - expect(id).toBe("ep-001"); - expect(upsertCalled).toBe(true); - - const body = upsertBody as unknown as Record; - const points = body.points as Array>; - expect(points.length).toBe(1); - expect(points[0].id).toBe("ep-001"); - - const payload = points[0].payload as Record; - expect(payload.type).toBe("task"); - expect(payload.outcome).toBe("success"); - expect(payload.session_id).toBe("session-1"); - }); - - test("recall() searches Qdrant and returns episodes", async () => { - const vec = make768dVector(); - const now = Date.now(); - - globalThis.fetch = mock((url: string | Request, _init?: RequestInit) => { - const urlStr = typeof url === "string" ? url : url.url; - - // Ollama embed (for query) - if (urlStr.includes("/api/embed")) { - return Promise.resolve( - new Response(JSON.stringify({ embeddings: [vec] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ); - } - - // Qdrant query - if (urlStr.includes("/points/query")) { - return Promise.resolve( - new Response( - JSON.stringify({ - result: { - points: [ - { - id: "ep-001", - score: 0.9, - payload: { - type: "task", - summary: "Deployed the staging server", - detail: "Full detail here", - session_id: "session-1", - user_id: "user-1", - outcome: "success", - importance: 0.8, - started_at: now - 3600000, - ended_at: now, - tools_used: ["Bash"], - files_touched: ["/deploy.sh"], - lessons: ["test first"], - access_count: 2, - }, - }, - ], - }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - ); - } - - // Qdrant payload update (access count) - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new EpisodicStore(qdrant, embedder, TEST_CONFIG); - - const episodes = await store.recall("What happened with the staging deployment?"); - - expect(episodes.length).toBe(1); - expect(episodes[0].id).toBe("ep-001"); - expect(episodes[0].summary).toBe("Deployed the staging server"); - expect(episodes[0].outcome).toBe("success"); - }); - - test("recall() applies recency-biased scoring by default", async () => { - const vec = make768dVector(); - const now = Date.now(); - - globalThis.fetch = mock((url: string | Request) => { - const urlStr = typeof url === "string" ? url : url.url; - - if (urlStr.includes("/api/embed")) { - return Promise.resolve(new Response(JSON.stringify({ embeddings: [vec] }), { status: 200 })); - } - - if (urlStr.includes("/points/query")) { - return Promise.resolve( - new Response( - JSON.stringify({ - result: { - points: [ - { - id: "old-ep", - score: 0.95, - payload: { - type: "task", - summary: "Old high-score episode", - importance: 0.3, - started_at: now - 30 * 24 * 3600 * 1000, // 30 days ago - }, - }, - { - id: "new-ep", - score: 0.7, - payload: { - type: "task", - summary: "Recent lower-score episode", - importance: 0.7, - started_at: now - 3600000, // 1 hour ago - }, - }, - ], - }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - ); - } - - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new EpisodicStore(qdrant, embedder, TEST_CONFIG); - - const episodes = await store.recall("test query"); - - // With recency-biased scoring, the recent episode should rank higher - // despite having a lower raw search score - expect(episodes[0].id).toBe("new-ep"); - expect(episodes[1].id).toBe("old-ep"); - }); - - test("recall() metadata strategy favors reinforced memories", async () => { - const vec = make768dVector(); - const now = Date.now(); - - globalThis.fetch = mock((url: string | Request) => { - const urlStr = typeof url === "string" ? url : url.url; - - if (urlStr.includes("/api/embed")) { - return Promise.resolve(new Response(JSON.stringify({ embeddings: [vec] }), { status: 200 })); - } - - if (urlStr.includes("/points/query")) { - return Promise.resolve( - new Response( - JSON.stringify({ - result: { - points: [ - { - id: "stale-ep", - score: 0.82, - payload: { - type: "task", - summary: "Stale one-off episode", - importance: 0.3, - access_count: 0, - last_accessed_at: new Date(now - 45 * 24 * 3600 * 1000).toISOString(), - started_at: now - 45 * 24 * 3600 * 1000, - }, - }, - { - id: "durable-ep", - score: 0.7, - payload: { - type: "task", - summary: "Frequently reused deployment memory", - importance: 0.8, - access_count: 6, - last_accessed_at: new Date(now - 2 * 24 * 3600 * 1000).toISOString(), - started_at: now - 45 * 24 * 3600 * 1000, - }, - }, - ], - }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - ); - } - - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new EpisodicStore(qdrant, embedder, TEST_CONFIG); - - const episodes = await store.recall("deployment", { strategy: "metadata" }); - - expect(episodes[0].id).toBe("durable-ep"); - expect(episodes[1].id).toBe("stale-ep"); - }); -}); diff --git a/src/memory/__tests__/qdrant-client.test.ts b/src/memory/__tests__/qdrant-client.test.ts deleted file mode 100644 index 3ae90fb..0000000 --- a/src/memory/__tests__/qdrant-client.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { afterAll, describe, expect, mock, test } from "bun:test"; -import type { MemoryConfig } from "../../config/types.ts"; -import { QdrantClient } from "../qdrant-client.ts"; - -const TEST_CONFIG: MemoryConfig = { - qdrant: { url: "http://localhost:6333" }, - ollama: { url: "http://localhost:11434", model: "nomic-embed-text" }, - collections: { episodes: "episodes", semantic_facts: "semantic_facts", procedures: "procedures" }, - embedding: { dimensions: 768, batch_size: 32 }, - context: { max_tokens: 50000, episode_limit: 10, fact_limit: 20, procedure_limit: 5 }, -}; - -describe("QdrantClient", () => { - const originalFetch = globalThis.fetch; - - afterAll(() => { - globalThis.fetch = originalFetch; - }); - - test("createCollection sends PUT with correct schema", async () => { - const calls: { url: string; method: string; body: string }[] = []; - - globalThis.fetch = mock((url: string | Request, init?: RequestInit) => { - const urlStr = typeof url === "string" ? url : url.url; - - // collectionExists check returns 404 (doesn't exist) - if (init?.method === undefined || init?.method === "GET") { - return Promise.resolve(new Response("", { status: 404 })); - } - - calls.push({ - url: urlStr, - method: init?.method ?? "GET", - body: init?.body as string, - }); - - return Promise.resolve( - new Response(JSON.stringify({ status: "ok", result: true }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ); - }) as unknown as typeof fetch; - - const client = new QdrantClient(TEST_CONFIG); - await client.createCollection("test_collection", { - vectors: { - summary: { size: 768, distance: "Cosine" }, - }, - sparse_vectors: { - text_bm25: {}, - }, - }); - - expect(calls.length).toBe(1); - expect(calls[0].url).toContain("/collections/test_collection"); - expect(calls[0].method).toBe("PUT"); - - const body = JSON.parse(calls[0].body); - expect(body.vectors.summary.size).toBe(768); - expect(body.sparse_vectors.text_bm25).toBeDefined(); - }); - - test("createCollection skips if collection already exists", async () => { - let putCalled = false; - - globalThis.fetch = mock((_url: string | Request, init?: RequestInit) => { - if (init?.method === "PUT") { - putCalled = true; - } - // collectionExists returns 200 (exists) - return Promise.resolve( - new Response(JSON.stringify({ status: "ok" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ); - }) as unknown as typeof fetch; - - const client = new QdrantClient(TEST_CONFIG); - await client.createCollection("existing", { vectors: {} }); - - expect(putCalled).toBe(false); - }); - - test("upsert sends points with named vectors", async () => { - let capturedBody: Record | null = null; - - globalThis.fetch = mock((_url: string | Request, init?: RequestInit) => { - if (init?.body) { - capturedBody = JSON.parse(init.body as string); - } - return Promise.resolve( - new Response(JSON.stringify({ status: "ok", result: { operation_id: 1 } }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ); - }) as unknown as typeof fetch; - - const client = new QdrantClient(TEST_CONFIG); - await client.upsert("episodes", [ - { - id: "test-id", - vector: { - summary: [0.1, 0.2, 0.3], - text_bm25: { indices: [1, 42], values: [0.5, 0.8] }, - }, - payload: { type: "task", summary: "test" }, - }, - ]); - - const body = capturedBody as unknown as Record; - expect(body).not.toBeNull(); - const points = body.points as Array>; - expect(points.length).toBe(1); - expect(points[0].id).toBe("test-id"); - expect((points[0].vector as Record).summary).toEqual([0.1, 0.2, 0.3]); - expect((points[0].vector as Record).text_bm25).toEqual({ indices: [1, 42], values: [0.5, 0.8] }); - }); - - test("search with hybrid search sends prefetch+RRF", async () => { - let capturedBody: Record | null = null; - - globalThis.fetch = mock((_url: string | Request, init?: RequestInit) => { - if (init?.body) { - capturedBody = JSON.parse(init.body as string); - } - return Promise.resolve( - new Response( - JSON.stringify({ - result: { - points: [ - { id: "result-1", score: 0.95, payload: { summary: "test memory" } }, - { id: "result-2", score: 0.8, payload: { summary: "another memory" } }, - ], - }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - ); - }) as unknown as typeof fetch; - - const client = new QdrantClient(TEST_CONFIG); - const results = await client.search("episodes", { - denseVector: [0.1, 0.2, 0.3], - denseVectorName: "summary", - sparseVector: { indices: [1, 42], values: [0.5, 0.8] }, - sparseVectorName: "text_bm25", - limit: 5, - }); - - expect(results.length).toBe(2); - expect(results[0].id).toBe("result-1"); - expect(results[0].score).toBe(0.95); - expect(results[0].payload.summary).toBe("test memory"); - - // Verify hybrid search structure - const hybridBody = capturedBody as unknown as Record; - expect(hybridBody).not.toBeNull(); - expect(hybridBody.prefetch).toBeDefined(); - expect((hybridBody.query as Record).fusion).toBe("rrf"); - }); - - test("search with dense-only sends direct query", async () => { - let capturedBody: Record | null = null; - - globalThis.fetch = mock((_url: string | Request, init?: RequestInit) => { - if (init?.body) { - capturedBody = JSON.parse(init.body as string); - } - return Promise.resolve( - new Response(JSON.stringify({ result: { points: [] } }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ); - }) as unknown as typeof fetch; - - const client = new QdrantClient(TEST_CONFIG); - await client.search("episodes", { - denseVector: [0.1, 0.2], - denseVectorName: "summary", - limit: 5, - }); - - const denseBody = capturedBody as unknown as Record; - expect(denseBody).not.toBeNull(); - expect(denseBody.query).toEqual([0.1, 0.2]); - expect(denseBody.using).toBe("summary"); - expect(denseBody.prefetch).toBeUndefined(); - }); - - test("search returns empty array when no vectors provided", async () => { - const client = new QdrantClient(TEST_CONFIG); - const results = await client.search("episodes", { limit: 5 }); - expect(results).toEqual([]); - }); - - test("isHealthy returns true when Qdrant responds", async () => { - globalThis.fetch = mock(() => - Promise.resolve(new Response('{"title":"ok"}', { status: 200 })), - ) as unknown as typeof fetch; - - const client = new QdrantClient(TEST_CONFIG); - expect(await client.isHealthy()).toBe(true); - }); - - test("isHealthy returns false when Qdrant is down", async () => { - globalThis.fetch = mock(() => Promise.reject(new Error("ECONNREFUSED"))) as unknown as typeof fetch; - - const client = new QdrantClient(TEST_CONFIG); - expect(await client.isHealthy()).toBe(false); - }); - - test("deletePoint sends correct request", async () => { - let capturedUrl = ""; - let capturedBody: Record | null = null; - - globalThis.fetch = mock((url: string | Request, init?: RequestInit) => { - capturedUrl = typeof url === "string" ? url : url.url; - if (init?.body) { - capturedBody = JSON.parse(init.body as string); - } - return Promise.resolve( - new Response(JSON.stringify({ status: "ok" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ); - }) as unknown as typeof fetch; - - const client = new QdrantClient(TEST_CONFIG); - await client.deletePoint("episodes", "point-123"); - - expect(capturedUrl).toContain("/collections/episodes/points/delete"); - const deleteBody = capturedBody as unknown as Record; - expect(deleteBody).not.toBeNull(); - expect(deleteBody.points).toEqual(["point-123"]); - }); -}); diff --git a/src/memory/__tests__/semantic.test.ts b/src/memory/__tests__/semantic.test.ts deleted file mode 100644 index f2f555d..0000000 --- a/src/memory/__tests__/semantic.test.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { afterAll, describe, expect, mock, test } from "bun:test"; -import type { MemoryConfig } from "../../config/types.ts"; -import { EmbeddingClient } from "../embeddings.ts"; -import { QdrantClient } from "../qdrant-client.ts"; -import { SemanticStore } from "../semantic.ts"; -import type { SemanticFact } from "../types.ts"; - -const TEST_CONFIG: MemoryConfig = { - qdrant: { url: "http://localhost:6333" }, - ollama: { url: "http://localhost:11434", model: "nomic-embed-text" }, - collections: { episodes: "episodes", semantic_facts: "semantic_facts", procedures: "procedures" }, - embedding: { dimensions: 768, batch_size: 32 }, - context: { max_tokens: 50000, episode_limit: 10, fact_limit: 20, procedure_limit: 5 }, -}; - -function makeTestFact(overrides?: Partial): SemanticFact { - return { - id: "fact-001", - subject: "staging server", - predicate: "runs on", - object: "port 3001", - natural_language: "The staging server runs on port 3001", - source_episode_ids: ["ep-001"], - confidence: 0.9, - valid_from: new Date().toISOString(), - valid_until: null, - version: 1, - previous_version_id: null, - category: "domain_knowledge", - tags: ["infrastructure"], - ...overrides, - }; -} - -function make768dVector(): number[] { - return Array.from({ length: 768 }, (_, i) => Math.sin(i * 0.01)); -} - -describe("SemanticStore", () => { - const originalFetch = globalThis.fetch; - - afterAll(() => { - globalThis.fetch = originalFetch; - }); - - test("store() embeds fact and upserts to Qdrant", async () => { - const vec = make768dVector(); - let upsertBody: Record | null = null; - - globalThis.fetch = mock((url: string | Request, init?: RequestInit) => { - const urlStr = typeof url === "string" ? url : url.url; - - if (urlStr.includes("/api/embed")) { - return Promise.resolve(new Response(JSON.stringify({ embeddings: [vec] }), { status: 200 })); - } - - // Contradiction search returns no results - if (urlStr.includes("/points/query")) { - return Promise.resolve(new Response(JSON.stringify({ result: { points: [] } }), { status: 200 })); - } - - if (urlStr.includes("/points") && init?.method === "PUT") { - upsertBody = JSON.parse(init.body as string); - } - - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new SemanticStore(qdrant, embedder, TEST_CONFIG); - - const fact = makeTestFact(); - const id = await store.store(fact); - - expect(id).toBe("fact-001"); - expect(upsertBody).not.toBeNull(); - - const upsertData = upsertBody as unknown as Record; - const points = upsertData.points as Array>; - const payload = points[0].payload as Record; - expect(payload.subject).toBe("staging server"); - expect(payload.predicate).toBe("runs on"); - expect(payload.category).toBe("domain_knowledge"); - }); - - test("recall() returns facts filtered to currently valid", async () => { - const vec = make768dVector(); - let capturedBody: Record | null = null; - - globalThis.fetch = mock((url: string | Request, init?: RequestInit) => { - const urlStr = typeof url === "string" ? url : url.url; - - if (urlStr.includes("/api/embed")) { - return Promise.resolve(new Response(JSON.stringify({ embeddings: [vec] }), { status: 200 })); - } - - if (urlStr.includes("/points/query")) { - if (init?.body) capturedBody = JSON.parse(init.body as string); - return Promise.resolve( - new Response( - JSON.stringify({ - result: { - points: [ - { - id: "fact-001", - score: 0.9, - payload: { - subject: "staging server", - predicate: "runs on", - object: "port 3001", - natural_language: "The staging server runs on port 3001", - confidence: 0.9, - valid_from: Date.now() - 86400000, - valid_until: null, - category: "domain_knowledge", - version: 1, - }, - }, - ], - }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - ); - } - - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new SemanticStore(qdrant, embedder, TEST_CONFIG); - - const facts = await store.recall("staging server port"); - - expect(facts.length).toBe(1); - expect(facts[0].subject).toBe("staging server"); - expect(facts[0].valid_until).toBeNull(); - - // Verify the filter includes valid_until is_null check - const recallBody = capturedBody as unknown as Record; - expect(recallBody).not.toBeNull(); - const filter = recallBody.filter as Record | undefined; - expect(filter).toBeDefined(); - }); - - test("findContradictions() detects conflicting facts", async () => { - const vec = make768dVector(); - - globalThis.fetch = mock((url: string | Request) => { - const urlStr = typeof url === "string" ? url : url.url; - - if (urlStr.includes("/api/embed")) { - return Promise.resolve(new Response(JSON.stringify({ embeddings: [vec] }), { status: 200 })); - } - - if (urlStr.includes("/points/query")) { - return Promise.resolve( - new Response( - JSON.stringify({ - result: { - points: [ - { - id: "fact-old", - score: 0.92, - payload: { - subject: "staging server", - predicate: "runs on", - object: "port 3000", - natural_language: "The staging server runs on port 3000", - confidence: 0.8, - valid_until: null, - }, - }, - ], - }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - ); - } - - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new SemanticStore(qdrant, embedder, TEST_CONFIG); - - const newFact = makeTestFact({ object: "port 3001" }); - const contradictions = await store.findContradictions(newFact); - - // The existing fact says port 3000, new fact says port 3001 - contradiction - expect(contradictions.length).toBe(1); - expect(contradictions[0].object).toBe("port 3000"); - }); - - test("findContradictions() does not flag same-object facts", async () => { - const vec = make768dVector(); - - globalThis.fetch = mock((url: string | Request) => { - const urlStr = typeof url === "string" ? url : url.url; - - if (urlStr.includes("/api/embed")) { - return Promise.resolve(new Response(JSON.stringify({ embeddings: [vec] }), { status: 200 })); - } - - if (urlStr.includes("/points/query")) { - return Promise.resolve( - new Response( - JSON.stringify({ - result: { - points: [ - { - id: "fact-same", - score: 0.95, - payload: { - subject: "staging server", - predicate: "runs on", - object: "port 3001", - valid_until: null, - }, - }, - ], - }, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ), - ); - } - - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new SemanticStore(qdrant, embedder, TEST_CONFIG); - - const newFact = makeTestFact({ object: "port 3001" }); - const contradictions = await store.findContradictions(newFact); - - // Same object value is not a contradiction - expect(contradictions.length).toBe(0); - }); - - test("resolveContradiction() invalidates old fact when new has higher confidence", async () => { - let updatePayloadCalled = false; - let updateBody: Record | null = null; - - globalThis.fetch = mock((_url: string | Request, init?: RequestInit) => { - if (init?.method === "POST" && init.body) { - updatePayloadCalled = true; - updateBody = JSON.parse(init.body as string); - } - return Promise.resolve(new Response(JSON.stringify({ status: "ok" }), { status: 200 })); - }) as unknown as typeof fetch; - - const qdrant = new QdrantClient(TEST_CONFIG); - const embedder = new EmbeddingClient(TEST_CONFIG); - const store = new SemanticStore(qdrant, embedder, TEST_CONFIG); - - const newFact = makeTestFact({ confidence: 0.9 }); - const oldFact = makeTestFact({ id: "fact-old", confidence: 0.7 }); - - await store.resolveContradiction(newFact, oldFact); - - expect(updatePayloadCalled).toBe(true); - // The old fact should have valid_until set - const updateData = updateBody as unknown as Record; - const payload = updateData.payload as Record; - expect(payload.valid_until).toBeDefined(); - expect(typeof payload.valid_until).toBe("number"); - }); -}); diff --git a/src/memory/__tests__/system.test.ts b/src/memory/__tests__/system.test.ts new file mode 100644 index 0000000..f6177a2 --- /dev/null +++ b/src/memory/__tests__/system.test.ts @@ -0,0 +1,201 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { MemoryConfig } from "../../config/types.ts"; +import { setClawMemDefaultLlama, type ClawMemLlamaLike } from "../clawmem-runtime.ts"; +import { MemorySystem } from "../system.ts"; +import type { Episode, Procedure, SemanticFact } from "../types.ts"; + +function createConfig(storePath: string): MemoryConfig { + return { + clawmem: { + store_path: storePath, + embed_model: "embedding", + busy_timeout_ms: 1000, + }, + collections: { + episodes: "episodes", + semantic_facts: "semantic_facts", + procedures: "procedures", + }, + context: { + max_tokens: 50000, + episode_limit: 10, + fact_limit: 20, + procedure_limit: 5, + }, + }; +} + +function createFakeLlm(): ClawMemLlamaLike { + return { + embed: async (text: string) => ({ + embedding: buildEmbedding(text), + model: "fake-embed", + }), + }; +} + +function buildEmbedding(text: string): number[] { + const lower = text.toLowerCase(); + const keywords = ["deploy", "staging", "server", "port", "branch", "procedure", "test", "user"]; + const base: number[] = keywords.map((keyword) => (lower.includes(keyword) ? 1 : 0)); + base.push(Math.max(0.01, Math.min(lower.length / 1000, 1))); + return base; +} + +function createEpisode(): Episode { + return { + id: "ep-1", + type: "task", + summary: "Deploy staging server", + detail: "Updated the release pipeline and deployed the staging server to port 3001.", + parent_id: null, + session_id: "session-1", + user_id: "user-1", + tools_used: ["Bash"], + files_touched: ["src/index.ts"], + outcome: "success", + outcome_detail: "Deployment completed cleanly", + lessons: ["Run tests before deployment"], + started_at: new Date(Date.now() - 60_000).toISOString(), + ended_at: new Date().toISOString(), + duration_seconds: 60, + importance: 0.9, + access_count: 0, + last_accessed_at: new Date().toISOString(), + decay_rate: 1, + }; +} + +function createFact(overrides?: Partial): SemanticFact { + return { + id: crypto.randomUUID(), + subject: "staging server", + predicate: "runs on", + object: "port 3000", + natural_language: "The staging server runs on port 3000.", + source_episode_ids: ["ep-1"], + confidence: 0.7, + valid_from: new Date().toISOString(), + valid_until: null, + version: 1, + previous_version_id: null, + category: "codebase", + tags: ["staging"], + ...overrides, + }; +} + +function createProcedure(): Procedure { + return { + id: "proc-1", + name: "deploy_staging", + description: "Deploy the application to staging.", + trigger: "Use when the user asks for a staging deploy.", + steps: [ + { + order: 1, + action: "Run tests", + tool: "Bash", + expected_outcome: "All tests pass", + error_handling: null, + decision_point: false, + }, + { + order: 2, + action: "Deploy to staging", + tool: "Bash", + expected_outcome: "Staging is updated", + error_handling: null, + decision_point: false, + }, + ], + preconditions: ["CI is green"], + postconditions: ["Staging has the latest build"], + parameters: {}, + source_episode_ids: ["ep-1"], + success_count: 0, + failure_count: 0, + last_used_at: new Date(0).toISOString(), + confidence: 0.8, + version: 1, + }; +} + +describe("MemorySystem with ClawMem", () => { + let tempDir: string; + let memory: MemorySystem; + + beforeEach(async () => { + tempDir = mkdtempSync(join(tmpdir(), "phantom-clawmem-")); + await setClawMemDefaultLlama(createFakeLlm()); + memory = new MemorySystem(createConfig(join(tempDir, "memory.sqlite"))); + await memory.initialize(); + }); + + afterEach(async () => { + await memory.close(); + await setClawMemDefaultLlama(null); + await removeTempDir(tempDir); + }); + + test("stores and recalls episodic memory", async () => { + await memory.storeEpisode(createEpisode()); + + const episodes = await memory.recallEpisodes("deploy staging", { limit: 5 }); + expect(episodes).toHaveLength(1); + expect(episodes[0]?.summary).toContain("Deploy staging server"); + }); + + test("invalidates contradicted facts when a stronger fact arrives", async () => { + await memory.storeFact(createFact()); + await memory.storeFact( + createFact({ + subject: "staging server", + predicate: "runs on", + object: "port 3001", + natural_language: "The staging server runs on port 3001.", + confidence: 0.95, + }), + ); + + const facts = await memory.recallFacts("staging server port", { limit: 10 }); + expect(facts).toHaveLength(1); + expect(facts[0]?.object).toBe("port 3001"); + expect(facts[0]?.valid_until).toBeNull(); + }); + + test("finds procedures and updates their outcomes", async () => { + await memory.storeProcedure(createProcedure()); + await memory.updateProcedureOutcome("proc-1", true); + + const procedure = await memory.findProcedure("deploy the app to staging"); + expect(procedure).not.toBeNull(); + expect(procedure?.success_count).toBe(1); + expect(procedure?.failure_count).toBe(0); + }); + + test("reports clawmem health when initialized", async () => { + const health = await memory.healthCheck(); + expect(health).toEqual({ clawmem: true, configured: true }); + }); +}); + +async function removeTempDir(path: string): Promise { + for (let attempt = 0; attempt < 5; attempt++) { + try { + rmSync(path, { recursive: true, force: true }); + return; + } catch (error: unknown) { + const code = error instanceof Error && "code" in error ? String(error.code) : ""; + if (code !== "EBUSY") { + throw error; + } + if (attempt < 4) { + await Bun.sleep(50); + } + } + } +} diff --git a/src/memory/clawmem-records.ts b/src/memory/clawmem-records.ts new file mode 100644 index 0000000..06fcc4a --- /dev/null +++ b/src/memory/clawmem-records.ts @@ -0,0 +1,206 @@ +import type { Episode, Procedure, SemanticFact } from "./types.ts"; + +type MemoryRecordKind = "episode" | "fact" | "procedure"; + +type MemoryRecordEnvelope = + | { kind: "episode"; record: Episode } + | { kind: "fact"; record: SemanticFact } + | { kind: "procedure"; record: Procedure }; + +export type SerializedMemoryDocument = { + path: string; + title: string; + body: string; + embeddingText: string; + contentType: "progress" | "decision" | "project"; + confidence: number; + qualityScore: number; + semanticPayload: string; + topicKey: string; +}; + +const ENVELOPE_PREFIX = "PHANTOM_MEMORY:"; + +export function episodeDocumentPath(id: string): string { + return `${id}.md`; +} + +export function factDocumentPath(id: string): string { + return `${id}.md`; +} + +export function procedureDocumentPath(id: string): string { + return `${id}.md`; +} + +export function serializeEpisode(episode: Episode): SerializedMemoryDocument { + const lines = [ + `Type: ${episode.type}`, + `Summary: ${episode.summary}`, + `Detail: ${episode.detail}`, + `Outcome: ${episode.outcome}`, + `Outcome Detail: ${episode.outcome_detail}`, + `Session: ${episode.session_id}`, + `User: ${episode.user_id}`, + `Started: ${episode.started_at}`, + `Ended: ${episode.ended_at}`, + `Duration Seconds: ${episode.duration_seconds}`, + episode.tools_used.length > 0 ? `Tools Used: ${episode.tools_used.join(", ")}` : "", + episode.files_touched.length > 0 ? `Files Touched: ${episode.files_touched.join(", ")}` : "", + episode.lessons.length > 0 ? `Lessons: ${episode.lessons.join(" | ")}` : "", + ] + .filter(Boolean) + .join("\n"); + + return { + path: episodeDocumentPath(episode.id), + title: compactTitle(episode.summary, `${episode.type} episode`), + body: buildBody("episode", episode, lines), + embeddingText: lines, + contentType: "progress", + confidence: clampScore(episode.importance), + qualityScore: clampScore((episode.importance + 0.5) / 1.5), + semanticPayload: stableJson({ + id: episode.id, + summary: episode.summary, + detail: episode.detail, + outcome: episode.outcome, + session_id: episode.session_id, + user_id: episode.user_id, + started_at: episode.started_at, + ended_at: episode.ended_at, + }), + topicKey: episode.session_id, + }; +} + +export function serializeFact(fact: SemanticFact): SerializedMemoryDocument { + const lines = [ + `Natural Language: ${fact.natural_language}`, + `Subject: ${fact.subject}`, + `Predicate: ${fact.predicate}`, + `Object: ${fact.object}`, + `Category: ${fact.category}`, + `Confidence: ${fact.confidence}`, + `Valid From: ${fact.valid_from}`, + `Valid Until: ${fact.valid_until ?? "active"}`, + fact.tags.length > 0 ? `Tags: ${fact.tags.join(", ")}` : "", + ] + .filter(Boolean) + .join("\n"); + + return { + path: factDocumentPath(fact.id), + title: compactTitle(fact.natural_language, `${fact.subject} ${fact.predicate} ${fact.object}`), + body: buildBody("fact", fact, lines), + embeddingText: lines, + contentType: "decision", + confidence: clampScore(fact.confidence), + qualityScore: clampScore(fact.confidence), + semanticPayload: stableJson({ + subject: fact.subject, + predicate: fact.predicate, + object: fact.object, + category: fact.category, + confidence: fact.confidence, + valid_from: fact.valid_from, + valid_until: fact.valid_until, + version: fact.version, + tags: fact.tags, + }), + topicKey: `${fact.subject}:${fact.predicate}`, + }; +} + +export function serializeProcedure(procedure: Procedure): SerializedMemoryDocument { + const steps = procedure.steps + .map((step) => `${step.order}. ${step.action} -> ${step.expected_outcome}`) + .join("\n"); + const lines = [ + `Name: ${procedure.name}`, + `Description: ${procedure.description}`, + `Trigger: ${procedure.trigger}`, + steps ? `Steps:\n${steps}` : "", + procedure.preconditions.length > 0 ? `Preconditions: ${procedure.preconditions.join(" | ")}` : "", + procedure.postconditions.length > 0 ? `Postconditions: ${procedure.postconditions.join(" | ")}` : "", + `Success Count: ${procedure.success_count}`, + `Failure Count: ${procedure.failure_count}`, + `Last Used At: ${procedure.last_used_at}`, + `Confidence: ${procedure.confidence}`, + ] + .filter(Boolean) + .join("\n"); + const totalRuns = procedure.success_count + procedure.failure_count; + const successRate = totalRuns === 0 ? 0.5 : procedure.success_count / totalRuns; + + return { + path: procedureDocumentPath(procedure.id), + title: compactTitle(procedure.name, "procedure"), + body: buildBody("procedure", procedure, lines), + embeddingText: lines, + contentType: "project", + confidence: clampScore(procedure.confidence), + qualityScore: clampScore((successRate + procedure.confidence) / 2), + semanticPayload: stableJson({ + name: procedure.name, + description: procedure.description, + trigger: procedure.trigger, + steps: procedure.steps, + preconditions: procedure.preconditions, + postconditions: procedure.postconditions, + parameters: procedure.parameters, + success_count: procedure.success_count, + failure_count: procedure.failure_count, + last_used_at: procedure.last_used_at, + confidence: procedure.confidence, + version: procedure.version, + }), + topicKey: procedure.name, + }; +} + +export function parseEpisodeDocument(body: string): Episode | null { + return parseEnvelope(body, "episode"); +} + +export function parseFactDocument(body: string): SemanticFact | null { + return parseEnvelope(body, "fact"); +} + +export function parseProcedureDocument(body: string): Procedure | null { + return parseEnvelope(body, "procedure"); +} + +function buildBody(kind: MemoryRecordKind, record: Episode | SemanticFact | Procedure, text: string): string { + return `${ENVELOPE_PREFIX}${JSON.stringify({ kind, record })}\n\n${text}\n`; +} + +function parseEnvelope( + body: string, + expectedKind: MemoryRecordKind, +): T | null { + const firstLine = body.split("\n", 1)[0]?.trim(); + if (!firstLine?.startsWith(ENVELOPE_PREFIX)) return null; + + const payload = firstLine.slice(ENVELOPE_PREFIX.length); + try { + const parsed = JSON.parse(payload) as MemoryRecordEnvelope; + return parsed.kind === expectedKind ? (parsed.record as T) : null; + } catch { + return null; + } +} + +function compactTitle(primary: string, fallback: string): string { + const value = primary.trim() || fallback; + return value.length <= 120 ? value : `${value.slice(0, 117)}...`; +} + +function clampScore(value: number): number { + if (!Number.isFinite(value)) return 0.5; + return Math.min(Math.max(value, 0), 1); +} + +function stableJson(value: unknown): string { + return JSON.stringify(value); +} diff --git a/src/memory/clawmem-runtime.ts b/src/memory/clawmem-runtime.ts new file mode 100644 index 0000000..a06655c --- /dev/null +++ b/src/memory/clawmem-runtime.ts @@ -0,0 +1,197 @@ +import type { Database } from "bun:sqlite"; + +export type ClawMemSearchResult = { + filepath: string; + displayPath: string; + title: string; + context: string | null; + hash: string; + docid: string; + collectionName: string; + modifiedAt: string; + bodyLength: number; + body?: string; + score: number; + source: "fts" | "vec"; + chunkPos?: number; + fragmentType?: string; + fragmentLabel?: string; +}; + +export type ClawMemIndexStatus = { + totalDocuments: number; + needsEmbedding: number; + hasVectorIndex: boolean; + collections: Array<{ name: string; documentCount: number }>; +}; + +export type ClawMemSaveMemoryParams = { + collection: string; + path: string; + title: string; + body: string; + contentType: string; + confidence?: number; + qualityScore?: number; + semanticPayload?: string; + topicKey?: string; +}; + +export type ClawMemSaveMemoryResult = { + action: "inserted" | "deduplicated" | "updated"; + docId: number; + duplicateCount?: number; + revisionCount?: number; +}; + +export type ClawMemStore = { + db: Database; + dbPath: string; + close: () => void; + ensureVecTable: (dimensions: number) => void; + getStatus: () => ClawMemIndexStatus; + searchFTS: ( + query: string, + limit?: number, + collectionId?: number, + collections?: string[], + dateRange?: { start: string; end: string }, + ) => ClawMemSearchResult[]; + searchVec: ( + query: string, + model: string, + limit?: number, + collectionId?: number, + collections?: string[], + dateRange?: { start: string; end: string }, + ) => Promise; + saveMemory: (params: ClawMemSaveMemoryParams) => ClawMemSaveMemoryResult; + insertEmbedding: ( + hash: string, + seq: number, + pos: number, + embedding: Float32Array, + model: string, + embeddedAt: string, + fragmentType?: string, + fragmentLabel?: string, + canonicalId?: string, + ) => void; + incrementAccessCount: (paths: string[]) => void; +}; + +export type ClawMemEnrichedResult = { + filepath: string; + displayPath: string; + title: string; + score: number; + body?: string; + contentType: string; + modifiedAt: string; + accessCount: number; + confidence: number; + qualityScore: number; + pinned: boolean; + context: string | null; + hash: string; + docid: string; + collectionName: string; + bodyLength: number; + source: "fts" | "vec"; + chunkPos?: number; + fragmentType?: string; + fragmentLabel?: string; + lastAccessedAt?: string | null; + duplicateCount: number; + revisionCount: number; +}; + +export type ClawMemScoredResult = ClawMemEnrichedResult & { + compositeScore: number; + recencyScore: number; +}; + +export type ClawMemRankedResult = { + file: string; + displayPath: string; + title: string; + body: string; + score: number; +}; + +export type ClawMemLlamaEmbedResult = { + embedding: number[]; + model: string; +}; + +export type ClawMemLlamaLike = { + embed: ( + text: string, + options?: { + model?: string; + isQuery?: boolean; + }, + ) => Promise; +}; + +type StoreModule = { + createStore: (dbPath?: string, opts?: { readonly?: boolean; busyTimeout?: number }) => ClawMemStore; +}; + +type SearchUtilsModule = { + enrichResults: (store: ClawMemStore, results: ClawMemSearchResult[], query: string) => ClawMemEnrichedResult[]; + reciprocalRankFusion: ( + resultLists: ClawMemRankedResult[][], + weights: number[], + k?: number, + ) => ClawMemRankedResult[]; + toRanked: (result: ClawMemSearchResult) => ClawMemRankedResult; +}; + +type MemoryModule = { + applyCompositeScoring: (results: ClawMemEnrichedResult[], query: string) => ClawMemScoredResult[]; +}; + +type LlmModule = { + formatDocForEmbedding: (text: string, title?: string) => string; + getDefaultLlamaCpp: () => ClawMemLlamaLike; + setDefaultLlamaCpp: (llm: ClawMemLlamaLike | null) => void; +}; + +const dynamicImport = new Function("specifier", "return import(specifier);") as (specifier: string) => Promise; + +let storeModulePromise: Promise | null = null; +let searchUtilsModulePromise: Promise | null = null; +let memoryModulePromise: Promise | null = null; +let llmModulePromise: Promise | null = null; + +export function loadClawMemStoreModule(): Promise { + storeModulePromise ??= dynamicImport("clawmem/src/store.ts"); + return storeModulePromise; +} + +export function loadClawMemSearchUtilsModule(): Promise { + searchUtilsModulePromise ??= dynamicImport("clawmem/src/search-utils.ts"); + return searchUtilsModulePromise; +} + +export function loadClawMemMemoryModule(): Promise { + memoryModulePromise ??= dynamicImport("clawmem/src/memory.ts"); + return memoryModulePromise; +} + +export function loadClawMemLlmModule(): Promise { + llmModulePromise ??= dynamicImport("clawmem/src/llm.ts"); + return llmModulePromise; +} + +export async function createClawMemStore( + dbPath?: string, + opts?: { readonly?: boolean; busyTimeout?: number }, +): Promise { + return (await loadClawMemStoreModule()).createStore(dbPath, opts); +} + +export async function setClawMemDefaultLlama(llm: ClawMemLlamaLike | null): Promise { + (await loadClawMemLlmModule()).setDefaultLlamaCpp(llm); +} diff --git a/src/memory/config.ts b/src/memory/config.ts index 195b652..c6a406e 100644 --- a/src/memory/config.ts +++ b/src/memory/config.ts @@ -7,23 +7,22 @@ const DEFAULT_CONFIG_PATH = "config/memory.yaml"; /** * Apply environment variable overrides for Docker and bare-metal compatibility. - * QDRANT_URL, OLLAMA_URL, and EMBEDDING_MODEL env vars take precedence over YAML config. + * CLAWMEM_STORE_PATH, CLAWMEM_EMBED_MODEL, and CLAWMEM_BUSY_TIMEOUT_MS + * env vars take precedence over YAML config. */ function applyEnvOverrides(config: MemoryConfig): MemoryConfig { - const qdrantUrl = process.env.QDRANT_URL; - const ollamaUrl = process.env.OLLAMA_URL; - const embeddingModel = process.env.EMBEDDING_MODEL; + const storePath = readEnv(process.env.CLAWMEM_STORE_PATH); + const embedModel = readEnv(process.env.CLAWMEM_EMBED_MODEL); + const busyTimeoutMs = readEnv(process.env.CLAWMEM_BUSY_TIMEOUT_MS); + const parsedBusyTimeout = busyTimeoutMs ? Number.parseInt(busyTimeoutMs, 10) : Number.NaN; return { ...config, - qdrant: { - ...config.qdrant, - ...(qdrantUrl ? { url: qdrantUrl } : {}), - }, - ollama: { - ...config.ollama, - ...(ollamaUrl ? { url: ollamaUrl } : {}), - ...(embeddingModel ? { model: embeddingModel } : {}), + clawmem: { + ...config.clawmem, + ...(storePath ? { store_path: storePath } : {}), + ...(embedModel ? { embed_model: embedModel } : {}), + ...(Number.isFinite(parsedBusyTimeout) ? { busy_timeout_ms: parsedBusyTimeout } : {}), }, }; } @@ -52,3 +51,9 @@ export function loadMemoryConfig(path?: string): MemoryConfig { return applyEnvOverrides(result.data); } + +function readEnv(value: string | undefined): string | undefined { + if (!value) return undefined; + const normalized = value.trim(); + return normalized === "" || normalized === "undefined" ? undefined : normalized; +} diff --git a/src/memory/embeddings.ts b/src/memory/embeddings.ts deleted file mode 100644 index f829c36..0000000 --- a/src/memory/embeddings.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { MemoryConfig } from "../config/types.ts"; -import type { SparseVector } from "./types.ts"; - -export class EmbeddingClient { - private baseUrl: string; - private model: string; - - constructor(config: MemoryConfig) { - this.baseUrl = config.ollama.url; - this.model = config.ollama.model; - } - - async embed(text: string): Promise { - const response = await fetch(`${this.baseUrl}/api/embed`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: this.model, input: text }), - }); - - if (!response.ok) { - const body = await response.text().catch(() => ""); - throw new Error( - `Ollama embedding failed (${response.status}): ${body || response.statusText}. ` + - `Is Ollama running at ${this.baseUrl} with model "${this.model}" pulled?`, - ); - } - - const data = (await response.json()) as { embeddings: number[][] }; - - if (!data.embeddings?.[0]) { - throw new Error("Ollama returned empty embeddings. Check that the model is loaded."); - } - - return data.embeddings[0]; - } - - async embedBatch(texts: string[]): Promise { - const response = await fetch(`${this.baseUrl}/api/embed`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: this.model, input: texts }), - }); - - if (!response.ok) { - const body = await response.text().catch(() => ""); - throw new Error(`Ollama batch embedding failed (${response.status}): ${body || response.statusText}`); - } - - const data = (await response.json()) as { embeddings: number[][] }; - - if (!data.embeddings || data.embeddings.length !== texts.length) { - throw new Error(`Ollama returned ${data.embeddings?.length ?? 0} embeddings for ${texts.length} inputs`); - } - - return data.embeddings; - } - - async isHealthy(): Promise { - try { - const response = await fetch(`${this.baseUrl}/api/tags`, { - signal: AbortSignal.timeout(3000), - }); - return response.ok; - } catch { - return false; - } - } -} - -/** - * Generate a BM25-style sparse vector from text. - * Tokenizes on word boundaries, computes term frequencies, - * and maps tokens to stable integer indices via a simple hash. - */ -export function textToSparseVector(text: string): SparseVector { - const tokens = text - .toLowerCase() - .replace(/[^\w\s]/g, " ") - .split(/\s+/) - .filter((t) => t.length > 1); - - if (tokens.length === 0) { - return { indices: [], values: [] }; - } - - const tf = new Map(); - for (const token of tokens) { - tf.set(token, (tf.get(token) ?? 0) + 1); - } - - const indices: number[] = []; - const values: number[] = []; - - for (const [token, count] of tf.entries()) { - indices.push(stableHash(token)); - values.push(count / tokens.length); - } - - return { indices, values }; -} - -/** - * Stable hash for token to sparse vector index mapping. - * Uses FNV-1a to produce a positive 32-bit integer. - */ -function stableHash(str: string): number { - let hash = 0x811c9dc5; - for (let i = 0; i < str.length; i++) { - hash ^= str.charCodeAt(i); - hash = (hash * 0x01000193) >>> 0; - } - return hash >>> 0; -} diff --git a/src/memory/episodic.ts b/src/memory/episodic.ts deleted file mode 100644 index 64c0674..0000000 --- a/src/memory/episodic.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type { MemoryConfig } from "../config/types.ts"; -import { type EmbeddingClient, textToSparseVector } from "./embeddings.ts"; -import type { QdrantClient } from "./qdrant-client.ts"; -import { calculateEpisodeRecallScore } from "./ranking.ts"; -import type { Episode, QdrantSearchResult, RecallOptions } from "./types.ts"; - -const COLLECTION_SCHEMA = { - vectors: { - summary: { size: 768, distance: "Cosine" }, - detail: { size: 768, distance: "Cosine" }, - }, - sparse_vectors: { - text_bm25: {}, - }, -} as const; - -const PAYLOAD_INDEXES: { field: string; type: "keyword" | "integer" | "float" }[] = [ - { field: "type", type: "keyword" }, - { field: "outcome", type: "keyword" }, - { field: "session_id", type: "keyword" }, - { field: "user_id", type: "keyword" }, - { field: "started_at", type: "integer" }, - { field: "ended_at", type: "integer" }, - { field: "importance", type: "float" }, - { field: "access_count", type: "integer" }, - { field: "tools_used", type: "keyword" }, - { field: "files_touched", type: "keyword" }, - { field: "parent_id", type: "keyword" }, -]; - -export class EpisodicStore { - private qdrant: QdrantClient; - private embedder: EmbeddingClient; - private collectionName: string; - - constructor(qdrant: QdrantClient, embedder: EmbeddingClient, config: MemoryConfig) { - this.qdrant = qdrant; - this.embedder = embedder; - this.collectionName = config.collections.episodes; - } - - async initialize(): Promise { - await this.qdrant.createCollection(this.collectionName, { - vectors: { ...COLLECTION_SCHEMA.vectors }, - sparse_vectors: { ...COLLECTION_SCHEMA.sparse_vectors }, - }); - - for (const index of PAYLOAD_INDEXES) { - await this.qdrant.createPayloadIndex(this.collectionName, index.field, index.type); - } - } - - async store(episode: Episode): Promise { - const [summaryVec, detailVec] = await this.embedder.embedBatch([episode.summary, episode.detail]); - - const combinedText = `${episode.summary} ${episode.detail} ${episode.lessons.join(" ")}`; - const sparse = textToSparseVector(combinedText); - - await this.qdrant.upsert(this.collectionName, [ - { - id: episode.id, - vector: { - summary: summaryVec, - detail: detailVec, - text_bm25: sparse, - }, - payload: { - type: episode.type, - summary: episode.summary, - detail: episode.detail, - parent_id: episode.parent_id, - session_id: episode.session_id, - user_id: episode.user_id, - tools_used: episode.tools_used, - files_touched: episode.files_touched, - outcome: episode.outcome, - outcome_detail: episode.outcome_detail, - lessons: episode.lessons, - started_at: new Date(episode.started_at).getTime(), - ended_at: new Date(episode.ended_at).getTime(), - duration_seconds: episode.duration_seconds, - importance: episode.importance, - access_count: episode.access_count, - last_accessed_at: episode.last_accessed_at, - decay_rate: episode.decay_rate, - }, - }, - ]); - - return episode.id; - } - - async recall(query: string, options?: RecallOptions): Promise { - const limit = options?.limit ?? 10; - const strategy = options?.strategy ?? "recency"; - - const queryVec = await this.embedder.embed(query); - const sparse = textToSparseVector(query); - - const filter = this.buildFilter(options); - - const results = await this.qdrant.search(this.collectionName, { - denseVector: queryVec, - denseVectorName: "summary", - sparseVector: sparse, - sparseVectorName: "text_bm25", - filter, - limit: limit * 2, - withPayload: true, - }); - - const scored = this.applyStrategy(results, strategy); - const topResults = scored.slice(0, limit); - - // Update access counts in background - this.updateAccessCounts(topResults.map((r) => r.id)).catch(() => {}); - - return topResults.map((r) => this.payloadToEpisode(r)); - } - - async updateAccessCount(id: string): Promise { - await this.qdrant.updatePayload(this.collectionName, id, { - access_count: { $inc: 1 }, - last_accessed_at: new Date().toISOString(), - }); - } - - private async updateAccessCounts(ids: string[]): Promise { - for (const id of ids) { - try { - await this.qdrant.updatePayload(this.collectionName, id, { - access_count: { $inc: 1 }, - last_accessed_at: new Date().toISOString(), - }); - } catch { - // Non-critical, best-effort - } - } - } - - private buildFilter(options?: RecallOptions): Record | undefined { - if (!options) return undefined; - - const must: Record[] = []; - - if (options.timeRange) { - must.push({ - key: "started_at", - range: { - gte: options.timeRange.from.getTime(), - lte: options.timeRange.to.getTime(), - }, - }); - } - - if (options.filters) { - for (const [key, value] of Object.entries(options.filters)) { - if (Array.isArray(value)) { - must.push({ key, match: { any: value } }); - } else { - must.push({ key, match: { value } }); - } - } - } - - if (must.length === 0) return undefined; - return { must }; - } - - private applyStrategy(results: QdrantSearchResult[], strategy: RecallOptions["strategy"]): QdrantSearchResult[] { - return results - .map((r) => { - return { - ...r, - score: calculateEpisodeRecallScore( - r.score, - { - importance: (r.payload.importance as number) ?? 0.5, - accessCount: (r.payload.access_count as number) ?? 0, - startedAt: (r.payload.started_at as number) ?? 0, - lastAccessedAt: (r.payload.last_accessed_at as string | undefined) ?? undefined, - decayRate: (r.payload.decay_rate as number) ?? 1, - }, - strategy, - ), - }; - }) - .sort((a, b) => b.score - a.score); - } - - private payloadToEpisode(result: QdrantSearchResult): Episode { - const p = result.payload; - return { - id: result.id, - type: (p.type as Episode["type"]) ?? "task", - summary: (p.summary as string) ?? "", - detail: (p.detail as string) ?? "", - parent_id: (p.parent_id as string | null) ?? null, - session_id: (p.session_id as string) ?? "", - user_id: (p.user_id as string) ?? "", - tools_used: (p.tools_used as string[]) ?? [], - files_touched: (p.files_touched as string[]) ?? [], - outcome: (p.outcome as Episode["outcome"]) ?? "success", - outcome_detail: (p.outcome_detail as string) ?? "", - lessons: (p.lessons as string[]) ?? [], - started_at: p.started_at ? new Date(p.started_at as number).toISOString() : "", - ended_at: p.ended_at ? new Date(p.ended_at as number).toISOString() : "", - duration_seconds: (p.duration_seconds as number) ?? 0, - importance: (p.importance as number) ?? 0.5, - access_count: (p.access_count as number) ?? 0, - last_accessed_at: (p.last_accessed_at as string) ?? "", - decay_rate: (p.decay_rate as number) ?? 1.0, - }; - } -} diff --git a/src/memory/procedural.ts b/src/memory/procedural.ts deleted file mode 100644 index db69d67..0000000 --- a/src/memory/procedural.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { MemoryConfig } from "../config/types.ts"; -import { type EmbeddingClient, textToSparseVector } from "./embeddings.ts"; -import type { QdrantClient } from "./qdrant-client.ts"; -import type { Procedure, QdrantSearchResult } from "./types.ts"; - -const COLLECTION_SCHEMA = { - vectors: { - description: { size: 768, distance: "Cosine" }, - }, - sparse_vectors: { - text_bm25: {}, - }, -} as const; - -const PAYLOAD_INDEXES: { field: string; type: "keyword" | "integer" | "float" }[] = [ - { field: "name", type: "keyword" }, - { field: "confidence", type: "float" }, - { field: "success_count", type: "integer" }, - { field: "failure_count", type: "integer" }, - { field: "last_used_at", type: "integer" }, -]; - -export class ProceduralStore { - private qdrant: QdrantClient; - private embedder: EmbeddingClient; - private collectionName: string; - - constructor(qdrant: QdrantClient, embedder: EmbeddingClient, config: MemoryConfig) { - this.qdrant = qdrant; - this.embedder = embedder; - this.collectionName = config.collections.procedures; - } - - async initialize(): Promise { - await this.qdrant.createCollection(this.collectionName, { - vectors: { ...COLLECTION_SCHEMA.vectors }, - sparse_vectors: { ...COLLECTION_SCHEMA.sparse_vectors }, - }); - - for (const index of PAYLOAD_INDEXES) { - await this.qdrant.createPayloadIndex(this.collectionName, index.field, index.type); - } - } - - async store(procedure: Procedure): Promise { - const embeddingText = `${procedure.description} ${procedure.trigger}`; - const descVec = await this.embedder.embed(embeddingText); - const sparse = textToSparseVector(embeddingText); - - await this.qdrant.upsert(this.collectionName, [ - { - id: procedure.id, - vector: { - description: descVec, - text_bm25: sparse, - }, - payload: { - name: procedure.name, - description: procedure.description, - trigger: procedure.trigger, - steps: procedure.steps, - preconditions: procedure.preconditions, - postconditions: procedure.postconditions, - parameters: procedure.parameters, - source_episode_ids: procedure.source_episode_ids, - success_count: procedure.success_count, - failure_count: procedure.failure_count, - last_used_at: new Date(procedure.last_used_at).getTime(), - confidence: procedure.confidence, - version: procedure.version, - }, - }, - ]); - - return procedure.id; - } - - async find(taskDescription: string): Promise { - const queryVec = await this.embedder.embed(taskDescription); - const sparse = textToSparseVector(taskDescription); - - const results = await this.qdrant.search(this.collectionName, { - denseVector: queryVec, - denseVectorName: "description", - sparseVector: sparse, - sparseVectorName: "text_bm25", - limit: 1, - withPayload: true, - }); - - if (results.length === 0 || results[0].score < 0.3) { - return null; - } - - return this.payloadToProcedure(results[0]); - } - - async updateOutcome(id: string, success: boolean): Promise { - const field = success ? "success_count" : "failure_count"; - await this.qdrant.updatePayload(this.collectionName, id, { - [field]: { $inc: 1 }, - last_used_at: Date.now(), - }); - } - - private payloadToProcedure(result: QdrantSearchResult): Procedure { - const p = result.payload; - return { - id: result.id, - name: (p.name as string) ?? "", - description: (p.description as string) ?? "", - trigger: (p.trigger as string) ?? "", - steps: (p.steps as Procedure["steps"]) ?? [], - preconditions: (p.preconditions as string[]) ?? [], - postconditions: (p.postconditions as string[]) ?? [], - parameters: (p.parameters as Procedure["parameters"]) ?? {}, - source_episode_ids: (p.source_episode_ids as string[]) ?? [], - success_count: (p.success_count as number) ?? 0, - failure_count: (p.failure_count as number) ?? 0, - last_used_at: p.last_used_at ? new Date(p.last_used_at as number).toISOString() : "", - confidence: (p.confidence as number) ?? 0.5, - version: (p.version as number) ?? 1, - }; - } -} diff --git a/src/memory/qdrant-client.ts b/src/memory/qdrant-client.ts deleted file mode 100644 index ecf599f..0000000 --- a/src/memory/qdrant-client.ts +++ /dev/null @@ -1,276 +0,0 @@ -import type { MemoryConfig } from "../config/types.ts"; -import type { QdrantPoint, QdrantSearchResult, SparseVector } from "./types.ts"; - -type VectorConfig = Record; -type SparseVectorConfig = Record; - -type CollectionSchema = { - vectors: VectorConfig; - sparse_vectors?: SparseVectorConfig; -}; - -type QdrantResponse = { - status?: string; - result?: unknown; - time?: number; -}; - -type QdrantQueryResponse = { - result?: { points?: QdrantScoredPoint[] }; -}; - -type QdrantScoredPoint = { - id: string | number; - score: number; - payload?: Record; -}; - -export class QdrantClient { - private baseUrl: string; - - constructor(config: MemoryConfig) { - this.baseUrl = config.qdrant.url; - } - - async createCollection(name: string, schema: CollectionSchema): Promise { - const existing = await this.collectionExists(name); - if (existing) return; - - const response = await this.request("PUT", `/collections/${name}`, schema); - - if (response.status !== "ok" && response.result !== true) { - throw new Error(`Failed to create collection "${name}": ${JSON.stringify(response)}`); - } - } - - async collectionExists(name: string): Promise { - try { - const response = await fetch(`${this.baseUrl}/collections/${name}`, { - signal: AbortSignal.timeout(5000), - }); - return response.ok; - } catch { - return false; - } - } - - async upsert(collection: string, points: QdrantPoint[]): Promise { - const qdrantPoints = points.map((p) => { - const vector: Record = {}; - - for (const [key, vec] of Object.entries(p.vector)) { - if (Array.isArray(vec)) { - vector[key] = vec; - } else { - vector[key] = { indices: vec.indices, values: vec.values }; - } - } - - return { - id: p.id, - vector, - payload: p.payload, - }; - }); - - await this.request("PUT", `/collections/${collection}/points?wait=true`, { - points: qdrantPoints, - }); - } - - async search( - collection: string, - options: { - denseVector?: number[]; - denseVectorName?: string; - sparseVector?: SparseVector; - sparseVectorName?: string; - filter?: Record; - limit?: number; - withPayload?: boolean; - }, - ): Promise { - const limit = options.limit ?? 10; - const withPayload = options.withPayload ?? true; - const hasDense = options.denseVector && options.denseVectorName; - const hasSparse = options.sparseVector && options.sparseVectorName && options.sparseVector.indices.length > 0; - - if (!hasDense && !hasSparse) { - return []; - } - - // Hybrid search with RRF if both vectors present - if (hasDense && hasSparse) { - return this.hybridSearch(collection, { - denseVector: options.denseVector as number[], - denseVectorName: options.denseVectorName as string, - sparseVector: options.sparseVector as SparseVector, - sparseVectorName: options.sparseVectorName as string, - filter: options.filter, - limit, - withPayload, - }); - } - - // Dense-only search - if (hasDense) { - return this.denseSearch(collection, { - vector: options.denseVector as number[], - vectorName: options.denseVectorName as string, - filter: options.filter, - limit, - withPayload, - }); - } - - // Sparse-only search - return this.sparseSearch(collection, { - vector: options.sparseVector as SparseVector, - vectorName: options.sparseVectorName as string, - filter: options.filter, - limit, - withPayload, - }); - } - - async deletePoint(collection: string, id: string): Promise { - await this.request("POST", `/collections/${collection}/points/delete`, { - points: [id], - }); - } - - async updatePayload(collection: string, id: string, payload: Record): Promise { - await this.request("POST", `/collections/${collection}/points/payload`, { - payload, - points: [id], - }); - } - - async createPayloadIndex( - collection: string, - fieldName: string, - fieldType: "keyword" | "integer" | "float" | "text", - ): Promise { - await this.request("PUT", `/collections/${collection}/index`, { - field_name: fieldName, - field_schema: fieldType, - }); - } - - async isHealthy(): Promise { - try { - const response = await fetch(`${this.baseUrl}/`, { - signal: AbortSignal.timeout(3000), - }); - return response.ok; - } catch { - return false; - } - } - - private async hybridSearch( - collection: string, - options: { - denseVector: number[]; - denseVectorName: string; - sparseVector: SparseVector; - sparseVectorName: string; - filter?: Record; - limit: number; - withPayload: boolean; - }, - ): Promise { - return this.queryPoints(collection, { - prefetch: [ - { query: options.denseVector, using: options.denseVectorName, limit: options.limit * 2 }, - { - query: { indices: options.sparseVector.indices, values: options.sparseVector.values }, - using: options.sparseVectorName, - limit: options.limit * 2, - }, - ], - query: { fusion: "rrf" }, - limit: options.limit, - with_payload: options.withPayload, - filter: options.filter, - }); - } - - private async denseSearch( - collection: string, - options: { - vector: number[]; - vectorName: string; - filter?: Record; - limit: number; - withPayload: boolean; - }, - ): Promise { - return this.queryPoints(collection, { - query: options.vector, - using: options.vectorName, - limit: options.limit, - with_payload: options.withPayload, - filter: options.filter, - }); - } - - private async sparseSearch( - collection: string, - options: { - vector: SparseVector; - vectorName: string; - filter?: Record; - limit: number; - withPayload: boolean; - }, - ): Promise { - return this.queryPoints(collection, { - query: { indices: options.vector.indices, values: options.vector.values }, - using: options.vectorName, - limit: options.limit, - with_payload: options.withPayload, - filter: options.filter, - }); - } - - private async queryPoints(collection: string, body: Record): Promise { - const filtered = { ...body }; - if (!filtered.filter) filtered.filter = undefined; - const response = (await this.request( - "POST", - `/collections/${collection}/points/query`, - filtered, - )) as QdrantQueryResponse; - return this.extractResults(response); - } - - private extractResults(response: QdrantQueryResponse): QdrantSearchResult[] { - const points = response.result?.points ?? []; - return points.map((p) => ({ - id: String(p.id), - score: p.score, - payload: p.payload ?? {}, - })); - } - - private async request( - method: string, - path: string, - body?: Record, - ): Promise> { - const response = await fetch(`${this.baseUrl}${path}`, { - method, - headers: { "Content-Type": "application/json" }, - body: body ? JSON.stringify(body) : undefined, - signal: AbortSignal.timeout(30000), - }); - - if (!response.ok) { - const text = await response.text().catch(() => ""); - throw new Error(`Qdrant ${method} ${path} failed (${response.status}): ${text || response.statusText}`); - } - - return (await response.json()) as QdrantResponse & Record; - } -} diff --git a/src/memory/semantic.ts b/src/memory/semantic.ts deleted file mode 100644 index 619d8e4..0000000 --- a/src/memory/semantic.ts +++ /dev/null @@ -1,195 +0,0 @@ -import type { MemoryConfig } from "../config/types.ts"; -import { type EmbeddingClient, textToSparseVector } from "./embeddings.ts"; -import type { QdrantClient } from "./qdrant-client.ts"; -import type { QdrantSearchResult, RecallOptions, SemanticFact } from "./types.ts"; - -const COLLECTION_SCHEMA = { - vectors: { - fact: { size: 768, distance: "Cosine" }, - }, - sparse_vectors: { - text_bm25: {}, - }, -} as const; - -const PAYLOAD_INDEXES: { field: string; type: "keyword" | "integer" | "float" }[] = [ - { field: "subject", type: "keyword" }, - { field: "predicate", type: "keyword" }, - { field: "category", type: "keyword" }, - { field: "confidence", type: "float" }, - { field: "valid_from", type: "integer" }, - { field: "valid_until", type: "integer" }, - { field: "version", type: "integer" }, - { field: "tags", type: "keyword" }, -]; - -const SIMILARITY_THRESHOLD = 0.85; - -export class SemanticStore { - private qdrant: QdrantClient; - private embedder: EmbeddingClient; - private collectionName: string; - - constructor(qdrant: QdrantClient, embedder: EmbeddingClient, config: MemoryConfig) { - this.qdrant = qdrant; - this.embedder = embedder; - this.collectionName = config.collections.semantic_facts; - } - - async initialize(): Promise { - await this.qdrant.createCollection(this.collectionName, { - vectors: { ...COLLECTION_SCHEMA.vectors }, - sparse_vectors: { ...COLLECTION_SCHEMA.sparse_vectors }, - }); - - for (const index of PAYLOAD_INDEXES) { - await this.qdrant.createPayloadIndex(this.collectionName, index.field, index.type); - } - } - - async store(fact: SemanticFact): Promise { - // Check for contradictions before storing - const contradictions = await this.findContradictions(fact); - - for (const existing of contradictions) { - await this.resolveContradiction(fact, existing); - } - - const factVec = await this.embedder.embed(fact.natural_language); - const sparse = textToSparseVector(`${fact.subject} ${fact.predicate} ${fact.object} ${fact.natural_language}`); - - await this.qdrant.upsert(this.collectionName, [ - { - id: fact.id, - vector: { - fact: factVec, - text_bm25: sparse, - }, - payload: { - subject: fact.subject, - predicate: fact.predicate, - object: fact.object, - natural_language: fact.natural_language, - source_episode_ids: fact.source_episode_ids, - confidence: fact.confidence, - valid_from: new Date(fact.valid_from).getTime(), - valid_until: fact.valid_until ? new Date(fact.valid_until).getTime() : null, - version: fact.version, - previous_version_id: fact.previous_version_id, - category: fact.category, - tags: fact.tags, - }, - }, - ]); - - return fact.id; - } - - async recall(query: string, options?: RecallOptions): Promise { - const limit = options?.limit ?? 20; - - const queryVec = await this.embedder.embed(query); - const sparse = textToSparseVector(query); - - // Default: only return currently-valid facts - const filter = this.buildFilter(options); - - const results = await this.qdrant.search(this.collectionName, { - denseVector: queryVec, - denseVectorName: "fact", - sparseVector: sparse, - sparseVectorName: "text_bm25", - filter, - limit, - withPayload: true, - }); - - const minScore = options?.minScore ?? 0; - return results.filter((r) => r.score >= minScore).map((r) => this.payloadToFact(r)); - } - - async findContradictions(newFact: SemanticFact): Promise { - // Search for facts with the same subject and predicate - const queryText = `${newFact.subject} ${newFact.predicate}`; - const queryVec = await this.embedder.embed(queryText); - - const results = await this.qdrant.search(this.collectionName, { - denseVector: queryVec, - denseVectorName: "fact", - filter: { - must: [{ key: "subject", match: { value: newFact.subject } }, { is_null: { key: "valid_until" } }], - }, - limit: 10, - withPayload: true, - }); - - return results - .filter((r) => { - if (r.id === newFact.id) return false; - if (r.score < SIMILARITY_THRESHOLD) return false; - const existingObject = r.payload.object as string; - return existingObject !== newFact.object; - }) - .map((r) => this.payloadToFact(r)); - } - - async resolveContradiction(newFact: SemanticFact, existingFact: SemanticFact): Promise { - // Newer fact with higher or equal confidence supersedes the old one - if (newFact.confidence >= existingFact.confidence) { - await this.qdrant.updatePayload(this.collectionName, existingFact.id, { - valid_until: new Date(newFact.valid_from).getTime(), - }); - } - } - - private buildFilter(options?: RecallOptions): Record | undefined { - const must: Record[] = []; - - // Default: only currently-valid facts - if (!options?.timeRange) { - must.push({ is_null: { key: "valid_until" } }); - } - - if (options?.timeRange) { - must.push({ - key: "valid_from", - range: { - gte: options.timeRange.from.getTime(), - lte: options.timeRange.to.getTime(), - }, - }); - } - - if (options?.filters) { - for (const [key, value] of Object.entries(options.filters)) { - if (Array.isArray(value)) { - must.push({ key, match: { any: value } }); - } else { - must.push({ key, match: { value } }); - } - } - } - - if (must.length === 0) return undefined; - return { must }; - } - - private payloadToFact(result: QdrantSearchResult): SemanticFact { - const p = result.payload; - return { - id: result.id, - subject: (p.subject as string) ?? "", - predicate: (p.predicate as string) ?? "", - object: (p.object as string) ?? "", - natural_language: (p.natural_language as string) ?? "", - source_episode_ids: (p.source_episode_ids as string[]) ?? [], - confidence: (p.confidence as number) ?? 0.5, - valid_from: p.valid_from ? new Date(p.valid_from as number).toISOString() : "", - valid_until: p.valid_until ? new Date(p.valid_until as number).toISOString() : null, - version: (p.version as number) ?? 1, - previous_version_id: (p.previous_version_id as string | null) ?? null, - category: (p.category as SemanticFact["category"]) ?? "domain_knowledge", - tags: (p.tags as string[]) ?? [], - }; - } -} diff --git a/src/memory/system.ts b/src/memory/system.ts index 8800674..58bc937 100644 --- a/src/memory/system.ts +++ b/src/memory/system.ts @@ -1,131 +1,433 @@ +import { mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; import type { MemoryConfig } from "../config/types.ts"; -import { EmbeddingClient } from "./embeddings.ts"; -import { EpisodicStore } from "./episodic.ts"; -import { ProceduralStore } from "./procedural.ts"; -import { QdrantClient } from "./qdrant-client.ts"; -import { SemanticStore } from "./semantic.ts"; +import { calculateEpisodeRecallScore } from "./ranking.ts"; +import { + episodeDocumentPath, + factDocumentPath, + parseEpisodeDocument, + parseFactDocument, + parseProcedureDocument, + procedureDocumentPath, + serializeEpisode, + serializeFact, + serializeProcedure, + type SerializedMemoryDocument, +} from "./clawmem-records.ts"; +import { + createClawMemStore, + loadClawMemLlmModule, + loadClawMemMemoryModule, + loadClawMemSearchUtilsModule, + type ClawMemScoredResult as ScoredResult, + type ClawMemSearchResult as SearchResult, + type ClawMemStore as Store, +} from "./clawmem-runtime.ts"; import type { ConsolidationResult, Episode, MemoryHealth, Procedure, RecallOptions, SemanticFact } from "./types.ts"; export class MemorySystem { - private qdrant: QdrantClient; - private embedder: EmbeddingClient; - private episodic: EpisodicStore; - private semantic: SemanticStore; - private procedural: ProceduralStore; + private readonly configured = true; + private readonly storePath: string; + private store: Store | null = null; private initialized = false; - constructor(config: MemoryConfig) { - this.qdrant = new QdrantClient(config); - this.embedder = new EmbeddingClient(config); - this.episodic = new EpisodicStore(this.qdrant, this.embedder, config); - this.semantic = new SemanticStore(this.qdrant, this.embedder, config); - this.procedural = new ProceduralStore(this.qdrant, this.embedder, config); + constructor(private readonly config: MemoryConfig) { + this.storePath = resolve(process.cwd(), config.clawmem.store_path); } async initialize(): Promise { - const health = await this.healthCheck(); - - if (!health.qdrant) { - console.warn("[memory] Qdrant is not available. Memory system running in degraded mode."); - return; - } - - if (!health.ollama) { - console.warn("[memory] Ollama is not available. Memory system running in degraded mode."); - return; - } - try { - await this.episodic.initialize(); - await this.semantic.initialize(); - await this.procedural.initialize(); + mkdirSync(dirname(this.storePath), { recursive: true }); + if (!process.env.CLAWMEM_EMBED_MODEL) { + process.env.CLAWMEM_EMBED_MODEL = this.config.clawmem.embed_model; + } + this.store = await createClawMemStore(this.storePath, { + busyTimeout: this.config.clawmem.busy_timeout_ms, + }); this.initialized = true; - console.log("[memory] Memory system initialized successfully."); + console.log(`[memory] ClawMem initialized at ${this.store.dbPath}.`); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); - console.error(`[memory] Failed to initialize: ${msg}`); + this.store = null; + this.initialized = false; + console.error(`[memory] Failed to initialize ClawMem: ${msg}`); } } async close(): Promise { + this.store?.close(); + this.store = null; this.initialized = false; } isReady(): boolean { - return this.initialized; + return this.initialized && this.store !== null; } async healthCheck(): Promise { - const [qdrant, ollama] = await Promise.all([this.qdrant.isHealthy(), this.embedder.isHealthy()]); - return { qdrant, ollama, configured: true }; + try { + if (this.store) { + this.store.getStatus(); + return { clawmem: true, configured: this.configured }; + } + + const probe = await createClawMemStore(this.storePath, { + busyTimeout: this.config.clawmem.busy_timeout_ms, + }); + probe.getStatus(); + probe.close(); + return { clawmem: true, configured: this.configured }; + } catch { + return { clawmem: false, configured: this.configured }; + } } - // Episodic memory async storeEpisode(episode: Episode): Promise { if (!this.initialized) return episode.id; - return this.episodic.store(episode); + await this.persistDocument(this.config.collections.episodes, serializeEpisode(episode)); + return this.readEpisode(episode.id)?.id ?? episode.id; } async recallEpisodes(query: string, options?: RecallOptions): Promise { if (!this.initialized) return []; - return this.episodic.recall(query, options); + const limit = options?.limit ?? 10; + const matches = await this.searchCollection(query, [this.config.collections.episodes], options, parseEpisodeDocument, limit * 3); + + return matches + .map(({ record, result }) => ({ + record, + score: calculateEpisodeRecallScore( + result.compositeScore, + { + importance: record.importance, + accessCount: record.access_count, + startedAt: record.started_at, + lastAccessedAt: record.last_accessed_at, + decayRate: record.decay_rate, + }, + options?.strategy, + ), + })) + .filter((entry) => options?.minScore == null || entry.score >= options.minScore) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map((entry) => entry.record); } - // Semantic memory async storeFact(fact: SemanticFact): Promise { if (!this.initialized) return fact.id; - return this.semantic.store(fact); + const contradictions = await this.findContradictions(fact); + for (const existing of contradictions) { + await this.resolveContradiction(fact, existing); + } + await this.persistDocument(this.config.collections.semantic_facts, serializeFact(fact)); + return this.readFact(fact.id)?.id ?? fact.id; } async recallFacts(query: string, options?: RecallOptions): Promise { if (!this.initialized) return []; - return this.semantic.recall(query, options); + const limit = options?.limit ?? 20; + const matches = await this.searchCollection( + query, + [this.config.collections.semantic_facts], + options, + parseFactDocument, + limit * 3, + ); + + return matches + .map(({ record, result }) => ({ record, score: result.compositeScore })) + .filter(({ record, score }) => { + if (options?.timeRange) { + const validFrom = Date.parse(record.valid_from); + if (Number.isFinite(validFrom)) { + if (validFrom < options.timeRange.from.getTime() || validFrom > options.timeRange.to.getTime()) { + return false; + } + } + } else if (record.valid_until) { + return false; + } + + return matchesFilters(record, options?.filters) && (options?.minScore == null || score >= options.minScore); + }) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map((entry) => entry.record); } async findContradictions(fact: SemanticFact): Promise { if (!this.initialized) return []; - return this.semantic.findContradictions(fact); + const facts = await this.recallFacts(`${fact.subject} ${fact.predicate}`, { + limit: 10, + filters: { subject: fact.subject }, + }); + + return facts.filter((existing) => { + if (existing.id === fact.id) return false; + if (existing.valid_until) return false; + return existing.subject === fact.subject && existing.predicate === fact.predicate && existing.object !== fact.object; + }); } async resolveContradiction(newFact: SemanticFact, existingFact: SemanticFact): Promise { if (!this.initialized) return; - return this.semantic.resolveContradiction(newFact, existingFact); + if (newFact.confidence < existingFact.confidence) return; + + const current = this.readFact(existingFact.id); + if (!current) return; + + await this.persistDocument(this.config.collections.semantic_facts, serializeFact({ + ...current, + valid_until: newFact.valid_from, + })); } - // Procedural memory async storeProcedure(procedure: Procedure): Promise { if (!this.initialized) return procedure.id; - return this.procedural.store(procedure); + await this.persistDocument(this.config.collections.procedures, serializeProcedure(procedure)); + return this.readProcedure(procedure.id)?.id ?? procedure.id; } async findProcedure(taskDescription: string): Promise { if (!this.initialized) return null; - return this.procedural.find(taskDescription); + const [bestMatch] = await this.searchCollection( + taskDescription, + [this.config.collections.procedures], + { limit: 5 }, + parseProcedureDocument, + 5, + ); + + if (!bestMatch || bestMatch.result.compositeScore < 0.2) { + return null; + } + + return bestMatch.record; } async updateProcedureOutcome(id: string, success: boolean): Promise { if (!this.initialized) return; - return this.procedural.updateOutcome(id, success); + const procedure = this.readProcedure(id); + if (!procedure) return; + + await this.persistDocument(this.config.collections.procedures, serializeProcedure({ + ...procedure, + success_count: procedure.success_count + (success ? 1 : 0), + failure_count: procedure.failure_count + (success ? 0 : 1), + last_used_at: new Date().toISOString(), + })); } - // Consolidation (delegates to consolidation.ts, called from index.ts) async consolidateSession(_sessionId: string): Promise { return { episodesCreated: 0, factsExtracted: 0, proceduresDetected: 0, durationMs: 0 }; } - getEpisodicStore(): EpisodicStore { - return this.episodic; + private async persistDocument(collection: string, document: SerializedMemoryDocument): Promise { + const store = this.requireStore(); + const result = store.saveMemory({ + collection, + path: document.path, + title: document.title, + body: document.body, + contentType: document.contentType, + confidence: document.confidence, + qualityScore: document.qualityScore, + semanticPayload: document.semanticPayload, + topicKey: document.topicKey, + }); + + if (result.action === "deduplicated") { + return; + } + + await this.indexDocument(collection, document); } - getSemanticStore(): SemanticStore { - return this.semantic; + private async indexDocument(collection: string, document: SerializedMemoryDocument): Promise { + const store = this.requireStore(); + const current = this.readStoredDocument(collection, document.path); + if (!current) return; + + const llmModule = await loadClawMemLlmModule(); + const llm = llmModule.getDefaultLlamaCpp(); + const embedding = await llm.embed(llmModule.formatDocForEmbedding(document.embeddingText, document.title), { + model: this.config.clawmem.embed_model, + }); + if (!embedding?.embedding || embedding.embedding.length === 0) { + return; + } + + store.ensureVecTable(embedding.embedding.length); + store.insertEmbedding( + current.hash, + 0, + 0, + new Float32Array(embedding.embedding), + embedding.model, + new Date().toISOString(), + "document", + document.title, + ); } - getProceduralStore(): ProceduralStore { - return this.procedural; + private async searchCollection( + query: string, + collections: string[], + options: RecallOptions | undefined, + parse: (body: string) => T | null, + candidateLimit: number, + ): Promise> { + const results = await this.hybridSearch(query, collections, candidateLimit, options?.timeRange); + const matches: Array<{ record: T; result: ScoredResult }> = []; + + for (const result of results) { + const record = parse(result.body ?? ""); + if (!record) continue; + if (!matchesFilters(record, options?.filters)) continue; + matches.push({ record, result }); + } + + return dedupeByJson(matches).slice(0, candidateLimit); } - getEmbedder(): EmbeddingClient { - return this.embedder; + private async hybridSearch( + query: string, + collections: string[], + limit: number, + timeRange?: { from: Date; to: Date }, + ): Promise { + const store = this.requireStore(); + const [{ enrichResults, reciprocalRankFusion, toRanked }, { applyCompositeScoring }] = await Promise.all([ + loadClawMemSearchUtilsModule(), + loadClawMemMemoryModule(), + ]); + const dateRange = timeRange + ? { + start: timeRange.from.toISOString(), + end: timeRange.to.toISOString(), + } + : undefined; + const searchLimit = Math.max(limit, 1); + const ftsResults = store.searchFTS(query, searchLimit * 3, undefined, collections, dateRange); + let vecResults: SearchResult[] = []; + + try { + vecResults = await store.searchVec(query, this.config.clawmem.embed_model, searchLimit * 3, undefined, collections, dateRange); + } catch { + vecResults = []; + } + + if (ftsResults.length === 0 && vecResults.length === 0) { + return []; + } + + const rankedLists = [ftsResults.map(toRanked)]; + const weights = [1]; + if (vecResults.length > 0) { + rankedLists.push(vecResults.map(toRanked)); + weights.push(1.15); + } + + const fused = reciprocalRankFusion(rankedLists, weights); + const rawByFile = new Map(); + for (const result of [...ftsResults, ...vecResults]) { + const current = rawByFile.get(result.filepath); + if (!current || result.score > current.score) { + rawByFile.set(result.filepath, result); + } + } + + const fusedResults = fused + .map((result) => { + const raw = rawByFile.get(result.file); + return raw ? { ...raw, score: result.score } : null; + }) + .filter((result): result is SearchResult => result !== null); + + const scored = applyCompositeScoring(enrichResults(store, fusedResults, query), query).slice(0, searchLimit); + if (scored.length > 0) { + store.incrementAccessCount(scored.map((result) => result.displayPath)); + } + + return scored; } + + private readEpisode(id: string): Episode | null { + const stored = this.readStoredDocument(this.config.collections.episodes, episodeDocumentPath(id)); + return stored ? parseEpisodeDocument(stored.body) : null; + } + + private readFact(id: string): SemanticFact | null { + const stored = this.readStoredDocument(this.config.collections.semantic_facts, factDocumentPath(id)); + return stored ? parseFactDocument(stored.body) : null; + } + + private readProcedure(id: string): Procedure | null { + const stored = this.readStoredDocument(this.config.collections.procedures, procedureDocumentPath(id)); + return stored ? parseProcedureDocument(stored.body) : null; + } + + private readStoredDocument(collection: string, path: string): { hash: string; body: string } | null { + const store = this.requireStore(); + return store.db + .prepare( + ` + SELECT d.hash, c.doc AS body + FROM documents d + JOIN content c ON c.hash = d.hash + WHERE d.collection = ? AND d.path = ? AND d.active = 1 + LIMIT 1 + `, + ) + .get(collection, path) as { hash: string; body: string } | null; + } + + private requireStore(): Store { + if (!this.store) { + throw new Error("Memory system is not initialized."); + } + return this.store; + } +} + +function dedupeByJson(items: T[]): T[] { + const seen = new Set(); + const deduped: T[] = []; + + for (const item of items) { + const key = JSON.stringify(item); + if (seen.has(key)) continue; + seen.add(key); + deduped.push(item); + } + + return deduped; +} + +function matchesFilters(record: unknown, filters?: Record): boolean { + if (!filters) return true; + if (!record || typeof record !== "object") return false; + + for (const [key, expected] of Object.entries(filters)) { + const actual = (record as Record)[key]; + + if (Array.isArray(expected)) { + if (Array.isArray(actual)) { + if (!expected.some((value) => actual.includes(value))) return false; + } else if (!expected.includes(actual)) { + return false; + } + continue; + } + + if (Array.isArray(actual)) { + if (!actual.includes(expected)) return false; + continue; + } + + if (actual !== expected) return false; + } + + return true; } diff --git a/src/memory/types.ts b/src/memory/types.ts index 8e80072..a2c7653 100644 --- a/src/memory/types.ts +++ b/src/memory/types.ts @@ -82,25 +82,7 @@ export type ConsolidationResult = { durationMs: number; }; -export type SparseVector = { - indices: number[]; - values: number[]; -}; - -export type QdrantPoint = { - id: string; - vector: Record; - payload: Record; -}; - -export type QdrantSearchResult = { - id: string; - score: number; - payload: Record; -}; - export type MemoryHealth = { - qdrant: boolean; - ollama: boolean; + clawmem: boolean; configured: boolean; }; diff --git a/src/secrets/crypto.ts b/src/secrets/crypto.ts index 540fc0f..455c9dd 100644 --- a/src/secrets/crypto.ts +++ b/src/secrets/crypto.ts @@ -20,7 +20,7 @@ let cachedKey: Buffer | null = null; export function getEncryptionKey(): Buffer { if (cachedKey) return cachedKey; - const envKey = process.env.SECRET_ENCRYPTION_KEY; + const envKey = readEnv(process.env.SECRET_ENCRYPTION_KEY); if (envKey) { const buf = Buffer.from(envKey, "hex"); if (buf.length !== KEY_LENGTH) { @@ -82,3 +82,9 @@ export function decryptSecret(encrypted: string, iv: string, authTag: string): s export function resetKeyCache(): void { cachedKey = null; } + +function readEnv(value: string | undefined): string | undefined { + if (!value) return undefined; + const normalized = value.trim(); + return normalized === "" || normalized === "undefined" ? undefined : normalized; +} diff --git a/tsconfig.json b/tsconfig.json index 9c50811..ddae8ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.d.ts"], "exclude": ["node_modules", "dist"] }