diff --git a/package-lock.json b/package-lock.json index f1a596e1..6f70d760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/express": "^4.17.21", "@types/node": "^20.0.0", "esbuild": "^0.20.0", + "tsx": "^4.20.6", "typescript": "^5.3.0" }, "engines": { @@ -338,6 +339,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", @@ -355,6 +373,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", @@ -372,6 +407,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", @@ -2491,6 +2543,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-uri": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", @@ -3865,6 +3930,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -4454,6 +4529,459 @@ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", "license": "Apache-2.0" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "license": "Apache-2.0", diff --git a/package.json b/package.json index 2f591d00..d57047fb 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@types/express": "^4.17.21", "@types/node": "^20.0.0", "esbuild": "^0.20.0", + "tsx": "^4.20.6", "typescript": "^5.3.0" }, "files": [ diff --git a/plugin/scripts/search-server.js b/plugin/scripts/search-server.js index bf9ee89e..d2da7499 100755 --- a/plugin/scripts/search-server.js +++ b/plugin/scripts/search-server.js @@ -69,7 +69,7 @@ var Sl=Object.create;var Ia=Object.defineProperty;var xl=Object.getOwnPropertyDe INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes) VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes); END; - `),console.error("[SessionSearch] FTS5 tables created successfully")}catch(e){console.error("[SessionSearch] FTS migration error:",e.message)}}escapeFTS5(e){return e}buildFilterClause(e,t,s="o"){let r=[];if(e.project&&(r.push(`${s}.project = ?`),t.push(e.project)),e.type)if(Array.isArray(e.type)){let n=e.type.map(()=>"?").join(",");r.push(`${s}.type IN (${n})`),t.push(...e.type)}else r.push(`${s}.type = ?`),t.push(e.type);if(e.dateRange){let{start:n,end:i}=e.dateRange;if(n){let o=typeof n=="number"?n:new Date(n).getTime();r.push(`${s}.created_at_epoch >= ?`),t.push(o)}if(i){let o=typeof i=="number"?i:new Date(i).getTime();r.push(`${s}.created_at_epoch <= ?`),t.push(o)}}if(e.concepts){let n=Array.isArray(e.concepts)?e.concepts:[e.concepts],i=n.map(()=>`EXISTS (SELECT 1 FROM json_each(${s}.concepts) WHERE value = ?)`);i.length>0&&(r.push(`(${i.join(" OR ")})`),t.push(...n))}if(e.files){let n=Array.isArray(e.files)?e.files:[e.files],i=n.map(()=>`( + `),console.error("[SessionSearch] FTS5 tables created successfully")}catch(e){console.error("[SessionSearch] FTS migration error:",e.message)}}escapeFTS5(e){return`"${e.replace(/"/g,'""')}"`}buildFilterClause(e,t,s="o"){let r=[];if(e.project&&(r.push(`${s}.project = ?`),t.push(e.project)),e.type)if(Array.isArray(e.type)){let n=e.type.map(()=>"?").join(",");r.push(`${s}.type IN (${n})`),t.push(...e.type)}else r.push(`${s}.type = ?`),t.push(e.type);if(e.dateRange){let{start:n,end:i}=e.dateRange;if(n){let o=typeof n=="number"?n:new Date(n).getTime();r.push(`${s}.created_at_epoch >= ?`),t.push(o)}if(i){let o=typeof i=="number"?i:new Date(i).getTime();r.push(`${s}.created_at_epoch <= ?`),t.push(o)}}if(e.concepts){let n=Array.isArray(e.concepts)?e.concepts:[e.concepts],i=n.map(()=>`EXISTS (SELECT 1 FROM json_each(${s}.concepts) WHERE value = ?)`);i.length>0&&(r.push(`(${i.join(" OR ")})`),t.push(...n))}if(e.files){let n=Array.isArray(e.files)?e.files:[e.files],i=n.map(()=>`( EXISTS (SELECT 1 FROM json_each(${s}.files_read) WHERE value LIKE ?) OR EXISTS (SELECT 1 FROM json_each(${s}.files_modified) WHERE value LIKE ?) )`);i.length>0&&(r.push(`(${i.join(" OR ")})`),n.forEach(o=>{t.push(`%${o}%`,`%${o}%`)}))}return r.length>0?r.join(" AND "):""}buildOrderClause(e="relevance",t=!0,s="observations_fts"){switch(e){case"relevance":return t?`ORDER BY ${s}.rank ASC`:"ORDER BY o.created_at_epoch DESC";case"date_desc":return"ORDER BY o.created_at_epoch DESC";case"date_asc":return"ORDER BY o.created_at_epoch ASC";default:return"ORDER BY o.created_at_epoch DESC"}}searchObservations(e,t={}){let s=[],{limit:r=50,offset:n=0,orderBy:i="relevance",...o}=t,c=this.escapeFTS5(e);s.push(c);let u=this.buildFilterClause(o,s,"o"),p=u?`AND ${u}`:"",m=this.buildOrderClause(i,!0),E=` diff --git a/src/services/sqlite/SessionSearch.ts b/src/services/sqlite/SessionSearch.ts index 8389eb45..2eee80a5 100644 --- a/src/services/sqlite/SessionSearch.ts +++ b/src/services/sqlite/SessionSearch.ts @@ -136,12 +136,19 @@ export class SessionSearch { /** * Escape FTS5 special characters in user input + * + * FTS5 uses double quotes for phrase searches and treats certain characters + * as operators (*, AND, OR, NOT, parentheses, etc.). To prevent injection, + * we wrap user input in double quotes and escape internal quotes by doubling them. + * This converts any user input into a safe phrase search. + * + * @param text - User input to escape for FTS5 MATCH queries + * @returns Safely escaped FTS5 query string */ private escapeFTS5(text: string): string { - // FTS5 special characters: " * ( ) AND OR NOT - // For safety, we'll wrap the entire query in quotes for phrase search - // or let advanced users pass boolean operators directly - return text; + // Escape internal double quotes by doubling them (FTS5 standard) + // Then wrap the entire string in double quotes for phrase search + return `"${text.replace(/"/g, '""')}"`; } /** diff --git a/tests/session-search.test.ts b/tests/session-search.test.ts new file mode 100644 index 00000000..ede04c2a --- /dev/null +++ b/tests/session-search.test.ts @@ -0,0 +1,332 @@ +import { test, describe } from 'node:test'; +import assert from 'node:assert'; +import Database from 'better-sqlite3'; +import { SessionSearch } from '../src/services/sqlite/SessionSearch'; +import fs from 'fs'; +import path from 'path'; + +const TEST_DB_DIR = '/tmp/claude-mem-test'; +const TEST_DB_PATH = path.join(TEST_DB_DIR, 'test.db'); + +describe('SessionSearch FTS5 Injection Tests', () => { + let search: SessionSearch; + let db: Database.Database; + + // Setup test database before each test + function setupTestDB() { + // Clean up any existing test database + if (fs.existsSync(TEST_DB_DIR)) { + fs.rmSync(TEST_DB_DIR, { recursive: true, force: true }); + } + fs.mkdirSync(TEST_DB_DIR, { recursive: true }); + + // Create database with required schema + db = new Database(TEST_DB_PATH); + db.pragma('journal_mode = WAL'); + + // Create minimal schema needed for search tests + // Note: Using claude_session_id to match SessionSearch expectations + db.exec(` + CREATE TABLE sdk_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + claude_session_id TEXT UNIQUE NOT NULL, + project TEXT NOT NULL, + started_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)) + ); + + CREATE TABLE observations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + claude_session_id TEXT NOT NULL, + prompt_number INTEGER DEFAULT 1, + type TEXT NOT NULL, + title TEXT, + subtitle TEXT, + narrative TEXT, + text TEXT, + facts TEXT, + concepts TEXT, + files_read TEXT, + files_modified TEXT, + project TEXT, + created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)), + FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id) + ); + + CREATE TABLE session_summaries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + claude_session_id TEXT NOT NULL, + prompt_number INTEGER DEFAULT 1, + request TEXT, + investigated TEXT, + learned TEXT, + completed TEXT, + next_steps TEXT, + notes TEXT, + files_read TEXT, + files_edited TEXT, + project TEXT, + created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)), + FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id) + ); + + CREATE TABLE user_prompts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + claude_session_id TEXT NOT NULL, + prompt_number INTEGER DEFAULT 1, + prompt_text TEXT NOT NULL, + created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)), + FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id) + ); + + -- Create FTS5 tables manually + CREATE VIRTUAL TABLE observations_fts USING fts5( + title, + subtitle, + narrative, + text, + facts, + concepts, + content='observations', + content_rowid='id' + ); + + CREATE VIRTUAL TABLE session_summaries_fts USING fts5( + request, + investigated, + learned, + completed, + next_steps, + notes, + content='session_summaries', + content_rowid='id' + ); + + CREATE VIRTUAL TABLE user_prompts_fts USING fts5( + prompt_text, + content='user_prompts', + content_rowid='id' + ); + + -- Create triggers for observations + CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN + INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts) + VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts); + END; + + CREATE TRIGGER observations_ad AFTER DELETE ON observations BEGIN + INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts) + VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts); + END; + + CREATE TRIGGER observations_au AFTER UPDATE ON observations BEGIN + INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts) + VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts); + INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts) + VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts); + END; + + -- Create triggers for session_summaries + CREATE TRIGGER session_summaries_ai AFTER INSERT ON session_summaries BEGIN + INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes) + VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes); + END; + + CREATE TRIGGER session_summaries_ad AFTER DELETE ON session_summaries BEGIN + INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes) + VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes); + END; + + CREATE TRIGGER session_summaries_au AFTER UPDATE ON session_summaries BEGIN + INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes) + VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes); + INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes) + VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes); + END; + + -- Create triggers for user_prompts + CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN + INSERT INTO user_prompts_fts(rowid, prompt_text) + VALUES (new.id, new.prompt_text); + END; + + CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN + INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text) + VALUES('delete', old.id, old.prompt_text); + END; + + CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN + INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text) + VALUES('delete', old.id, old.prompt_text); + INSERT INTO user_prompts_fts(rowid, prompt_text) + VALUES (new.id, new.prompt_text); + END; + `); + + db.close(); + + // Create SessionSearch instance + return new SessionSearch(TEST_DB_PATH); + } + + function teardownTestDB() { + if (search) { + search.close(); + search = null; + } + if (fs.existsSync(TEST_DB_DIR)) { + fs.rmSync(TEST_DB_DIR, { recursive: true, force: true }); + } + } + + test('should escape double quotes in search queries', () => { + search = setupTestDB(); + + // Insert test data + const db = new Database(TEST_DB_PATH); + db.exec(` + INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-1', 'test-project'); + INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project) + VALUES ('test-session-1', 1, 'feature', 'Test observation', 'A test "quoted" narrative', 'Some text', '[]', '[]', '[]', '[]', 'test-project'); + `); + db.close(); + + // Test query with double quotes - should not cause injection + const maliciousQuery = 'test" OR 1=1 --'; + + // This should not throw an error and should search safely + const results = search.searchObservations(maliciousQuery); + + // With proper escaping, this should return 0 results (no match for the literal string) + // Without escaping, it could match everything due to OR 1=1 + assert.strictEqual(Array.isArray(results), true, 'Should return an array'); + + teardownTestDB(); + }); + + test('should handle FTS5 special operators safely', () => { + search = setupTestDB(); + + // Insert test data + const db = new Database(TEST_DB_PATH); + db.exec(` + INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-2', 'test-project'); + INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project) + VALUES ('test-session-2', 1, 'feature', 'Security feature', 'Implements security', 'Authentication system', '[]', '[]', '[]', '[]', 'test-project'); + `); + db.close(); + + // Test queries with FTS5 operators that should be escaped + const testQueries = [ + 'AND OR NOT', // Boolean operators + '(parentheses)', // Grouping + 'asterisk*', // Wildcard + 'column:value', // Column filter attempt + ]; + + testQueries.forEach(query => { + // Should not throw an error + const results = search.searchObservations(query); + assert.strictEqual(Array.isArray(results), true, `Should return array for query: ${query}`); + }); + + teardownTestDB(); + }); + + test('should find exact phrase matches when properly escaped', () => { + search = setupTestDB(); + + // Insert test data + const db = new Database(TEST_DB_PATH); + db.exec(` + INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-3', 'test-project'); + INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project) + VALUES ('test-session-3', 1, 'feature', 'Hello world', 'This is a hello world example', 'Hello world program', '[]', '[]', '[]', '[]', 'test-project'); + INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project) + VALUES ('test-session-3', 2, 'feature', 'Goodbye moon', 'This is something else', 'Different content', '[]', '[]', '[]', '[]', 'test-project'); + `); + db.close(); + + // Search for exact phrase + const results = search.searchObservations('hello world'); + + assert.strictEqual(Array.isArray(results), true, 'Should return an array'); + assert.ok(results.length > 0, 'Should find at least one result'); + assert.ok( + results.some(r => r.title?.toLowerCase().includes('hello') || r.narrative?.toLowerCase().includes('hello')), + 'Should find observation with "hello"' + ); + + teardownTestDB(); + }); + + test('should handle empty and special character queries safely', () => { + search = setupTestDB(); + + // Insert test data + const db = new Database(TEST_DB_PATH); + db.exec(` + INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-4', 'test-project'); + INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project) + VALUES ('test-session-4', 1, 'feature', 'Test', 'Test observation', 'Test content', '[]', '[]', '[]', '[]', 'test-project'); + `); + db.close(); + + // Test edge cases + const edgeCases = [ + '""', // Empty quoted string + ' ', // Whitespace only + '!!!', // Special characters + '@#$%', // More special characters + ]; + + edgeCases.forEach(query => { + // Should not throw an error + const results = search.searchObservations(query); + assert.strictEqual(Array.isArray(results), true, `Should return array for edge case: "${query}"`); + }); + + teardownTestDB(); + }); + + test('should search session summaries safely', () => { + search = setupTestDB(); + + // Insert test data + const db = new Database(TEST_DB_PATH); + db.exec(` + INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-5', 'test-project'); + INSERT INTO session_summaries (claude_session_id, prompt_number, request, investigated, learned, completed, next_steps, notes, files_read, files_edited, project) + VALUES ('test-session-5', 1, 'Implement feature', 'Looked into options', 'Learned new approach', 'Completed task', 'Next: testing', 'Notes here', '[]', '[]', 'test-project'); + `); + db.close(); + + // Test with potential injection + const maliciousQuery = 'feature" OR type:*'; + const results = search.searchSessions(maliciousQuery); + + assert.strictEqual(Array.isArray(results), true, 'Should return an array'); + + teardownTestDB(); + }); + + test('should search user prompts safely', () => { + search = setupTestDB(); + + // Insert test data + const db = new Database(TEST_DB_PATH); + db.exec(` + INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-6', 'test-project'); + INSERT INTO user_prompts (claude_session_id, prompt_number, prompt_text) + VALUES ('test-session-6', 1, 'Please implement authentication'); + `); + db.close(); + + // Test with potential injection + const maliciousQuery = 'authentication" AND request:*'; + const results = search.searchUserPrompts(maliciousQuery); + + assert.strictEqual(Array.isArray(results), true, 'Should return an array'); + + teardownTestDB(); + }); +});