feat(09-01): install @libsql/client, schema, DB init, path helpers

- Install @libsql/client@^0.17.2 to server package
- Create skill-registry-schema.ts with 4 sqliteTable definitions (skills, skillVersions, skillFiles, communityRatings)
- Create skill-registry-db.ts with lazy singleton getSkillRegistryDb() and resetSkillRegistryDb()
- Add resolveSkillRegistryDbPath() and resolveSkillCacheDir() to home-paths.ts
- Add skill-registry-schema.test.ts with 8 passing tests (TDD green)
This commit is contained in:
Mikkel Georgsen 2026-04-01 01:00:25 +02:00
parent ade26c0cc2
commit 16ceef77d2
6 changed files with 514 additions and 7 deletions

264
pnpm-lock.yaml generated
View file

@ -78,7 +78,7 @@ importers:
version: 17.3.1
drizzle-orm:
specifier: 0.38.4
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
version: 0.38.4(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
embedded-postgres:
specifier: ^18.1.0-beta.16
version: 18.1.0-beta.16(patch_hash=55uhvnotpqyiy37rn3pqpukhei)
@ -236,7 +236,7 @@ importers:
version: link:../shared
drizzle-orm:
specifier: ^0.38.4
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
version: 0.38.4(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
embedded-postgres:
specifier: ^18.1.0-beta.16
version: 18.1.0-beta.16(patch_hash=55uhvnotpqyiy37rn3pqpukhei)
@ -449,6 +449,9 @@ importers:
'@aws-sdk/client-s3':
specifier: ^3.888.0
version: 3.994.0
'@libsql/client':
specifier: ^0.17.2
version: 0.17.2
'@paperclipai/adapter-claude-local':
specifier: workspace:*
version: link:../packages/adapters/claude-local
@ -490,7 +493,7 @@ importers:
version: 3.0.1(ajv@8.18.0)
better-auth:
specifier: 1.4.18
version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0))
version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0))
chokidar:
specifier: ^4.0.3
version: 4.0.3
@ -505,7 +508,7 @@ importers:
version: 17.3.1
drizzle-orm:
specifier: ^0.38.4
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
version: 0.38.4(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
embedded-postgres:
specifier: ^18.1.0-beta.16
version: 18.1.0-beta.16(patch_hash=55uhvnotpqyiy37rn3pqpukhei)
@ -2017,6 +2020,63 @@ packages:
'@lezer/yaml@1.0.4':
resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==}
'@libsql/client@0.17.2':
resolution: {integrity: sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q==}
'@libsql/core@0.17.2':
resolution: {integrity: sha512-L8qv12HZ/jRBcETVR3rscP0uHNxh+K3EABSde6scCw7zfOdiLqO3MAkJaeE1WovPsjXzsN/JBoZED4+7EZVT3g==}
'@libsql/darwin-arm64@0.5.29':
resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==}
cpu: [arm64]
os: [darwin]
'@libsql/darwin-x64@0.5.29':
resolution: {integrity: sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==}
cpu: [x64]
os: [darwin]
'@libsql/hrana-client@0.9.0':
resolution: {integrity: sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw==}
'@libsql/isomorphic-ws@0.1.5':
resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==}
'@libsql/linux-arm-gnueabihf@0.5.29':
resolution: {integrity: sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==}
cpu: [arm]
os: [linux]
'@libsql/linux-arm-musleabihf@0.5.29':
resolution: {integrity: sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==}
cpu: [arm]
os: [linux]
'@libsql/linux-arm64-gnu@0.5.29':
resolution: {integrity: sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==}
cpu: [arm64]
os: [linux]
'@libsql/linux-arm64-musl@0.5.29':
resolution: {integrity: sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==}
cpu: [arm64]
os: [linux]
'@libsql/linux-x64-gnu@0.5.29':
resolution: {integrity: sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==}
cpu: [x64]
os: [linux]
'@libsql/linux-x64-musl@0.5.29':
resolution: {integrity: sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==}
cpu: [x64]
os: [linux]
'@libsql/win32-x64-msvc@0.5.29':
resolution: {integrity: sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==}
cpu: [x64]
os: [win32]
'@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
@ -2037,6 +2097,9 @@ packages:
'@mermaid-js/parser@1.0.0':
resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==}
'@neon-rs/load@0.0.4':
resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
'@noble/ciphers@2.1.1':
resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==}
engines: {node: '>= 20.19.0'}
@ -3853,6 +3916,9 @@ packages:
engines: {node: '>=20'}
hasBin: true
cross-fetch@4.1.0:
resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -4033,6 +4099,10 @@ packages:
dagre-d3-es@7.0.13:
resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
data-urls@7.0.0:
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@ -4096,6 +4166,10 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@ -4389,6 +4463,10 @@ packages:
picomatch:
optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
finalhandler@2.1.1:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'}
@ -4401,6 +4479,10 @@ packages:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
formidable@3.5.4:
resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
engines: {node: '>=14.0.0'}
@ -4599,6 +4681,9 @@ packages:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -4664,6 +4749,11 @@ packages:
engines: {node: '>=16'}
hasBin: true
libsql@0.5.29:
resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==}
cpu: [x64, arm64, wasm32, arm]
os: [darwin, linux, win32]
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
@ -5029,6 +5119,24 @@ packages:
next-tick@1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@ -5213,6 +5321,9 @@ packages:
process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
promise-limit@2.7.0:
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@ -5628,6 +5739,9 @@ packages:
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
engines: {node: '>=16'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@6.0.0:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'}
@ -5906,6 +6020,13 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@8.0.1:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'}
@ -5918,6 +6039,9 @@ packages:
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@ -7665,6 +7789,68 @@ snapshots:
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@libsql/client@0.17.2':
dependencies:
'@libsql/core': 0.17.2
'@libsql/hrana-client': 0.9.0
js-base64: 3.7.8
libsql: 0.5.29
promise-limit: 2.7.0
transitivePeerDependencies:
- bufferutil
- encoding
- utf-8-validate
'@libsql/core@0.17.2':
dependencies:
js-base64: 3.7.8
'@libsql/darwin-arm64@0.5.29':
optional: true
'@libsql/darwin-x64@0.5.29':
optional: true
'@libsql/hrana-client@0.9.0':
dependencies:
'@libsql/isomorphic-ws': 0.1.5
cross-fetch: 4.1.0
js-base64: 3.7.8
node-fetch: 3.3.2
transitivePeerDependencies:
- bufferutil
- encoding
- utf-8-validate
'@libsql/isomorphic-ws@0.1.5':
dependencies:
'@types/ws': 8.18.1
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@libsql/linux-arm-gnueabihf@0.5.29':
optional: true
'@libsql/linux-arm-musleabihf@0.5.29':
optional: true
'@libsql/linux-arm64-gnu@0.5.29':
optional: true
'@libsql/linux-arm64-musl@0.5.29':
optional: true
'@libsql/linux-x64-gnu@0.5.29':
optional: true
'@libsql/linux-x64-musl@0.5.29':
optional: true
'@libsql/win32-x64-msvc@0.5.29':
optional: true
'@marijn/find-cluster-break@1.0.2': {}
'@mdxeditor/editor@3.52.4(@codemirror/language@6.12.1)(@lezer/highlight@1.2.3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.29)':
@ -7744,6 +7930,8 @@ snapshots:
dependencies:
langium: 4.2.1
'@neon-rs/load@0.0.4': {}
'@noble/ciphers@2.1.1': {}
'@noble/hashes@1.8.0': {}
@ -9446,7 +9634,7 @@ snapshots:
baseline-browser-mapping@2.9.19: {}
better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)):
better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)):
dependencies:
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
@ -9462,7 +9650,7 @@ snapshots:
zod: 4.3.6
optionalDependencies:
drizzle-kit: 0.31.9
drizzle-orm: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
drizzle-orm: 0.38.4(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
pg: 8.18.0
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
@ -9669,6 +9857,12 @@ snapshots:
'@epic-web/invariant': 1.0.0
cross-spawn: 7.0.6
cross-fetch@4.1.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -9880,6 +10074,8 @@ snapshots:
d3: 7.9.0
lodash-es: 4.17.23
data-uri-to-buffer@4.0.1: {}
data-urls@7.0.0(@noble/hashes@2.0.1):
dependencies:
whatwg-mimetype: 5.0.0
@ -9926,6 +10122,8 @@ snapshots:
dequal@2.0.3: {}
detect-libc@2.0.2: {}
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
@ -9971,9 +10169,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4):
drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4):
optionalDependencies:
'@electric-sql/pglite': 0.3.15
'@libsql/client': 0.17.2
'@types/react': 19.2.14
kysely: 0.28.11
pg: 8.18.0
@ -10240,6 +10439,11 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
finalhandler@2.1.1:
dependencies:
debug: 4.4.3
@ -10261,6 +10465,10 @@ snapshots:
format@0.2.2: {}
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
formidable@3.5.4:
dependencies:
'@paralleldrive/cuid2': 2.3.1
@ -10451,6 +10659,8 @@ snapshots:
joycon@3.1.1: {}
js-base64@3.7.8: {}
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@ -10520,6 +10730,21 @@ snapshots:
dependencies:
isomorphic.js: 0.2.5
libsql@0.5.29:
dependencies:
'@neon-rs/load': 0.0.4
detect-libc: 2.0.2
optionalDependencies:
'@libsql/darwin-arm64': 0.5.29
'@libsql/darwin-x64': 0.5.29
'@libsql/linux-arm-gnueabihf': 0.5.29
'@libsql/linux-arm-musleabihf': 0.5.29
'@libsql/linux-arm64-gnu': 0.5.29
'@libsql/linux-arm64-musl': 0.5.29
'@libsql/linux-x64-gnu': 0.5.29
'@libsql/linux-x64-musl': 0.5.29
'@libsql/win32-x64-msvc': 0.5.29
lightningcss-android-arm64@1.30.2:
optional: true
@ -11165,6 +11390,18 @@ snapshots:
next-tick@1.1.0: {}
node-domexception@1.0.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-releases@2.0.27: {}
object-assign@4.1.1: {}
@ -11362,6 +11599,8 @@ snapshots:
process-warning@5.0.0: {}
promise-limit@2.7.0: {}
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
@ -11905,6 +12144,8 @@ snapshots:
dependencies:
tldts: 7.0.26
tr46@0.0.3: {}
tr46@6.0.0:
dependencies:
punycode: 2.3.1
@ -12253,6 +12494,10 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
webidl-conversions@8.0.1: {}
whatwg-mimetype@5.0.0: {}
@ -12265,6 +12510,11 @@ snapshots:
transitivePeerDependencies:
- '@noble/hashes'
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which@2.0.2:
dependencies:
isexe: 2.0.0

View file

@ -44,6 +44,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.888.0",
"@libsql/client": "^0.17.2",
"@paperclipai/adapter-claude-local": "workspace:*",
"@paperclipai/adapter-codex-local": "workspace:*",
"@paperclipai/adapter-cursor-local": "workspace:*",

View file

@ -0,0 +1,136 @@
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
// We reset the singleton between tests by calling resetSkillRegistryDb
// and redirecting PAPERCLIP_HOME to an isolated temp dir
describe("skill-registry-schema", () => {
let tmpDir: string;
let originalPaperclipHome: string | undefined;
beforeEach(async () => {
tmpDir = await mkdtemp(path.join(os.tmpdir(), "skill-registry-test-"));
originalPaperclipHome = process.env.PAPERCLIP_HOME;
process.env.PAPERCLIP_HOME = tmpDir;
});
afterEach(async () => {
// Reset the DB singleton so next test gets a fresh instance
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
resetSkillRegistryDb();
if (originalPaperclipHome === undefined) {
delete process.env.PAPERCLIP_HOME;
} else {
process.env.PAPERCLIP_HOME = originalPaperclipHome;
}
await rm(tmpDir, { recursive: true, force: true });
});
it("Test 1: getSkillRegistryDb() creates registry.db at the resolved path and returns a drizzle instance", async () => {
const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
const db = await getSkillRegistryDb();
expect(db).toBeDefined();
expect(typeof db.select).toBe("function");
expect(typeof db.insert).toBe("function");
});
it("Test 2: skills table has correct columns", async () => {
const { skills } = await import("../services/skill-registry-schema.js");
const cols = Object.keys(skills);
// Check the table has the expected name
expect((skills as any)[Symbol.for("drizzle:Name") as any] ?? skills._.name).toBeDefined();
const colNames = Object.keys(skills);
// Drizzle table object has column accessors
expect(skills.id).toBeDefined();
expect(skills.sourceId).toBeDefined();
expect(skills.name).toBeDefined();
expect(skills.description).toBeDefined();
expect(skills.sourceUrl).toBeDefined();
expect(skills.activeVersionId).toBeDefined();
expect(skills.removedAt).toBeDefined();
expect(skills.createdAt).toBeDefined();
expect(skills.updatedAt).toBeDefined();
});
it("Test 3: skill_versions table has correct columns", async () => {
const { skillVersions } = await import("../services/skill-registry-schema.js");
expect(skillVersions.id).toBeDefined();
expect(skillVersions.skillId).toBeDefined();
expect(skillVersions.version).toBeDefined();
expect(skillVersions.fetchedAt).toBeDefined();
expect(skillVersions.cacheDir).toBeDefined();
});
it("Test 4: skill_files table has correct columns", async () => {
const { skillFiles } = await import("../services/skill-registry-schema.js");
expect(skillFiles.id).toBeDefined();
expect(skillFiles.versionId).toBeDefined();
expect(skillFiles.path).toBeDefined();
expect(skillFiles.kind).toBeDefined();
expect(skillFiles.sizeBytes).toBeDefined();
});
it("Test 5: community_ratings table has correct columns", async () => {
const { communityRatings } = await import("../services/skill-registry-schema.js");
expect(communityRatings.id).toBeDefined();
expect(communityRatings.skillId).toBeDefined();
expect(communityRatings.fetchedAt).toBeDefined();
expect(communityRatings.averageRating).toBeDefined();
expect(communityRatings.ratingCount).toBeDefined();
expect(communityRatings.source).toBeDefined();
});
it("Test 6: soft-delete — inserting a skill and setting removed_at keeps the row queryable with a WHERE filter", async () => {
const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
const { skills } = await import("../services/skill-registry-schema.js");
const { eq, isNull, isNotNull } = await import("drizzle-orm");
const db = await getSkillRegistryDb();
const now = Date.now();
await db.insert(skills).values({
id: "test-skill-1",
sourceId: "src-1",
name: "Test Skill",
description: "A test skill",
sourceUrl: null,
activeVersionId: null,
removedAt: null,
createdAt: now,
updatedAt: now,
});
// Before soft-delete: row is visible
const before = await db.select().from(skills).where(isNull(skills.removedAt));
expect(before.length).toBe(1);
// Apply soft-delete
await db.update(skills).set({ removedAt: now + 1000 }).where(eq(skills.id, "test-skill-1"));
// After soft-delete: not visible via active filter
const activeAfter = await db.select().from(skills).where(isNull(skills.removedAt));
expect(activeAfter.length).toBe(0);
// But still visible via removed filter
const removedAfter = await db.select().from(skills).where(isNotNull(skills.removedAt));
expect(removedAfter.length).toBe(1);
expect(removedAfter[0]!.id).toBe("test-skill-1");
});
it("Test 7: resolveSkillRegistryDbPath() returns path ending in skills/registry.db under instance root", async () => {
const { resolveSkillRegistryDbPath } = await import("../home-paths.js");
const dbPath = resolveSkillRegistryDbPath();
expect(dbPath).toMatch(/skills[/\\]registry\.db$/);
expect(dbPath.startsWith(tmpDir)).toBe(true);
});
it("Test 8: resolveSkillCacheDir returns path ending in skills/cache/<skillId>/<versionId>", async () => {
const { resolveSkillCacheDir } = await import("../home-paths.js");
const cacheDir = resolveSkillCacheDir("my-skill", "abc123");
expect(cacheDir).toMatch(/skills[/\\]cache[/\\]my-skill[/\\]abc123$/);
expect(cacheDir.startsWith(tmpDir)).toBe(true);
});
});

View file

@ -113,3 +113,12 @@ export function resolveManagedProjectWorkspaceDir(input: {
export function resolveHomeAwarePath(value: string): string {
return path.resolve(expandHomePrefix(value));
}
// [nexus] Skill registry paths
export function resolveSkillRegistryDbPath(): string {
return path.resolve(resolvePaperclipInstanceRoot(), "skills", "registry.db");
}
export function resolveSkillCacheDir(skillId: string, versionId: string): string {
return path.resolve(resolvePaperclipInstanceRoot(), "skills", "cache", skillId, versionId);
}

View file

@ -0,0 +1,73 @@
import { mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "./skill-registry-schema.js";
import { resolveSkillRegistryDbPath } from "../home-paths.js";
export type SkillRegistryDb = ReturnType<typeof drizzle<typeof schema>>;
let _db: SkillRegistryDb | null = null;
const CREATE_SKILLS_TABLE = `
CREATE TABLE IF NOT EXISTS skills (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
source_url TEXT,
active_version_id TEXT,
removed_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)`;
const CREATE_SKILL_VERSIONS_TABLE = `
CREATE TABLE IF NOT EXISTS skill_versions (
id TEXT PRIMARY KEY,
skill_id TEXT NOT NULL,
version TEXT NOT NULL,
fetched_at INTEGER NOT NULL,
cache_dir TEXT
)`;
const CREATE_SKILL_FILES_TABLE = `
CREATE TABLE IF NOT EXISTS skill_files (
id TEXT PRIMARY KEY,
version_id TEXT NOT NULL,
path TEXT NOT NULL,
kind TEXT NOT NULL,
size_bytes INTEGER
)`;
const CREATE_COMMUNITY_RATINGS_TABLE = `
CREATE TABLE IF NOT EXISTS community_ratings (
id TEXT PRIMARY KEY,
skill_id TEXT NOT NULL,
fetched_at INTEGER NOT NULL,
average_rating REAL,
rating_count INTEGER,
source TEXT
)`;
export async function getSkillRegistryDb(): Promise<SkillRegistryDb> {
if (_db !== null) return _db;
const dbPath = resolveSkillRegistryDbPath();
await mkdir(dirname(dbPath), { recursive: true });
const client = createClient({ url: `file:${dbPath}` });
_db = drizzle({ client, schema });
await client.execute(CREATE_SKILLS_TABLE);
await client.execute(CREATE_SKILL_VERSIONS_TABLE);
await client.execute(CREATE_SKILL_FILES_TABLE);
await client.execute(CREATE_COMMUNITY_RATINGS_TABLE);
return _db;
}
/** Reset the singleton — used for test cleanup */
export function resetSkillRegistryDb(): void {
_db = null;
}

View file

@ -0,0 +1,38 @@
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
export const skills = sqliteTable("skills", {
id: text("id").primaryKey(),
sourceId: text("source_id").notNull(),
name: text("name").notNull(),
description: text("description"),
sourceUrl: text("source_url"),
activeVersionId: text("active_version_id"),
removedAt: integer("removed_at"), // unix ms, nullable — soft-delete
createdAt: integer("created_at").notNull(),
updatedAt: integer("updated_at").notNull(),
});
export const skillVersions = sqliteTable("skill_versions", {
id: text("id").primaryKey(),
skillId: text("skill_id").notNull(),
version: text("version").notNull(),
fetchedAt: integer("fetched_at").notNull(),
cacheDir: text("cache_dir"),
});
export const skillFiles = sqliteTable("skill_files", {
id: text("id").primaryKey(),
versionId: text("version_id").notNull(),
path: text("path").notNull(),
kind: text("kind").notNull(), // "skill" | "reference" | "script" | "asset"
sizeBytes: integer("size_bytes"),
});
export const communityRatings = sqliteTable("community_ratings", {
id: text("id").primaryKey(),
skillId: text("skill_id").notNull(),
fetchedAt: integer("fetched_at").notNull(),
averageRating: real("average_rating"),
ratingCount: integer("rating_count"),
source: text("source"),
});