kontexta 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +58 -0
- package/bundle/standalone/CHANGELOG.md +382 -0
- package/bundle/standalone/apps/web/.next/BUILD_ID +1 -0
- package/bundle/standalone/apps/web/.next/app-build-manifest.json +403 -0
- package/bundle/standalone/apps/web/.next/app-path-routes-manifest.json +60 -0
- package/bundle/standalone/apps/web/.next/build-manifest.json +33 -0
- package/bundle/standalone/apps/web/.next/package.json +1 -0
- package/bundle/standalone/apps/web/.next/prerender-manifest.json +148 -0
- package/bundle/standalone/apps/web/.next/react-loadable-manifest.json +300 -0
- package/bundle/standalone/apps/web/.next/required-server-files.json +344 -0
- package/bundle/standalone/apps/web/.next/routes-manifest.json +210 -0
- package/bundle/standalone/apps/web/.next/server/app/_not-found/page.js +2 -0
- package/bundle/standalone/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/_not-found.html +1 -0
- package/bundle/standalone/apps/web/.next/server/app/_not-found.meta +8 -0
- package/bundle/standalone/apps/web/.next/server/app/_not-found.rsc +22 -0
- package/bundle/standalone/apps/web/.next/server/app/api/about/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/about/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/about/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/login/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/login/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/logout/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/logout/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/setup/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/setup/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/setup/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/status/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/status/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/token/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/token/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/auth/token/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/conflicts/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/conflicts/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/conflicts/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/entities/[id]/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/entities/[id]/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/entities/[id]/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/entities/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/entities/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/entities/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/events/[id]/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/events/[id]/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/events/[id]/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/events/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/events/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/events/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/ics/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/ics/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/ics/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/links/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/links/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/links/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/settings/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/settings/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/calendar/settings/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/clip/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/clip/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/clip/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/docker-config/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/docker-config/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/docker-config/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/export/zip/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/export/zip/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/export/zip/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/download/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/download/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/download/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/export-pdf/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/export-pdf/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/export-pdf/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/history/restore/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/history/restore/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/history/restore/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/history/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/history/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/history/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/move/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/move/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/move/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/[id]/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/upload/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/upload/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/files/upload/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/folders/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/folders/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/folders/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/git/remote/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/git/remote/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/git/remote/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/git/sync-all/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/git/sync-all/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/git/sync-all/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/hands/test-pattern/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/hands/test-pattern/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/hands/test-pattern/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/hands/validate/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/hands/validate/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/hands/validate/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/health/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/health/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/health/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/install-snippets/route.js +17 -0
- package/bundle/standalone/apps/web/.next/server/app/api/install-snippets/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/install-snippets/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/mcp-tools/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/mcp-tools/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/mcp-tools/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/profile/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/profile/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/profile/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/hands-config/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/hands-config/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/hands-config/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/journal-config/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/journal-config/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/journal-config/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/journal-status/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/journal-status/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/journal-status/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/onboard/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/onboard/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/onboard/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/refresh/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/refresh/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/refresh/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/sync/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/sync/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/[id]/sync/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/onboard/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/onboard/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/onboard/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/projects/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/publish/html/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/publish/html/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/publish/html/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/publish/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/publish/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/publish/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/refresh/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/refresh/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/refresh/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/reindex/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/reindex/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/reindex/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/search/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/search/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/search/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/tags/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/tags/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/tags/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/ws-config/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/ws-config/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/api/ws-config/route_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/apple-icon.png/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/apple-icon.png/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/apple-icon.png.body +0 -0
- package/bundle/standalone/apps/web/.next/server/app/apple-icon.png.meta +1 -0
- package/bundle/standalone/apps/web/.next/server/app/calendar/page.js +2 -0
- package/bundle/standalone/apps/web/.next/server/app/calendar/page.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/calendar/page_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/favicon.ico/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/favicon.ico.body +0 -0
- package/bundle/standalone/apps/web/.next/server/app/favicon.ico.meta +1 -0
- package/bundle/standalone/apps/web/.next/server/app/icon.png/route.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/icon.png/route.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/icon.png.body +0 -0
- package/bundle/standalone/apps/web/.next/server/app/icon.png.meta +1 -0
- package/bundle/standalone/apps/web/.next/server/app/login/page.js +2 -0
- package/bundle/standalone/apps/web/.next/server/app/login/page.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/page.js +9 -0
- package/bundle/standalone/apps/web/.next/server/app/page.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/preview/motifs/page.js +6 -0
- package/bundle/standalone/apps/web/.next/server/app/preview/motifs/page.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/app/preview/motifs/page_client-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/app/preview/motifs.html +5 -0
- package/bundle/standalone/apps/web/.next/server/app/preview/motifs.meta +7 -0
- package/bundle/standalone/apps/web/.next/server/app/preview/motifs.rsc +29 -0
- package/bundle/standalone/apps/web/.next/server/app-paths-manifest.json +60 -0
- package/bundle/standalone/apps/web/.next/server/chunks/1018.js +115 -0
- package/bundle/standalone/apps/web/.next/server/chunks/1096.js +23 -0
- package/bundle/standalone/apps/web/.next/server/chunks/1139.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/128.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/1921.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2034.js +36 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2048.js +64 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2065.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2144.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2151.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2161.js +153 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2450.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/246.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/260.js +24 -0
- package/bundle/standalone/apps/web/.next/server/chunks/269.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2792.js +287 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2923.js +22 -0
- package/bundle/standalone/apps/web/.next/server/chunks/2944.js +77 -0
- package/bundle/standalone/apps/web/.next/server/chunks/3334.js +141 -0
- package/bundle/standalone/apps/web/.next/server/chunks/3668.js +9 -0
- package/bundle/standalone/apps/web/.next/server/chunks/3758.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/3833.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/3990.js +2 -0
- package/bundle/standalone/apps/web/.next/server/chunks/4274.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/4290.js +252 -0
- package/bundle/standalone/apps/web/.next/server/chunks/4347.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/4550.js +226 -0
- package/bundle/standalone/apps/web/.next/server/chunks/4824.js +166 -0
- package/bundle/standalone/apps/web/.next/server/chunks/509.js +43 -0
- package/bundle/standalone/apps/web/.next/server/chunks/5104.js +215 -0
- package/bundle/standalone/apps/web/.next/server/chunks/5186.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/5400.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/5558.js +80 -0
- package/bundle/standalone/apps/web/.next/server/chunks/6496.js +93 -0
- package/bundle/standalone/apps/web/.next/server/chunks/6693.js +148 -0
- package/bundle/standalone/apps/web/.next/server/chunks/6775.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/7020.js +29 -0
- package/bundle/standalone/apps/web/.next/server/chunks/7098.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/7117.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/7204.js +104 -0
- package/bundle/standalone/apps/web/.next/server/chunks/7342.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/7629.js +80 -0
- package/bundle/standalone/apps/web/.next/server/chunks/7784.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/8286.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/8349.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/8620.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/8675.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/8860.js +24 -0
- package/bundle/standalone/apps/web/.next/server/chunks/8896.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/8950.js +5 -0
- package/bundle/standalone/apps/web/.next/server/chunks/8960.js +79 -0
- package/bundle/standalone/apps/web/.next/server/chunks/899.js +2 -0
- package/bundle/standalone/apps/web/.next/server/chunks/9069.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/9315.js +34 -0
- package/bundle/standalone/apps/web/.next/server/chunks/9372.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/9555.js +6 -0
- package/bundle/standalone/apps/web/.next/server/chunks/9571.js +9 -0
- package/bundle/standalone/apps/web/.next/server/chunks/9681.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/97.js +90 -0
- package/bundle/standalone/apps/web/.next/server/chunks/9797.js +1 -0
- package/bundle/standalone/apps/web/.next/server/chunks/989.js +1 -0
- package/bundle/standalone/apps/web/.next/server/functions-config-manifest.json +4 -0
- package/bundle/standalone/apps/web/.next/server/instrumentation.js +1 -0
- package/bundle/standalone/apps/web/.next/server/middleware-build-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/middleware-manifest.json +6 -0
- package/bundle/standalone/apps/web/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/next-font-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/next-font-manifest.json +1 -0
- package/bundle/standalone/apps/web/.next/server/pages/404.html +1 -0
- package/bundle/standalone/apps/web/.next/server/pages/500.html +1 -0
- package/bundle/standalone/apps/web/.next/server/pages/_app.js +1 -0
- package/bundle/standalone/apps/web/.next/server/pages/_app.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/pages/_document.js +1 -0
- package/bundle/standalone/apps/web/.next/server/pages/_document.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/pages/_error.js +19 -0
- package/bundle/standalone/apps/web/.next/server/pages/_error.js.nft.json +1 -0
- package/bundle/standalone/apps/web/.next/server/pages-manifest.json +6 -0
- package/bundle/standalone/apps/web/.next/server/server-reference-manifest.js +1 -0
- package/bundle/standalone/apps/web/.next/server/server-reference-manifest.json +1 -0
- package/bundle/standalone/apps/web/.next/server/webpack-runtime.js +1 -0
- package/bundle/standalone/apps/web/.next/static/TwaiLte1NHCp3gRYUc-go/_buildManifest.js +1 -0
- package/bundle/standalone/apps/web/.next/static/TwaiLte1NHCp3gRYUc-go/_ssgManifest.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/02852184.a3b62691cdc349cf.js +80 -0
- package/bundle/standalone/apps/web/.next/static/chunks/1226.49d217dc03a1770f.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/1286.8da72129ab3aa8fd.js +77 -0
- package/bundle/standalone/apps/web/.next/static/chunks/1403cfaa-33c2b74c127517c2.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/171.a4732e77e941b2cd.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/1733.3a6dae5cc3785d0b.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/2081.313746c212bfc5c9.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/2086.0b05a08670aae1db.js +226 -0
- package/bundle/standalone/apps/web/.next/static/chunks/2377-956eb9b333193ac6.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/3034.e1171cbeeed85923.js +5 -0
- package/bundle/standalone/apps/web/.next/static/chunks/3068-293b252158eb9d15.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/3297.7664b51ee69c7513.js +23 -0
- package/bundle/standalone/apps/web/.next/static/chunks/338.77f45c7810885961.js +115 -0
- package/bundle/standalone/apps/web/.next/static/chunks/3471.7790a4192ef84a1a.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/3514.14aa56fe189fd052.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/3696.5d462babda0c0b9e.js +34 -0
- package/bundle/standalone/apps/web/.next/static/chunks/408.82356d1e11e880d9.js +93 -0
- package/bundle/standalone/apps/web/.next/static/chunks/46.9922e50f2158e1a9.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/4798.d851e8027b2db933.js +287 -0
- package/bundle/standalone/apps/web/.next/static/chunks/4908.2fed0b6fbc391b55.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/4915.97b9c59991c26fa7.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/492.234794e12315e4bb.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/4936.c65cdd63440ade70.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/5127.e5e917b5cb9c4e52.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/5141.93a42ebd1580da60.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/5159-7bcf68ab11c1091e.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/5514.f969d5c86c590512.js +73 -0
- package/bundle/standalone/apps/web/.next/static/chunks/559.c4615787d977bf47.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/5667.e0097b2112faa29b.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6367.fd87728718a97771.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6394.c2172a59c01bd0bd.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6396.d288334e13a70139.js +24 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6483.88695b22b21b10f1.js +9 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6561.176d35e576df3f9d.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6584.87fc375a0510108d.js +104 -0
- package/bundle/standalone/apps/web/.next/static/chunks/693.71c8b97896628a66.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6934.45e50410379dc377.js +79 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6a2ca82e.c26cb70da3106c1c.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/6b5bb80a.c511153adad62faf.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/7216.d1b096d2a35967fd.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/7342.fcb52cf5f7ce0e17.js +29 -0
- package/bundle/standalone/apps/web/.next/static/chunks/7541.43ff810bb14fd5c0.js +43 -0
- package/bundle/standalone/apps/web/.next/static/chunks/7563.376c2692778a62b3.js +148 -0
- package/bundle/standalone/apps/web/.next/static/chunks/7756.b0dd00e996549a39.js +24 -0
- package/bundle/standalone/apps/web/.next/static/chunks/8052.69c066e56bbd5d6c.js +64 -0
- package/bundle/standalone/apps/web/.next/static/chunks/8080c6f4.ed360e7424d6fe99.js +62 -0
- package/bundle/standalone/apps/web/.next/static/chunks/8280.81c2b0d61e27a576.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/8547.5bef8f1afd162429.js +90 -0
- package/bundle/standalone/apps/web/.next/static/chunks/8602.0b457ca55bd2c08e.js +141 -0
- package/bundle/standalone/apps/web/.next/static/chunks/8794.401ab179d53aa5d5.js +80 -0
- package/bundle/standalone/apps/web/.next/static/chunks/9053.c953d758ff5dc6ee.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/9122.cf7fd16fc8d7fad0.js +36 -0
- package/bundle/standalone/apps/web/.next/static/chunks/9255.78469e88fa06e186.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/944.ba7dcdca1c72114a.js +166 -0
- package/bundle/standalone/apps/web/.next/static/chunks/9794-16144bed6de4bf26.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/9852.e1937ca867a99475.js +215 -0
- package/bundle/standalone/apps/web/.next/static/chunks/9981.8fa0bb90a26c66eb.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/a118fb18.9c3b4291884abee0.js +119 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/_not-found/page-972b01bf23ed0942.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/about/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/auth/login/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/auth/logout/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/auth/setup/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/auth/status/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/auth/token/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/calendar/conflicts/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/calendar/entities/[id]/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/calendar/entities/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/calendar/events/[id]/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/calendar/events/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/calendar/ics/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/calendar/links/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/calendar/settings/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/clip/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/docker-config/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/export/zip/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/files/[id]/download/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/files/[id]/export-pdf/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/files/[id]/history/restore/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/files/[id]/history/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/files/[id]/move/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/files/[id]/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/files/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/files/upload/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/folders/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/git/remote/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/git/sync-all/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/hands/test-pattern/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/hands/validate/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/health/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/install-snippets/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/mcp-tools/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/profile/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/[id]/hands-config/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/[id]/journal-config/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/[id]/journal-status/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/[id]/onboard/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/[id]/refresh/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/[id]/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/[id]/sync/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/onboard/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/projects/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/publish/html/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/publish/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/refresh/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/reindex/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/search/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/tags/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/api/ws-config/route-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/calendar/page-180fba7a5e149d79.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/layout-9a9569fc88cd95d2.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/login/page-d299785546ea6ab6.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/page-2b151d30cb29d2e5.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/app/preview/motifs/page-c1d02d72a16497fa.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/d185f5cd.71b8f43667fbf60d.js +153 -0
- package/bundle/standalone/apps/web/.next/static/chunks/framework-d5846a86e24c79c9.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/main-0205543fb3b69ad5.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/main-app-20d292116f434ff3.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/pages/_app-fe81927504f65cde.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/pages/_error-2973b69c43483f0b.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/bundle/standalone/apps/web/.next/static/chunks/webpack-8818fea786a923fe.js +1 -0
- package/bundle/standalone/apps/web/.next/static/css/c00514317f297826.css +3 -0
- package/bundle/standalone/apps/web/.next/static/media/0aa834ed78bf6d07-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/21350d82a1f187e9-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/438aa629764e75f3-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/4c9affa5bc8f420e-s.p.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/51251f8b9793cdb3-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/67957d42bae0796d-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/875ae681bfde4580-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/886030b0b59bc5a7-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/939c4f875ee75fbb-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/9b01607e2f794050-s.p.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/a5ded5fed781d3c1-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/bb3ef058b751a6ad-s.p.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/cc978ac5ee68c2b6-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/e857b654a2caa584-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/f28ff2d15b1e9bd8-s.woff2 +0 -0
- package/bundle/standalone/apps/web/.next/static/media/f911b923c6adde36-s.woff2 +0 -0
- package/bundle/standalone/apps/web/package.json +66 -0
- package/bundle/standalone/apps/web/public/logo-dark.png +0 -0
- package/bundle/standalone/apps/web/public/logo.png +0 -0
- package/bundle/standalone/apps/web/server.js +38 -0
- package/bundle/standalone/apps/web/src/lib/mcp-tools.json +1941 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/agent-rules/rules-block.md +185 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/calendar-tools.d.ts +2 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/calendar-tools.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/calendar-tools.js +205 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/calendar-tools.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/confirm.d.ts +25 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/confirm.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/confirm.js +40 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/confirm.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/executor.d.ts +13 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/executor.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/executor.js +250 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/executor.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/formatter.d.ts +11 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/formatter.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/formatter.js +39 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/formatter.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/loader.d.ts +4 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/loader.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/loader.js +164 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/loader.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/registry.d.ts +42 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/registry.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/registry.js +143 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/registry.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/sanitizer.d.ts +16 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/sanitizer.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/sanitizer.js +86 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/sanitizer.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/schema-doc.d.ts +2 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/schema-doc.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/schema-doc.js +202 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/schema-doc.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/types.d.ts +50 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/types.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/types.js +2 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/hands/types.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/index.d.ts +3 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/index.js +2117 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/index.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-capture.d.ts +31 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-capture.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-capture.js +293 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-capture.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-commit-upgrades-tool.d.ts +2 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-commit-upgrades-tool.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-commit-upgrades-tool.js +24 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-commit-upgrades-tool.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-housekeep-tool.d.ts +2 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-housekeep-tool.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-housekeep-tool.js +24 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-housekeep-tool.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-tools.d.ts +2 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-tools.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-tools.js +57 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/journal-tools.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/migrations/001-initial.sql +72 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/migrations/002-remote-git.sql +2 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/migrations/003-source-path-index.sql +2 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/migrations/004-journaling.sql +44 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/migrations/005-auth.sql +7 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/migrations/006-journal-locks.sql +11 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/migrations/007-calendar.sql +49 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/profile-tool.d.ts +9 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/profile-tool.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/profile-tool.js +25 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/profile-tool.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/re2-compat.d.ts +4 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/re2-compat.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/re2-compat.js +14 -0
- package/bundle/standalone/node_modules/kontexta-mcp/dist/re2-compat.js.map +1 -0
- package/bundle/standalone/node_modules/kontexta-mcp/package.json +66 -0
- package/bundle/standalone/node_modules/kxta-core/dist/agent-rules/index.d.ts +73 -0
- package/bundle/standalone/node_modules/kxta-core/dist/agent-rules/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/agent-rules/index.js +436 -0
- package/bundle/standalone/node_modules/kxta-core/dist/agent-rules/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/agent-rules/rules-block.md +185 -0
- package/bundle/standalone/node_modules/kxta-core/dist/bundle/index.d.ts +26 -0
- package/bundle/standalone/node_modules/kxta-core/dist/bundle/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/bundle/index.js +115 -0
- package/bundle/standalone/node_modules/kxta-core/dist/bundle/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/conflicts.d.ts +32 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/conflicts.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/conflicts.js +129 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/conflicts.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/ics.d.ts +16 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/ics.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/ics.js +73 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/ics.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/index.d.ts +9 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/index.js +7 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/repository.d.ts +65 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/repository.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/repository.js +219 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/repository.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/types.d.ts +52 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/types.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/types.js +2 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/types.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/validate.d.ts +11 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/validate.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/validate.js +39 -0
- package/bundle/standalone/node_modules/kxta-core/dist/calendar/validate.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/extract.d.ts +26 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/extract.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/extract.js +315 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/extract.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/frontmatter.d.ts +10 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/frontmatter.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/frontmatter.js +53 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/frontmatter.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/index.d.ts +12 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/index.js +38 -0
- package/bundle/standalone/node_modules/kxta-core/dist/clip/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/compat/env-shim.d.ts +2 -0
- package/bundle/standalone/node_modules/kxta-core/dist/compat/env-shim.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/compat/env-shim.js +26 -0
- package/bundle/standalone/node_modules/kxta-core/dist/compat/env-shim.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/compat/file-migration.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/compat/file-migration.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/compat/file-migration.js +30 -0
- package/bundle/standalone/node_modules/kxta-core/dist/compat/file-migration.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/index.d.ts +37 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/index.js +182 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/migrations/001-initial.sql +72 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/migrations/002-remote-git.sql +2 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/migrations/003-source-path-index.sql +2 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/migrations/004-journaling.sql +44 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/migrations/005-auth.sql +7 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/migrations/006-journal-locks.sql +11 -0
- package/bundle/standalone/node_modules/kxta-core/dist/db/migrations/007-calendar.sql +49 -0
- package/bundle/standalone/node_modules/kxta-core/dist/files/index.d.ts +84 -0
- package/bundle/standalone/node_modules/kxta-core/dist/files/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/files/index.js +631 -0
- package/bundle/standalone/node_modules/kxta-core/dist/files/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/files/sections.d.ts +48 -0
- package/bundle/standalone/node_modules/kxta-core/dist/files/sections.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/files/sections.js +112 -0
- package/bundle/standalone/node_modules/kxta-core/dist/files/sections.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/git/index.d.ts +92 -0
- package/bundle/standalone/node_modules/kxta-core/dist/git/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/git/index.js +801 -0
- package/bundle/standalone/node_modules/kxta-core/dist/git/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/index.d.ts +24 -0
- package/bundle/standalone/node_modules/kxta-core/dist/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/index.js +22 -0
- package/bundle/standalone/node_modules/kxta-core/dist/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/cooldown.d.ts +28 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/cooldown.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/cooldown.js +61 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/cooldown.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/distill.d.ts +16 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/distill.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/distill.js +311 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/distill.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/engine.d.ts +45 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/engine.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/engine.js +198 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/engine.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/git-watcher.d.ts +11 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/git-watcher.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/git-watcher.js +41 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/git-watcher.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/high-water.d.ts +16 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/high-water.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/high-water.js +30 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/high-water.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/housekeep.d.ts +22 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/housekeep.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/housekeep.js +104 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/housekeep.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/index.d.ts +27 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/index.js +18 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/build-failure-recovery.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/build-failure-recovery.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/build-failure-recovery.js +31 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/build-failure-recovery.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/error-recovery.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/error-recovery.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/error-recovery.js +31 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/error-recovery.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/exploration.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/exploration.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/exploration.js +28 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/exploration.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/extra-loader.d.ts +13 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/extra-loader.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/extra-loader.js +45 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/extra-loader.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/feature-development.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/feature-development.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/feature-development.js +21 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/feature-development.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/incident-response.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/incident-response.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/incident-response.js +21 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/incident-response.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/index.d.ts +16 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/index.js +40 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/pivot.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/pivot.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/pivot.js +15 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/pivot.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/read-only-investigation.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/read-only-investigation.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/read-only-investigation.js +24 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/read-only-investigation.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/refactor.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/refactor.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/refactor.js +42 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/refactor.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/tagging-pass.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/tagging-pass.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/tagging-pass.js +22 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/tagging-pass.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/test-cycle.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/test-cycle.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/test-cycle.js +23 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/patterns/test-cycle.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/presence.d.ts +2 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/presence.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/presence.js +17 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/presence.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/redact.d.ts +8 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/redact.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/redact.js +40 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/redact.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/renderer.d.ts +10 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/renderer.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/renderer.js +40 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/renderer.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/repository.d.ts +30 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/repository.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/repository.js +66 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/repository.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/strict-mode.d.ts +9 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/strict-mode.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/strict-mode.js +32 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/strict-mode.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/topic-detector.d.ts +4 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/topic-detector.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/topic-detector.js +71 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/topic-detector.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/types.d.ts +59 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/types.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/types.js +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/types.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/writer.d.ts +16 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/writer.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/writer.js +44 -0
- package/bundle/standalone/node_modules/kxta-core/dist/journal/writer.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/metadata/index.d.ts +97 -0
- package/bundle/standalone/node_modules/kxta-core/dist/metadata/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/metadata/index.js +517 -0
- package/bundle/standalone/node_modules/kxta-core/dist/metadata/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/metadata/settings.d.ts +13 -0
- package/bundle/standalone/node_modules/kxta-core/dist/metadata/settings.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/metadata/settings.js +33 -0
- package/bundle/standalone/node_modules/kxta-core/dist/metadata/settings.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/profile/index.d.ts +23 -0
- package/bundle/standalone/node_modules/kxta-core/dist/profile/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/profile/index.js +161 -0
- package/bundle/standalone/node_modules/kxta-core/dist/profile/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/project-map/index.d.ts +34 -0
- package/bundle/standalone/node_modules/kxta-core/dist/project-map/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/project-map/index.js +157 -0
- package/bundle/standalone/node_modules/kxta-core/dist/project-map/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/types.d.ts +69 -0
- package/bundle/standalone/node_modules/kxta-core/dist/types.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/types.js +5 -0
- package/bundle/standalone/node_modules/kxta-core/dist/types.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/extensions.d.ts +15 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/extensions.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/extensions.js +26 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/extensions.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/paths.d.ts +31 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/paths.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/paths.js +179 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/paths.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/safety.d.ts +37 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/safety.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/safety.js +156 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/safety.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/tokens.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/tokens.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/tokens.js +13 -0
- package/bundle/standalone/node_modules/kxta-core/dist/util/tokens.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/watcher/index.d.ts +9 -0
- package/bundle/standalone/node_modules/kxta-core/dist/watcher/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/watcher/index.js +214 -0
- package/bundle/standalone/node_modules/kxta-core/dist/watcher/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/whats-new/index.d.ts +36 -0
- package/bundle/standalone/node_modules/kxta-core/dist/whats-new/index.d.ts.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/dist/whats-new/index.js +94 -0
- package/bundle/standalone/node_modules/kxta-core/dist/whats-new/index.js.map +1 -0
- package/bundle/standalone/node_modules/kxta-core/package.json +20 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/cli.d.ts +9 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/cli.js +139 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/config.d.ts +25 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/config.js +62 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/index.d.ts +2 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/index.js +45 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/pipeline.d.ts +14 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/pipeline.js +42 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/blocks/endpoints.d.ts +6 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/blocks/endpoints.js +112 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/blocks/glossary.d.ts +2 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/blocks/glossary.js +42 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/blocks/mermaid.d.ts +2 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/blocks/mermaid.js +17 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/llms.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/llms.js +52 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/markdown.d.ts +24 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/markdown.js +87 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/nav.d.ts +21 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/nav.js +107 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/pdf.d.ts +10 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/render/pdf.js +70 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/seeds/01-overview.md +74 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/seeds/02-api.md +169 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/seeds/03-improvements.md +90 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/seeds/04-glossary.md +55 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/seeds/seeds.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/seeds/seeds.js +53 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/source/enumerate.d.ts +3 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/source/enumerate.js +50 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/source/reader.d.ts +20 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/source/reader.js +20 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/api-ref.css +593 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/app.js +297 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/brutalist.css +57 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/flat.css +182 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/minimal.css +578 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/ocean.css +179 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/paper.css +33 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/shell.d.ts +8 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/shell.js +181 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/solarized.css +162 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/terminal.css +37 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/template/theme.css +701 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/types.d.ts +97 -0
- package/bundle/standalone/node_modules/kxta-publish/dist/types.js +1 -0
- package/bundle/standalone/node_modules/kxta-publish/package.json +25 -0
- package/bundle/standalone/package.json +26 -0
- package/dist/chunk-KQTCKRN2.js +31 -0
- package/dist/doctor-W733U3FD.js +46 -0
- package/dist/index.js +45 -0
- package/dist/mcp-DGMGR3MZ.js +34 -0
- package/dist/start-RCW7AKUK.js +121 -0
- package/package.json +89 -0
|
@@ -0,0 +1,2117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createFile, readFile, updateFile, deleteFile, listFiles, addTags, removeTags, setFavorite, search, FtsQueryError, registerProject, discoverFiles, refreshIndex, listTags, listProjects, syncBackup, bundleSearch, estimateTokensFromBuffer, clipUrl, ClipError, getHistory, getDiff, getDatabase, findRelated, whatsNew, projectMap, getTagsForFiles, restoreVersion, parseOutline, findSection, replaceSection, listProjectFolders, createFolder, deleteFolder, moveFile, detectAgentContextFiles, assertPathInside, syncAgentRules, checkAgentRulesStatus, RULE_BLOCK_VERSION, gracefulShutdown, startDistillEngine, } from "kxta-core";
|
|
6
|
+
import RE2Class from "./re2-compat.js";
|
|
7
|
+
import { isAbsolute, join, resolve, sep, dirname } from "node:path";
|
|
8
|
+
import { statSync, lstatSync, openSync, readSync, closeSync, readFileSync, existsSync, realpathSync } from "node:fs";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { HandsRegistry } from "./hands/registry.js";
|
|
12
|
+
import { buildSchemaDoc } from "./hands/schema-doc.js";
|
|
13
|
+
import { formatExecResult } from "./hands/formatter.js";
|
|
14
|
+
import { killAllActiveChildren } from "./hands/executor.js";
|
|
15
|
+
import { initCapture, shutdownCapture, wrapHandler, startGitPoller } from "./journal-capture.js";
|
|
16
|
+
import { registerJournalTools } from "./journal-tools.js";
|
|
17
|
+
import { registerCommitUpgradesTool } from "./journal-commit-upgrades-tool.js";
|
|
18
|
+
import { registerHousekeepTool } from "./journal-housekeep-tool.js";
|
|
19
|
+
import { registerCalendarTools } from "./calendar-tools.js";
|
|
20
|
+
import { handleGetProfile } from "./profile-tool.js";
|
|
21
|
+
import { getDataDir } from "kxta-core";
|
|
22
|
+
const dataDir = getDataDir();
|
|
23
|
+
const PROJECT_TOKEN_WARN_THRESHOLD = Number(process.env.KONTEXTA_PROJECT_TOKEN_WARN ?? 100_000);
|
|
24
|
+
function getAgentRulesWarning(projectId) {
|
|
25
|
+
try {
|
|
26
|
+
const db = getDatabase();
|
|
27
|
+
let projects = [];
|
|
28
|
+
if (projectId === null)
|
|
29
|
+
return null; // KB only
|
|
30
|
+
if (typeof projectId === "number") {
|
|
31
|
+
const p = db.prepare("SELECT id, path, name FROM projects WHERE id = ?").get(projectId);
|
|
32
|
+
if (p)
|
|
33
|
+
projects = [p];
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
projects = db.prepare("SELECT id, path, name FROM projects").all();
|
|
37
|
+
}
|
|
38
|
+
const outdatedProjects = [];
|
|
39
|
+
for (const p of projects) {
|
|
40
|
+
if (!p || !p.path || !existsSync(p.path))
|
|
41
|
+
continue;
|
|
42
|
+
const contextFiles = detectAgentContextFiles(p.path);
|
|
43
|
+
if (contextFiles.length === 0)
|
|
44
|
+
continue;
|
|
45
|
+
const statuses = checkAgentRulesStatus(p.path, contextFiles);
|
|
46
|
+
if (statuses.some((s) => !s.upToDate)) {
|
|
47
|
+
outdatedProjects.push(p.name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (outdatedProjects.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
if (outdatedProjects.length === 1) {
|
|
53
|
+
return `Project "${outdatedProjects[0]}" has outdated agent rules (v${RULE_BLOCK_VERSION} available). Run onboard_agent to update.`;
|
|
54
|
+
}
|
|
55
|
+
return `${outdatedProjects.length} projects have outdated agent rules (v${RULE_BLOCK_VERSION} available). Run onboard_agent for each to update.`;
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error("Error checking agent rules status:", e);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function tokenWarning(total) {
|
|
63
|
+
if (!Number.isFinite(PROJECT_TOKEN_WARN_THRESHOLD) || PROJECT_TOKEN_WARN_THRESHOLD <= 0)
|
|
64
|
+
return null;
|
|
65
|
+
if (total <= PROJECT_TOKEN_WARN_THRESHOLD)
|
|
66
|
+
return null;
|
|
67
|
+
return (`Project content totals ~${total.toLocaleString()} tokens, above the ` +
|
|
68
|
+
`${PROJECT_TOKEN_WARN_THRESHOLD.toLocaleString()}-token soft cap. ` +
|
|
69
|
+
`Consider narrowing scope (folder/tag filters), adding ignore patterns ` +
|
|
70
|
+
`(e.g. node_modules-style build/cache dirs), or using bundle_search with a ` +
|
|
71
|
+
`max_tokens budget instead of pulling the full set into one prompt.`);
|
|
72
|
+
}
|
|
73
|
+
function estimateTokensFromFile(filePath, sizeBytes) {
|
|
74
|
+
if (sizeBytes <= 0)
|
|
75
|
+
return 1;
|
|
76
|
+
const sampleSize = Math.min(4096, sizeBytes);
|
|
77
|
+
if (sampleSize < 256)
|
|
78
|
+
return Math.max(1, Math.ceil(sizeBytes / 4));
|
|
79
|
+
let mostlyAscii = true;
|
|
80
|
+
let fd = null;
|
|
81
|
+
try {
|
|
82
|
+
fd = openSync(filePath, "r");
|
|
83
|
+
const buf = Buffer.alloc(sampleSize);
|
|
84
|
+
readSync(fd, buf, 0, sampleSize, 0);
|
|
85
|
+
mostlyAscii = buf.toString("utf-8").length > sampleSize * 0.7;
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
finally {
|
|
89
|
+
if (fd !== null)
|
|
90
|
+
try {
|
|
91
|
+
closeSync(fd);
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
return Math.max(1, Math.ceil(sizeBytes / (mostlyAscii ? 4 : 3)));
|
|
96
|
+
}
|
|
97
|
+
function annotateTokens(rec) {
|
|
98
|
+
let size_bytes = null;
|
|
99
|
+
let est_tokens = null;
|
|
100
|
+
if (typeof rec.content === "string") {
|
|
101
|
+
const buf = Buffer.from(rec.content, "utf-8");
|
|
102
|
+
size_bytes = buf.length;
|
|
103
|
+
est_tokens = estimateTokensFromBuffer(buf);
|
|
104
|
+
}
|
|
105
|
+
else if (rec.path) {
|
|
106
|
+
try {
|
|
107
|
+
size_bytes = statSync(rec.path).size;
|
|
108
|
+
est_tokens = estimateTokensFromFile(rec.path, size_bytes);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
112
|
+
return { ...rec, size_bytes, est_tokens };
|
|
113
|
+
}
|
|
114
|
+
function attachTags(records) {
|
|
115
|
+
if (records.length === 0)
|
|
116
|
+
return [];
|
|
117
|
+
const tagMap = getTagsForFiles(records.map((r) => r.id));
|
|
118
|
+
return records.map((r) => ({ ...r, tags: tagMap.get(r.id) ?? [] }));
|
|
119
|
+
}
|
|
120
|
+
// Robust runtime version loading: walks up directories to find package.json
|
|
121
|
+
let pkgVersion = "0.0.0";
|
|
122
|
+
let pkgVersionFound = false;
|
|
123
|
+
try {
|
|
124
|
+
let currentDir = dirname(fileURLToPath(import.meta.url));
|
|
125
|
+
while (currentDir !== dirname(currentDir)) {
|
|
126
|
+
const p = join(currentDir, "package.json");
|
|
127
|
+
if (existsSync(p)) {
|
|
128
|
+
const pkg = JSON.parse(readFileSync(p, "utf8"));
|
|
129
|
+
if (pkg.name === "kontexta-mcp" && pkg.version) {
|
|
130
|
+
pkgVersion = pkg.version;
|
|
131
|
+
pkgVersionFound = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
currentDir = dirname(currentDir);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
console.warn(`Failed to read package.json for version: ${e.message}. Defaulting to 0.0.0`);
|
|
140
|
+
}
|
|
141
|
+
if (!pkgVersionFound) {
|
|
142
|
+
console.warn("Could not locate kontexta-mcp package.json by walking up from module dir; defaulting version to 0.0.0");
|
|
143
|
+
}
|
|
144
|
+
const server = new McpServer({
|
|
145
|
+
name: "kontexta",
|
|
146
|
+
version: pkgVersion,
|
|
147
|
+
});
|
|
148
|
+
const handsRegistry = new HandsRegistry(server);
|
|
149
|
+
// Initialize journal capture for auto-wrapping all tool calls
|
|
150
|
+
const baseJournalDir = join(dataDir, "knowledge", "journal");
|
|
151
|
+
const projectSlug = process.env.KONTEXTA_DEFAULT_PROJECT_SLUG ?? "default";
|
|
152
|
+
const agent = process.env.KONTEXTA_AGENT ?? "unknown";
|
|
153
|
+
const sid = `${process.pid}-${Date.now().toString(36)}`;
|
|
154
|
+
initCapture({ projectSlug, baseDir: baseJournalDir, agent, sid });
|
|
155
|
+
const distillEngineEnabled = process.env.KONTEXTA_DISTILL_ENGINE !== "off";
|
|
156
|
+
const distillEngine = distillEngineEnabled
|
|
157
|
+
? startDistillEngine({
|
|
158
|
+
dataDir,
|
|
159
|
+
tickMs: Number(process.env.KONTEXTA_DISTILL_TICK_MS) || 5 * 60_000,
|
|
160
|
+
drainOnStart: process.env.KONTEXTA_DISTILL_DRAIN_ON_START !== "false",
|
|
161
|
+
maxEventsPerSlug: Number(process.env.KONTEXTA_DISTILL_MAX_EVENTS) || 500,
|
|
162
|
+
})
|
|
163
|
+
: null;
|
|
164
|
+
startGitPoller(process.env.KONTEXTA_PROJECT_PATH ?? process.cwd(), 30);
|
|
165
|
+
process.on("exit", shutdownCapture);
|
|
166
|
+
// Signal-handler ordering: kill detached Hands children FIRST so their
|
|
167
|
+
// process groups receive SIGTERM before this process exits and orphans
|
|
168
|
+
// them. shutdownCapture flushes the journal; then drain in-flight ops and close DB.
|
|
169
|
+
async function handleShutdownSignal(signal) {
|
|
170
|
+
console.warn(`[kontexta-mcp] received ${signal}; draining…`);
|
|
171
|
+
killAllActiveChildren("SIGTERM");
|
|
172
|
+
if (distillEngine) {
|
|
173
|
+
await Promise.race([
|
|
174
|
+
distillEngine.stop({ flush: true }),
|
|
175
|
+
new Promise((r) => setTimeout(r, 10_000)),
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
shutdownCapture();
|
|
179
|
+
try {
|
|
180
|
+
const remaining = await gracefulShutdown(10_000);
|
|
181
|
+
if (remaining > 0) {
|
|
182
|
+
console.warn(`[kontexta-mcp] drain timeout; ${remaining} ops still in-flight at exit`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
console.warn(`[kontexta-mcp] gracefulShutdown failed`, err);
|
|
187
|
+
}
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
let _shutdownInFlight = false;
|
|
191
|
+
process.on("SIGINT", () => { if (!_shutdownInFlight) {
|
|
192
|
+
_shutdownInFlight = true;
|
|
193
|
+
void handleShutdownSignal("SIGINT");
|
|
194
|
+
} });
|
|
195
|
+
process.on("SIGTERM", () => { if (!_shutdownInFlight) {
|
|
196
|
+
_shutdownInFlight = true;
|
|
197
|
+
void handleShutdownSignal("SIGTERM");
|
|
198
|
+
} });
|
|
199
|
+
// Auto-wrap every tool registration that follows. journal_append (legacy) is excluded
|
|
200
|
+
// because it will be removed in Task 15. Auto-wrap covers all current and future tools.
|
|
201
|
+
const _origServerTool = server.tool.bind(server);
|
|
202
|
+
server.tool = function (name, ...rest) {
|
|
203
|
+
if (name === "journal_append") {
|
|
204
|
+
return _origServerTool(name, ...rest);
|
|
205
|
+
}
|
|
206
|
+
const handler = rest[rest.length - 1];
|
|
207
|
+
if (typeof handler === "function") {
|
|
208
|
+
rest[rest.length - 1] = wrapHandler(name, handler);
|
|
209
|
+
}
|
|
210
|
+
return _origServerTool(name, ...rest);
|
|
211
|
+
};
|
|
212
|
+
// Legacy journal_append tool — excluded from wrapHandler (line 223) so it
|
|
213
|
+
// does NOT get the journal-backlog envelope injected. Kept for backward-compat.
|
|
214
|
+
server.tool("journal_append", "Append a timestamped text entry to today's daily journal file in the Knowledge Base. Creates the file if it doesn't already exist. Both calls on the same calendar day return the same file_id. Returns { file_id }.", {
|
|
215
|
+
text: z.string().describe("Text to append to today's journal entry"),
|
|
216
|
+
project_id: z.number().optional().describe("Optional project ID context"),
|
|
217
|
+
}, async ({ text }) => {
|
|
218
|
+
try {
|
|
219
|
+
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
220
|
+
const journalFolder = "journal";
|
|
221
|
+
const title = `journal-${today}`;
|
|
222
|
+
const db = getDatabase();
|
|
223
|
+
// Try to find an existing journal file for today.
|
|
224
|
+
const knowledgeDir = join(dataDir, "knowledge");
|
|
225
|
+
const expectedPath = join(knowledgeDir, journalFolder, `${title}.md`);
|
|
226
|
+
const existingRow = db
|
|
227
|
+
.prepare("SELECT id, path FROM files WHERE path = ? AND project_id IS NULL")
|
|
228
|
+
.get(expectedPath);
|
|
229
|
+
if (existingRow) {
|
|
230
|
+
// Append to existing file.
|
|
231
|
+
const existing = readFile(existingRow.id);
|
|
232
|
+
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
233
|
+
const newContent = `${existing.content}\n---\n**${timestamp}** ${text}\n`;
|
|
234
|
+
await updateFile(existingRow.id, newContent, dataDir);
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: JSON.stringify({ file_id: existingRow.id }, null, 2) }],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Create a new daily journal file.
|
|
241
|
+
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
242
|
+
const content = `# Journal — ${today}\n\n---\n**${timestamp}** ${text}\n`;
|
|
243
|
+
const result = await createFile({
|
|
244
|
+
title,
|
|
245
|
+
content,
|
|
246
|
+
destination: "knowledge",
|
|
247
|
+
folder: journalFolder,
|
|
248
|
+
dataDir,
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
content: [{ type: "text", text: JSON.stringify({ file_id: result.id }, null, 2) }],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
return {
|
|
257
|
+
isError: true,
|
|
258
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
server.tool("create_file", "Create a new markdown or mermaid file in the knowledge base or project. This operation writes a new file to disk and adds it to the local SQLite FTS5 index. Destination can be 'knowledge' (global KB), 'project' (reference file inside a project repo), or 'kontexta' (internal Kontexta schema file). If destination is 'project' or 'kontexta', project_id is strictly required. No external auth required. Rate limits do not apply (local operation). Returns the created file metadata including its new ID, path, and estimated tokens. If the destination directory does not exist, it will be created automatically. Use this tool to instantiate new contextual documents or notes. To modify an existing file, use 'update_file' instead. Parameters: 'destination' dictates required fields; if 'project' or 'kontexta', 'project_id' must be a valid integer. 'tags' and 'folder' are optional. Pass format='mmd' to create a Mermaid diagram file (.mmd); defaults to 'md'.", {
|
|
263
|
+
title: z.string().describe("Title of the file"),
|
|
264
|
+
content: z.string().describe("Content of the file"),
|
|
265
|
+
destination: z.enum(["knowledge", "project", "kontexta"]).describe("Destination type"),
|
|
266
|
+
project_id: z.number().optional().describe("Project ID (required for project/kontexta destinations)"),
|
|
267
|
+
folder: z.string().optional().describe("Optional folder path"),
|
|
268
|
+
tags: z.array(z.string()).optional().describe("Optional array of tags"),
|
|
269
|
+
format: z.enum(["md", "mmd"]).optional().describe("File extension to write. Defaults to 'md'."),
|
|
270
|
+
}, async ({ title, content, destination, project_id, folder, tags, format }) => {
|
|
271
|
+
const result = await createFile({
|
|
272
|
+
title,
|
|
273
|
+
content,
|
|
274
|
+
destination,
|
|
275
|
+
projectId: project_id,
|
|
276
|
+
folder,
|
|
277
|
+
tags,
|
|
278
|
+
dataDir,
|
|
279
|
+
format,
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
content: [{ type: "text", text: JSON.stringify(annotateTokens(result), null, 2) }],
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
server.tool("read_file", "Read one file's full body and metadata by ID. Read-only; no side effects, auth, or rate limits. Returns title, path, content, tags, est_tokens (so you can budget context before opening more files), and timestamps. Throws if the ID is unknown. Use for a single known file. Prefer `describe_file` to inspect without paying body tokens; `read_files` for batches; `read_file_lines`/`read_section` for partial reads; `read_file_by_path` when you only have the absolute path.", {
|
|
286
|
+
id: z.number().describe("File ID"),
|
|
287
|
+
}, async ({ id }) => {
|
|
288
|
+
const result = readFile(id);
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: JSON.stringify(annotateTokens(result), null, 2) }],
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
server.tool("read_files", "Batch read up to 200 files by ID in one call. Returns per-file annotated records, an aggregate `total_est_tokens`, and an isolated `errors[]` (one bad ID does NOT abort the batch). Read-only; no side effects, auth, or rate limits. Use instead of looping `read_file` to halve round-trips and get the combined token cost upfront. For >200 IDs, page yourself.", {
|
|
294
|
+
ids: z.array(z.number()).min(1).max(200).describe("File IDs to read (max 200 per call)"),
|
|
295
|
+
}, async ({ ids }) => {
|
|
296
|
+
const files = [];
|
|
297
|
+
const errors = [];
|
|
298
|
+
let total_est_tokens = 0;
|
|
299
|
+
for (const id of ids) {
|
|
300
|
+
try {
|
|
301
|
+
const r = annotateTokens(readFile(id));
|
|
302
|
+
files.push(r);
|
|
303
|
+
total_est_tokens += r.est_tokens ?? 0;
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
errors.push({ id, error: e?.message ?? String(e) });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
content: [{ type: "text", text: JSON.stringify({ files, total_est_tokens, error_count: errors.length, errors }, null, 2) }],
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
server.tool("describe_file", "Return everything ABOUT a file without pulling its content (no token cost from the body). Tags, size, est_tokens, history depth, related-file ids, backlinks, project, folder, last edited. Operates locally with no auth or rate limits. Use this when you'd otherwise chain read_file + list_tags + get_history + find_related just to decide whether to actually read the file. Parameters: 'id' must be a valid integer file ID.", {
|
|
314
|
+
id: z.number().describe("File ID"),
|
|
315
|
+
}, async ({ id }) => {
|
|
316
|
+
try {
|
|
317
|
+
const db = getDatabase();
|
|
318
|
+
const file = db
|
|
319
|
+
.prepare("SELECT * FROM files WHERE id = ?")
|
|
320
|
+
.get(id);
|
|
321
|
+
if (!file)
|
|
322
|
+
throw new Error(`File not found: ${id}`);
|
|
323
|
+
const tags = db
|
|
324
|
+
.prepare(`SELECT t.name FROM file_tags ft JOIN tags t ON t.id = ft.tag_id WHERE ft.file_id = ? ORDER BY t.name`)
|
|
325
|
+
.all(id).map((r) => r.name);
|
|
326
|
+
const favorite = !!db.prepare("SELECT 1 FROM favorites WHERE file_id = ?").get(id);
|
|
327
|
+
let projectName = null;
|
|
328
|
+
let folder = null;
|
|
329
|
+
if (file.project_id) {
|
|
330
|
+
const project = db
|
|
331
|
+
.prepare("SELECT name, path FROM projects WHERE id = ?")
|
|
332
|
+
.get(file.project_id);
|
|
333
|
+
projectName = project?.name ?? null;
|
|
334
|
+
if (project?.path && file.path?.startsWith(project.path)) {
|
|
335
|
+
const rel = file.path.slice(project.path.length).replace(/^[\/\\]+/, "");
|
|
336
|
+
const parts = rel.split(/[\/\\]/);
|
|
337
|
+
folder = parts.length > 1 ? parts.slice(0, -1).join("/") : null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const knowledgeRoot = join(dataDir, "knowledge");
|
|
342
|
+
if (file.path?.startsWith(knowledgeRoot)) {
|
|
343
|
+
const rel = file.path.slice(knowledgeRoot.length).replace(/^[\/\\]+/, "");
|
|
344
|
+
const parts = rel.split(/[\/\\]/);
|
|
345
|
+
folder = parts.length > 1 ? parts.slice(0, -1).join("/") : null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
let size_bytes = null;
|
|
349
|
+
let est_tokens = null;
|
|
350
|
+
try {
|
|
351
|
+
size_bytes = statSync(file.path).size;
|
|
352
|
+
est_tokens = Math.max(1, Math.ceil(size_bytes / 4));
|
|
353
|
+
}
|
|
354
|
+
catch { }
|
|
355
|
+
let history_count = 0;
|
|
356
|
+
try {
|
|
357
|
+
const hist = await getHistory(repoDirForFile(file), file.path);
|
|
358
|
+
history_count = hist.length;
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
let related = [];
|
|
362
|
+
try {
|
|
363
|
+
related = findRelated(id, 10).map((r) => ({ id: r.id, shared_tag_count: r.shared_tag_count }));
|
|
364
|
+
}
|
|
365
|
+
catch { }
|
|
366
|
+
const basenameWithExt = file.path.split(/[/\\]/).pop() ?? "";
|
|
367
|
+
const basename = basenameWithExt.replace(/\.md$/, "");
|
|
368
|
+
let backlinks = [];
|
|
369
|
+
if (basename && basenameWithExt) {
|
|
370
|
+
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
371
|
+
const linkPatterns = [
|
|
372
|
+
new RegExp(`\\]\\([^)]*${escapeRegex(basenameWithExt)}(?:[#?][^)]*)?\\)`),
|
|
373
|
+
new RegExp(`\\[\\[\\s*${escapeRegex(basename)}\\s*(?:\\|[^\\]]+)?\\]\\]`, "i"),
|
|
374
|
+
new RegExp(`(?:^|[^\\w/-])${escapeRegex(basenameWithExt)}(?:[^\\w]|$)`),
|
|
375
|
+
];
|
|
376
|
+
try {
|
|
377
|
+
const escFts = basename.replace(/"/g, '""');
|
|
378
|
+
const ftsQuery = `"${escFts} md" OR ${escFts}`;
|
|
379
|
+
const candidates = db
|
|
380
|
+
.prepare(`SELECT files.id, files.title, files.path
|
|
381
|
+
FROM fts_index
|
|
382
|
+
JOIN files ON files.id = fts_index.rowid
|
|
383
|
+
WHERE fts_index MATCH ? AND files.id != ?
|
|
384
|
+
LIMIT 200`)
|
|
385
|
+
.all(ftsQuery, id);
|
|
386
|
+
for (const c of candidates) {
|
|
387
|
+
try {
|
|
388
|
+
const content = readFileSync(c.path, "utf8");
|
|
389
|
+
if (linkPatterns.some((re) => re.test(content))) {
|
|
390
|
+
backlinks.push(c);
|
|
391
|
+
if (backlinks.length >= 50)
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch { }
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch { }
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
content: [
|
|
402
|
+
{
|
|
403
|
+
type: "text",
|
|
404
|
+
text: JSON.stringify({
|
|
405
|
+
id: file.id,
|
|
406
|
+
path: file.path,
|
|
407
|
+
title: file.title,
|
|
408
|
+
project_id: file.project_id,
|
|
409
|
+
project_name: projectName,
|
|
410
|
+
folder,
|
|
411
|
+
storage_type: file.storage_type,
|
|
412
|
+
tags,
|
|
413
|
+
favorite,
|
|
414
|
+
size_bytes,
|
|
415
|
+
est_tokens,
|
|
416
|
+
created_at: file.created_at,
|
|
417
|
+
updated_at: file.updated_at,
|
|
418
|
+
history_count,
|
|
419
|
+
related,
|
|
420
|
+
backlinks,
|
|
421
|
+
}, null, 2),
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
return {
|
|
428
|
+
isError: true,
|
|
429
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
server.tool("read_file_lines", "Return a 1-indexed inclusive line slice of a file. Out-of-range bounds clamp silently to the file's actual length; `to < from` throws. Read-only; no side effects, auth, or rate limits. Returns the snippet plus its size_bytes and est_tokens. Use to inspect a stack-trace region or a chunk of a large file without pulling the whole body. Prefer `read_section` if you know the heading, `grep_in_file` if you know a pattern but not the line number.", {
|
|
434
|
+
id: z.number().describe("File ID"),
|
|
435
|
+
from: z.number().int().positive().describe("First line (1-indexed, inclusive)"),
|
|
436
|
+
to: z.number().int().positive().describe("Last line (1-indexed, inclusive)"),
|
|
437
|
+
}, async ({ id, from, to }) => {
|
|
438
|
+
try {
|
|
439
|
+
if (to < from)
|
|
440
|
+
throw new Error("`to` must be >= `from`");
|
|
441
|
+
const file = readFile(id);
|
|
442
|
+
const lines = file.content.split("\n");
|
|
443
|
+
const start = Math.max(0, from - 1);
|
|
444
|
+
const end = Math.min(lines.length, to);
|
|
445
|
+
const slice = lines.slice(start, end).join("\n");
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "text",
|
|
450
|
+
text: JSON.stringify({
|
|
451
|
+
file_id: id,
|
|
452
|
+
path: file.path,
|
|
453
|
+
from: start + 1,
|
|
454
|
+
to: end,
|
|
455
|
+
total_lines: lines.length,
|
|
456
|
+
content: slice,
|
|
457
|
+
size_bytes: Buffer.byteLength(slice, "utf8"),
|
|
458
|
+
est_tokens: estimateTokensFromBuffer(Buffer.from(slice, "utf8")),
|
|
459
|
+
}, null, 2),
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
catch (e) {
|
|
465
|
+
return {
|
|
466
|
+
isError: true,
|
|
467
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
server.tool("grep_in_file", "Match a JS regex against one file's lines and return matched lines with line numbers (capped: default 100, max 500). Catches what FTS misses: URLs, hyphenated terms, code identifiers. Read-only; no side effects, auth, or rate limits. Invalid regex throws `invalid regex`. Returns `{matches, match_count, truncated}`. Use after `read_file_outline` when you know the file but need a specific reference; for cross-file regex use `regex_search`; for keyword/concept search use `search`.", {
|
|
472
|
+
id: z.number().describe("File ID"),
|
|
473
|
+
pattern: z.string().describe("Pattern to match. Treated as a JavaScript RegExp source."),
|
|
474
|
+
case_insensitive: z.boolean().optional().describe("Add the 'i' flag (default false)"),
|
|
475
|
+
max_matches: z.number().int().positive().max(500).optional().describe("Cap on returned hits (default 100)"),
|
|
476
|
+
}, async ({ id, pattern, case_insensitive, max_matches }) => {
|
|
477
|
+
try {
|
|
478
|
+
let re;
|
|
479
|
+
try {
|
|
480
|
+
re = new RE2Class(pattern, case_insensitive ? "i" : "");
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
throw new Error(`invalid regex: ${e?.message ?? e}`);
|
|
484
|
+
}
|
|
485
|
+
const file = readFile(id);
|
|
486
|
+
const lines = file.content.split("\n");
|
|
487
|
+
const cap = max_matches ?? 100;
|
|
488
|
+
const matches = [];
|
|
489
|
+
for (let i = 0; i < lines.length; i++) {
|
|
490
|
+
if (re.test(lines[i])) {
|
|
491
|
+
matches.push({ line: i + 1, text: lines[i] });
|
|
492
|
+
if (matches.length >= cap)
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: "text",
|
|
500
|
+
text: JSON.stringify({
|
|
501
|
+
file_id: id,
|
|
502
|
+
path: file.path,
|
|
503
|
+
pattern,
|
|
504
|
+
match_count: matches.length,
|
|
505
|
+
truncated: matches.length === cap,
|
|
506
|
+
matches,
|
|
507
|
+
}, null, 2),
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
catch (e) {
|
|
513
|
+
return {
|
|
514
|
+
isError: true,
|
|
515
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
server.tool("regex_search", "Match a JS regex against the body of every file in scope (project, KB, or all) and return per-file hits with line numbers. Slower than FTS `search` because it reads each file's content; use only when FTS misses substrings, URLs, or code identifiers. Read-only; no side effects, auth, or rate limits. Capped at 500 files / 10 hits per file by default; the response reports `files_truncated` and per-file truncation so the agent can re-scope. `project_id: null` = KB only; omit = everywhere. Invalid regex throws.", {
|
|
520
|
+
pattern: z.string().describe("JavaScript RegExp source"),
|
|
521
|
+
project_id: z.number().nullable().optional().describe("Scope to one project, null for KB-only, omit for everything"),
|
|
522
|
+
case_insensitive: z.boolean().optional(),
|
|
523
|
+
max_files: z.number().int().positive().max(2000).optional().describe("Cap on files scanned (default 500)"),
|
|
524
|
+
max_matches_per_file: z.number().int().positive().max(100).optional().describe("Per-file hit cap (default 10)"),
|
|
525
|
+
}, async ({ pattern, project_id, case_insensitive, max_files, max_matches_per_file }) => {
|
|
526
|
+
try {
|
|
527
|
+
let re;
|
|
528
|
+
try {
|
|
529
|
+
re = new RE2Class(pattern, case_insensitive ? "i" : "");
|
|
530
|
+
}
|
|
531
|
+
catch (e) {
|
|
532
|
+
throw new Error(`invalid regex: ${e?.message ?? e}`);
|
|
533
|
+
}
|
|
534
|
+
const fileCap = max_files ?? 500;
|
|
535
|
+
const perFileCap = max_matches_per_file ?? 10;
|
|
536
|
+
const db = getDatabase();
|
|
537
|
+
let where = "";
|
|
538
|
+
const params = [];
|
|
539
|
+
if (project_id === null) {
|
|
540
|
+
where = "WHERE project_id IS NULL";
|
|
541
|
+
}
|
|
542
|
+
else if (typeof project_id === "number") {
|
|
543
|
+
where = "WHERE project_id = ?";
|
|
544
|
+
params.push(project_id);
|
|
545
|
+
}
|
|
546
|
+
const rows = db
|
|
547
|
+
.prepare(`SELECT id, path, title FROM files ${where} LIMIT ?`)
|
|
548
|
+
.all(...params, fileCap);
|
|
549
|
+
const hits = [];
|
|
550
|
+
let scanned = 0;
|
|
551
|
+
for (const r of rows) {
|
|
552
|
+
scanned++;
|
|
553
|
+
let content;
|
|
554
|
+
try {
|
|
555
|
+
content = readFileSync(r.path, "utf8");
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const lines = content.split("\n");
|
|
561
|
+
const fileHits = [];
|
|
562
|
+
for (let i = 0; i < lines.length; i++) {
|
|
563
|
+
if (re.test(lines[i])) {
|
|
564
|
+
fileHits.push({ line: i + 1, text: lines[i] });
|
|
565
|
+
if (fileHits.length >= perFileCap)
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (fileHits.length > 0) {
|
|
570
|
+
hits.push({ file_id: r.id, path: r.path, title: r.title, match_count: fileHits.length, matches: fileHits });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
content: [
|
|
575
|
+
{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: JSON.stringify({
|
|
578
|
+
pattern,
|
|
579
|
+
files_scanned: scanned,
|
|
580
|
+
files_truncated: rows.length === fileCap,
|
|
581
|
+
file_hit_count: hits.length,
|
|
582
|
+
total_match_count: hits.reduce((s, h) => s + h.match_count, 0),
|
|
583
|
+
hits,
|
|
584
|
+
}, null, 2),
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
catch (e) {
|
|
590
|
+
return {
|
|
591
|
+
isError: true,
|
|
592
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
server.tool("update_file", "Update the entire content of an existing file by its ID. This replaces the file's content on disk and triggers an FTS5 re-index. Returns the updated file metadata including new estimated token counts. Operates locally with no external auth or rate limits. If you only need to modify a single section without replacing the entire file, use 'update_file_section' instead to save context budget. Parameters: 'id' must be a valid integer file ID. 'content' is the complete markdown string that will replace the file.", {
|
|
597
|
+
id: z.number().describe("File ID"),
|
|
598
|
+
content: z.string().describe("New content"),
|
|
599
|
+
}, async ({ id, content }) => {
|
|
600
|
+
const result = await updateFile(id, content, dataDir);
|
|
601
|
+
return {
|
|
602
|
+
content: [{ type: "text", text: JSON.stringify(annotateTokens(result), null, 2) }],
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
server.tool("delete_file", "DESTRUCTIVE. Permanently delete one file by ID. KB files are unlinked from disk AND removed from the FTS5 index; project reference files only have their index entry removed (the file on disk is left alone so the watcher does not fight your editor). Not idempotent — deleting an unknown ID throws. No external auth or rate limits. Returns `{success: true}`. Use only when the file is truly obsolete; to deprioritise without losing data, untag (`remove_tags`) or unfavorite (`set_favorite`) instead. Bulk variant: `delete_files`.", {
|
|
606
|
+
id: z.number().describe("File ID"),
|
|
607
|
+
}, async ({ id }) => {
|
|
608
|
+
await deleteFile(id, dataDir);
|
|
609
|
+
return {
|
|
610
|
+
content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }],
|
|
611
|
+
};
|
|
612
|
+
});
|
|
613
|
+
server.tool("list_files", "List file metadata with optional filters (project_id, tag, favorite, folder, untagged) and pagination. Read-only; no side effects, auth, or rate limits. Each row is annotated with tags, est_tokens, and size_bytes; the response includes `total_est_tokens` so you can budget before reading bodies. `project_id: null` returns ONLY Knowledge Base files; omit the field to span everything. Use to browse known structure; for keyword/content lookup use `search`; for a denser whole-vault dump use `project_map`.", {
|
|
614
|
+
project_id: z.number().nullable().optional().describe("Filter by project ID. Pass null to list ONLY Knowledge Base files (project_id IS NULL)."),
|
|
615
|
+
tag: z.string().optional().describe("Filter by tag name"),
|
|
616
|
+
favorite: z.boolean().optional().describe("Filter by favorite status"),
|
|
617
|
+
folder: z.string().optional().describe("Filter by folder path"),
|
|
618
|
+
untagged: z.boolean().optional().describe("If true, return only files that have no tags. Useful for bulk-tagging workflows."),
|
|
619
|
+
limit: z.number().optional().describe("Maximum number of results"),
|
|
620
|
+
offset: z.number().optional().describe("Offset for pagination"),
|
|
621
|
+
}, async ({ project_id, tag, favorite, folder, untagged, limit, offset }) => {
|
|
622
|
+
const filters = {};
|
|
623
|
+
if (project_id !== undefined)
|
|
624
|
+
filters.project_id = project_id;
|
|
625
|
+
if (tag !== undefined)
|
|
626
|
+
filters.tag = tag;
|
|
627
|
+
if (favorite !== undefined)
|
|
628
|
+
filters.favorite = favorite;
|
|
629
|
+
if (folder !== undefined)
|
|
630
|
+
filters.folder = folder;
|
|
631
|
+
if (untagged !== undefined)
|
|
632
|
+
filters.untagged = untagged;
|
|
633
|
+
if (limit !== undefined)
|
|
634
|
+
filters.limit = limit;
|
|
635
|
+
if (offset !== undefined)
|
|
636
|
+
filters.offset = offset;
|
|
637
|
+
const result = listFiles({ dataDir, filters });
|
|
638
|
+
const annotated = attachTags(result.map(annotateTokens));
|
|
639
|
+
const total_est_tokens = annotated.reduce((s, f) => s + (f.est_tokens ?? 0), 0);
|
|
640
|
+
return {
|
|
641
|
+
content: [{ type: "text", text: JSON.stringify({ files: annotated, total_est_tokens }, null, 2) }],
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
server.tool("search", "Full-text (SQLite FTS5) keyword search across files. Returns ranked matches with inline match_excerpt and title_highlight (no follow-up `read_file` needed for snippets) plus tags, est_tokens, size_bytes, and aggregate `total_est_tokens`. Read-only; no side effects, auth, or rate limits. FTS is tokenised: it WILL miss URLs, hyphenated terms, and partial substrings — fall back to `regex_search` for those. `project_id: null` searches only the KB; omit the field to span everything; `tags[]` requires ALL listed tags to match. For prompt-ready bundled bodies use `bundle_search`.", {
|
|
645
|
+
query: z.string().describe("Search query"),
|
|
646
|
+
project_id: z.number().nullable().optional().describe("Filter by project ID. Pass null to search ONLY Knowledge Base files."),
|
|
647
|
+
tags: z.array(z.string()).optional().describe("Filter by tags (all must match)"),
|
|
648
|
+
favorite: z.boolean().optional().describe("Filter by favorite status"),
|
|
649
|
+
}, async ({ query, project_id, tags, favorite }) => {
|
|
650
|
+
const filters = { query };
|
|
651
|
+
if (project_id !== undefined)
|
|
652
|
+
filters.project_id = project_id;
|
|
653
|
+
if (tags !== undefined)
|
|
654
|
+
filters.tags = tags;
|
|
655
|
+
if (favorite !== undefined)
|
|
656
|
+
filters.favorite = favorite;
|
|
657
|
+
let result;
|
|
658
|
+
try {
|
|
659
|
+
result = search(filters);
|
|
660
|
+
}
|
|
661
|
+
catch (e) {
|
|
662
|
+
if (e instanceof FtsQueryError) {
|
|
663
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
664
|
+
}
|
|
665
|
+
throw e;
|
|
666
|
+
}
|
|
667
|
+
const annotated = attachTags(result.map(annotateTokens));
|
|
668
|
+
const total_est_tokens = annotated.reduce((s, f) => s + (f.est_tokens ?? 0), 0);
|
|
669
|
+
return {
|
|
670
|
+
content: [{ type: "text", text: JSON.stringify({ matches: annotated, total_est_tokens }, null, 2) }],
|
|
671
|
+
};
|
|
672
|
+
});
|
|
673
|
+
server.tool("bundle_search", "Run an FTS search and concatenate matched bodies into a single prompt-ready bundle (XML `<document>` blocks or markdown headers + fences) capped at `max_tokens`. Files are added in rank order until the next would exceed the budget; the rest go to `skipped[]`. Read-only; no side effects, auth, or rate limits. Use instead of `search` + N×`read_file` when you need several related files as one context blob. Defaults: format=xml, max_tokens=50000. `project_id: null` = KB only; `tags[]` requires ALL to match.", {
|
|
674
|
+
query: z.string().describe("Full-text search query"),
|
|
675
|
+
project_id: z.number().nullable().optional().describe("Filter by project ID. Pass null to bundle ONLY Knowledge Base files."),
|
|
676
|
+
tags: z.array(z.string()).optional().describe("Filter by tags (all must match)"),
|
|
677
|
+
favorite: z.boolean().optional().describe("Filter by favorite status"),
|
|
678
|
+
format: z.enum(["xml", "markdown"]).default("xml")
|
|
679
|
+
.describe("Bundle format. xml = Anthropic-recommended <document> tags; markdown = ## headers + fenced blocks"),
|
|
680
|
+
max_tokens: z.number().int().positive().default(50000)
|
|
681
|
+
.describe("Token budget. Files added in rank order until the next would exceed; remainder go to skipped[]"),
|
|
682
|
+
}, async ({ query, project_id, tags, favorite, format, max_tokens }) => {
|
|
683
|
+
const filters = { query };
|
|
684
|
+
if (project_id !== undefined)
|
|
685
|
+
filters.project_id = project_id;
|
|
686
|
+
if (tags !== undefined)
|
|
687
|
+
filters.tags = tags;
|
|
688
|
+
if (favorite !== undefined)
|
|
689
|
+
filters.favorite = favorite;
|
|
690
|
+
let result;
|
|
691
|
+
try {
|
|
692
|
+
result = await bundleSearch(filters, { format, max_tokens });
|
|
693
|
+
}
|
|
694
|
+
catch (e) {
|
|
695
|
+
if (e instanceof FtsQueryError) {
|
|
696
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
697
|
+
}
|
|
698
|
+
throw e;
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
server.tool("add_tags", "Append tags to ONE file. Additive — existing tags are preserved; re-adding an existing tag is a no-op (idempotent per tag). New tag names auto-create rows in the global `tags` table. Persists to local SQLite. No external auth or rate limits. Returns `{success: true}`; throws if file_id is unknown. Use to label a single file. To tag every file matching a query in one call use `tag_search_results`; to remove tags use `remove_tags`.", {
|
|
705
|
+
file_id: z.number().describe("File ID"),
|
|
706
|
+
tags: z.array(z.string()).describe("Array of tag names to add"),
|
|
707
|
+
}, async ({ file_id, tags }) => {
|
|
708
|
+
addTags(file_id, tags);
|
|
709
|
+
return {
|
|
710
|
+
content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }],
|
|
711
|
+
};
|
|
712
|
+
});
|
|
713
|
+
server.tool("remove_tags", "Detach one or more tag IDs from ONE file. Destructive on the link only — does NOT delete the file or the global tag definition (orphan tags survive in `list_tags`). Idempotent: removing an already-absent tag is a no-op. No external auth or rate limits. Returns `{success: true}`. Note: takes tag IDs (integers), not names — fetch them via `list_tags`. To remove ALL tags from many files via a query, see `tag_search_results` (additive only) — there is no bulk-untag-by-query tool.", {
|
|
714
|
+
file_id: z.number().describe("File ID"),
|
|
715
|
+
tag_ids: z.array(z.number()).describe("Array of tag IDs to remove"),
|
|
716
|
+
}, async ({ file_id, tag_ids }) => {
|
|
717
|
+
removeTags(file_id, tag_ids);
|
|
718
|
+
return {
|
|
719
|
+
content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }],
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
server.tool("set_favorite", "Set or clear the favorite flag on one file (idempotent — re-setting the same value is a no-op; not a toggle, you pass the desired state). Persists to local SQLite. No external auth or rate limits. Returns `{success: true}`. Use to curate quick-access pins; `list_files`/`search`/`bundle_search` accept `favorite: true` to filter to the pinned set.", {
|
|
723
|
+
file_id: z.number().describe("File ID"),
|
|
724
|
+
favorite: z.boolean().describe("Favorite status"),
|
|
725
|
+
}, async ({ file_id, favorite }) => {
|
|
726
|
+
setFavorite(file_id, favorite);
|
|
727
|
+
return {
|
|
728
|
+
content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }],
|
|
729
|
+
};
|
|
730
|
+
});
|
|
731
|
+
server.tool("list_tags", "List every tag in the global SQLite database with id, name, and applied count. Read-only; no side effects, auth, or rate limits. Returns the entire taxonomy (not paginated). Use to discover existing labels before tagging (so you reuse rather than fork) or to find tag IDs to feed into `remove_tags`. For tags on a specific file, use `describe_file`.", {}, async () => {
|
|
732
|
+
const result = listTags();
|
|
733
|
+
return {
|
|
734
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
735
|
+
};
|
|
736
|
+
});
|
|
737
|
+
server.tool("list_projects", "List every registered project with id, name, absolute path, and a derived `has_hands` flag (true when the path exists on disk AND contains a `kontexta.json`). Read-only; no side effects, auth, or rate limits. Use to find the project_id to pass to scoped tools (`search`, `list_files`, `commit_backup`, `refresh_index`, etc.). To register a new project use `register_project`; to inspect its Hands tools use `list_hands`.", {}, async () => {
|
|
738
|
+
const result = listProjects();
|
|
739
|
+
const augmented = result.map((p) => {
|
|
740
|
+
const has_hands = !!(p.path && existsSync(p.path) && existsSync(`${p.path}/kontexta.json`));
|
|
741
|
+
const contextFiles = p.path ? detectAgentContextFiles(p.path) : [];
|
|
742
|
+
const statuses = p.path ? checkAgentRulesStatus(p.path, contextFiles) : [];
|
|
743
|
+
const outdated = statuses.filter((s) => !s.upToDate);
|
|
744
|
+
return {
|
|
745
|
+
...p,
|
|
746
|
+
has_hands,
|
|
747
|
+
agent_rules: {
|
|
748
|
+
status: outdated.length > 0 ? "outdated" : contextFiles.length > 0 ? "up_to_date" : "none",
|
|
749
|
+
latest_version: RULE_BLOCK_VERSION,
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
return {
|
|
754
|
+
content: [{ type: "text", text: JSON.stringify(augmented, null, 2) }],
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
server.tool("register_project", `Register a new project and link it to the Kontexta knowledge system.
|
|
758
|
+
|
|
759
|
+
SIDE EFFECTS: Writes project metadata to disk (persisted in the Kontexta data directory). Scans the project root recursively to discover and index all markdown files into the local database. Registers any kontexta.json-declared Hands tools found in the project root. This operation is idempotent — re-registering an existing project updates its metadata without data loss.
|
|
760
|
+
|
|
761
|
+
AUTH / RATE LIMITS: None. Operates entirely on the local file system.
|
|
762
|
+
|
|
763
|
+
PARAMETERS:
|
|
764
|
+
- name: Human-readable project name.
|
|
765
|
+
- path: Absolute path to the project root. Required. DO NOT guess or assume the path based on the active editor workspace unless the user explicitly asks to register the "current" or "open" project. If the user provides a project name but no path, ask them for the absolute path before calling this tool. Fails with a descriptive error if the path does not exist or is inaccessible.
|
|
766
|
+
- description: Optional free-text description stored with the project metadata.
|
|
767
|
+
|
|
768
|
+
RETURNS: A JSON object containing:
|
|
769
|
+
- project: { id, name, path, description, created_at }
|
|
770
|
+
- discovered_files_count: number of markdown files indexed
|
|
771
|
+
- discovered_files: array of { path, est_tokens, size_bytes } for each file
|
|
772
|
+
- total_est_tokens: estimated total token cost of all discovered files
|
|
773
|
+
- hands: { found, tools_registered, tools_disabled, warnings }
|
|
774
|
+
- warnings: array of non-fatal issues (e.g. scan failures, token budget exceeded)
|
|
775
|
+
|
|
776
|
+
ERROR CONDITIONS: Returns isError=true if path is missing or unresolvable. Scan failures are non-fatal and reported in warnings rather than as errors.`, {
|
|
777
|
+
name: z.string().describe("Project name"),
|
|
778
|
+
path: z.string().describe("Absolute path to the project root. Required. DO NOT guess from the active workspace unless asked. Ask the user if unsure."),
|
|
779
|
+
description: z.string().optional().describe("Optional project description"),
|
|
780
|
+
}, async ({ name, path, description }) => {
|
|
781
|
+
if (!path) {
|
|
782
|
+
return {
|
|
783
|
+
isError: true,
|
|
784
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Project path is required. Please provide the absolute path to the project." }, null, 2) }],
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
const project = registerProject(name, path, description);
|
|
789
|
+
let discoveredFiles = [];
|
|
790
|
+
let scanWarning = null;
|
|
791
|
+
try {
|
|
792
|
+
discoveredFiles = discoverFiles(project.id, dataDir);
|
|
793
|
+
}
|
|
794
|
+
catch (e) {
|
|
795
|
+
scanWarning = `Initial scan failed: ${e?.message ?? e}`;
|
|
796
|
+
console.warn(`registerProject succeeded but discoverFiles failed:`, e);
|
|
797
|
+
}
|
|
798
|
+
const annotated = discoveredFiles.map(annotateTokens);
|
|
799
|
+
const total_est_tokens = annotated.reduce((s, f) => s + (f.est_tokens ?? 0), 0);
|
|
800
|
+
const sizeWarning = tokenWarning(total_est_tokens);
|
|
801
|
+
const warnings = [scanWarning, sizeWarning].filter((w) => !!w);
|
|
802
|
+
const handsResult = handsRegistry.registerProject(project.name, project.path);
|
|
803
|
+
const handsSummary = {
|
|
804
|
+
found: handsResult.found,
|
|
805
|
+
tools_registered: handsResult.registered,
|
|
806
|
+
tools_disabled: handsResult.disabled,
|
|
807
|
+
warnings: handsResult.warnings,
|
|
808
|
+
};
|
|
809
|
+
const detected = detectAgentContextFiles(project.path);
|
|
810
|
+
const ruleStatuses = checkAgentRulesStatus(project.path, detected);
|
|
811
|
+
const outdated = ruleStatuses.filter((s) => !s.upToDate);
|
|
812
|
+
let recommendationReason = "";
|
|
813
|
+
if (detected.length > 0) {
|
|
814
|
+
if (outdated.length > 0) {
|
|
815
|
+
const versions = outdated
|
|
816
|
+
.map((s) => `${s.path} (found ${s.version ?? "none"})`)
|
|
817
|
+
.join(", ");
|
|
818
|
+
recommendationReason =
|
|
819
|
+
`Your project's agent instructions file is out of date — it still references kontexta workflow rules from an older release. ` +
|
|
820
|
+
`Files needing an update: ${versions}. Latest rules version is v${RULE_BLOCK_VERSION}. ` +
|
|
821
|
+
`Run onboard_agent to refresh the kontexta rules block in-place (your existing project content is preserved).`;
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
recommendationReason =
|
|
825
|
+
`Found ${detected.join(", ")} with kontexta workflow rules already at the latest version (v${RULE_BLOCK_VERSION}). No action needed — your AI agent will load these rules automatically every session.`;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
recommendationReason =
|
|
830
|
+
`No AI agent instructions file (e.g. CLAUDE.md, AGENTS.md, GEMINI.md, ANTIGRAVITY.md, .cursor/rules, .continue/rules, .aider/kontexta.md) was found in this project. ` +
|
|
831
|
+
`These files are how coding agents (Claude Code, Codex, Cursor, Gemini, Aider, etc.) load project-specific context at the start of every session. ` +
|
|
832
|
+
`Without one, your agent won't know this project is registered with kontexta and will skip the search/read/journal workflow — wasting tokens re-reading files it could have looked up. ` +
|
|
833
|
+
`Run onboard_agent with target_agent set to your coding tool to scaffold the right file (CLAUDE.md for Claude Code, .aider/kontexta.md for Aider, etc.) pre-populated with kontexta workflow rules.`;
|
|
834
|
+
}
|
|
835
|
+
const needsOnboarding = outdated.length > 0 || detected.length === 0;
|
|
836
|
+
const recommendation = detected.length > 0
|
|
837
|
+
? {
|
|
838
|
+
kind: "onboard_agent",
|
|
839
|
+
mode: "update",
|
|
840
|
+
reason: recommendationReason,
|
|
841
|
+
target_files: detected,
|
|
842
|
+
next_tool: "onboard_agent",
|
|
843
|
+
next_args: { project_id: project.id },
|
|
844
|
+
prompt: outdated.length > 0
|
|
845
|
+
? `Update the kontexta rules block in ${detected.join(", ")} to v${RULE_BLOCK_VERSION} now? (Your existing project-specific content above/below the rules block will be left untouched.)`
|
|
846
|
+
: null,
|
|
847
|
+
}
|
|
848
|
+
: {
|
|
849
|
+
kind: "onboard_agent",
|
|
850
|
+
mode: "create",
|
|
851
|
+
reason: recommendationReason,
|
|
852
|
+
target_files: [],
|
|
853
|
+
next_tool: "onboard_agent",
|
|
854
|
+
next_args: {
|
|
855
|
+
project_id: project.id,
|
|
856
|
+
target_agent: "<pass your agent: claude-code | codex | gemini | antigravity | cursor | continue | aider | cline | copilot>",
|
|
857
|
+
},
|
|
858
|
+
prompt: "Scaffold an AI agent instructions file now? Tell me which agent you use (claude-code, codex, gemini, antigravity, cursor, continue, aider, cline, or copilot) and I'll create the right file (e.g. CLAUDE.md) with the kontexta workflow rules pre-installed, so your agent picks them up on its next session.",
|
|
859
|
+
};
|
|
860
|
+
const content = [
|
|
861
|
+
{
|
|
862
|
+
type: "text",
|
|
863
|
+
text: JSON.stringify({
|
|
864
|
+
project,
|
|
865
|
+
discovered_files_count: annotated.length,
|
|
866
|
+
total_est_tokens,
|
|
867
|
+
discovered_files: annotated,
|
|
868
|
+
hands: handsSummary,
|
|
869
|
+
recommendation,
|
|
870
|
+
rules_status: ruleStatuses,
|
|
871
|
+
...(warnings.length ? { warnings } : {}),
|
|
872
|
+
}, null, 2),
|
|
873
|
+
},
|
|
874
|
+
];
|
|
875
|
+
if (needsOnboarding && recommendation.prompt) {
|
|
876
|
+
content.push({
|
|
877
|
+
type: "text",
|
|
878
|
+
text: `\nPROMPT: ${recommendation.prompt}`,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return { content };
|
|
882
|
+
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
return {
|
|
885
|
+
isError: true,
|
|
886
|
+
content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
server.tool("onboard_agent", `Write or update the kontexta workflow rules block in a project's agent context file(s). Idempotent — uses fenced markers + version to skip no-op writes.
|
|
891
|
+
|
|
892
|
+
MANDATORY: This tool modifies project configuration files. You MUST seek explicit user consent before calling this tool. Set 'confirm: true' only after the user has agreed.
|
|
893
|
+
|
|
894
|
+
PARAMETERS:
|
|
895
|
+
- project_id: number, required.
|
|
896
|
+
- confirm: boolean, required. Must be true to proceed.
|
|
897
|
+
- files: string[], optional. Paths relative to project root. For update mode, defaults to recommendation.target_files. Ignored when files is empty AND target_agent is provided (create mode).
|
|
898
|
+
- target_agent: enum claude-code | codex | gemini | cursor | continue | aider | cline | copilot | generic. Required when files is empty AND no context file currently exists. Picks the canonical filename and the starter scaffold.
|
|
899
|
+
|
|
900
|
+
RETURNS: { written: [{ path, action: created|updated|skipped, version }], skipped: [{ path, reason }] }`, {
|
|
901
|
+
project_id: z.number().describe("Project ID returned from register_project"),
|
|
902
|
+
confirm: z.boolean().describe("MANDATORY: Set to true only after obtaining explicit user consent to modify context files."),
|
|
903
|
+
files: z.array(z.string()).optional().describe("Project-relative paths to update; defaults to detected context files"),
|
|
904
|
+
target_agent: z.enum(["claude-code", "codex", "gemini", "antigravity", "cursor", "continue", "aider", "cline", "copilot", "generic"]).optional()
|
|
905
|
+
.describe("Required when files is empty AND no context file exists. Picks the canonical filename + scaffold."),
|
|
906
|
+
}, async ({ project_id, confirm, files, target_agent }) => {
|
|
907
|
+
try {
|
|
908
|
+
if (confirm !== true) {
|
|
909
|
+
return {
|
|
910
|
+
isError: true,
|
|
911
|
+
content: [{
|
|
912
|
+
type: "text",
|
|
913
|
+
text: JSON.stringify({
|
|
914
|
+
error: "User consent required",
|
|
915
|
+
details: "This tool modifies project configuration files. You must explain the proposed changes to the user and obtain their explicit consent. Once obtained, re-run this tool with 'confirm: true'."
|
|
916
|
+
}, null, 2)
|
|
917
|
+
}],
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const db = getDatabase();
|
|
921
|
+
const project = db
|
|
922
|
+
.prepare("SELECT id, name, path, description FROM projects WHERE id = ?")
|
|
923
|
+
.get(project_id);
|
|
924
|
+
if (!project || !project.path) {
|
|
925
|
+
return {
|
|
926
|
+
isError: true,
|
|
927
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Project ${project_id} not found or has no path` }, null, 2) }],
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const targetFiles = files ?? [];
|
|
931
|
+
if (targetFiles.length === 0 && !target_agent) {
|
|
932
|
+
return {
|
|
933
|
+
isError: true,
|
|
934
|
+
content: [{ type: "text", text: JSON.stringify({ error: "target_agent is required when files is empty (create mode)" }, null, 2) }],
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
const result = syncAgentRules({
|
|
938
|
+
projectPath: project.path,
|
|
939
|
+
project: { name: project.name, description: project.description },
|
|
940
|
+
files: targetFiles,
|
|
941
|
+
targetAgent: target_agent,
|
|
942
|
+
});
|
|
943
|
+
return {
|
|
944
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
catch (e) {
|
|
948
|
+
return {
|
|
949
|
+
isError: true,
|
|
950
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
server.tool("transfer_agent_context", `COPY existing agent context files (CLAUDE.md, AGENTS.md, .cursor/rules/*.mdc, etc.) from a project's repo into Kontexta's per-project knowledge base so they're indexed by FTS5 and can be git-synced through Kontexta's own backup engine.
|
|
955
|
+
|
|
956
|
+
This tool ONLY COPIES. It never deletes or modifies the originals in your repo. After a successful transfer, the response includes the list of source paths so the user can manually remove them if desired. No tool argument, no flag, and no code path in this tool ever calls a destructive filesystem operation against \`project.path\`.
|
|
957
|
+
|
|
958
|
+
MANDATORY: This tool writes new files into Kontexta's data dir. You MUST seek explicit user consent before calling. Set 'confirm: true' only after the user has agreed.
|
|
959
|
+
|
|
960
|
+
PARAMETERS:
|
|
961
|
+
- project_id: number, required. Project ID returned from register_project.
|
|
962
|
+
- confirm: boolean, required. Must be true.
|
|
963
|
+
- files: string[], optional. Project-relative paths to transfer. Omit or pass [] to transfer all detected agent context files (uses the same detection list as register_project / onboard_agent).
|
|
964
|
+
|
|
965
|
+
RETURNS: { transferred: [{ source_path, kb_id, kb_path, est_tokens }], skipped: [{ source_path, reason }], next_action }
|
|
966
|
+
Skip reasons: "missing" | "symlink" | "outside_project" | "already_transferred_same_content" | "read_error" | "write_error".
|
|
967
|
+
|
|
968
|
+
IDEMPOTENT: re-running with the same files copies nothing if the content is unchanged — duplicate transfers are detected via SHA-256 hash comparison against existing project KB rows.`, {
|
|
969
|
+
project_id: z.number().describe("Project ID returned from register_project"),
|
|
970
|
+
confirm: z.boolean().describe("MANDATORY: Set to true only after obtaining explicit user consent."),
|
|
971
|
+
files: z.array(z.string()).optional().describe("Project-relative paths to transfer. Omit to transfer all detected context files."),
|
|
972
|
+
}, async ({ project_id, confirm, files }) => {
|
|
973
|
+
try {
|
|
974
|
+
if (confirm !== true) {
|
|
975
|
+
return {
|
|
976
|
+
isError: true,
|
|
977
|
+
content: [{
|
|
978
|
+
type: "text",
|
|
979
|
+
text: JSON.stringify({
|
|
980
|
+
error: "User consent required",
|
|
981
|
+
details: "This tool copies project files into Kontexta's KB. Explain the proposed transfer to the user and obtain explicit consent, then re-run with 'confirm: true'. Originals in the project repo are NOT touched."
|
|
982
|
+
}, null, 2)
|
|
983
|
+
}],
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
const db = getDatabase();
|
|
987
|
+
const project = db
|
|
988
|
+
.prepare("SELECT id, name, slug, path FROM projects WHERE id = ?")
|
|
989
|
+
.get(project_id);
|
|
990
|
+
if (!project || !project.path) {
|
|
991
|
+
return {
|
|
992
|
+
isError: true,
|
|
993
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Project ${project_id} not found or has no path` }, null, 2) }],
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
const projectPath = project.path;
|
|
997
|
+
const targetRels = (files && files.length > 0) ? files : detectAgentContextFiles(projectPath);
|
|
998
|
+
// Pre-load existing project KB content hashes for idempotency check.
|
|
999
|
+
const existingHashes = new Set(db.prepare("SELECT content_hash FROM files WHERE project_id = ? AND storage_type = 'local' AND content_hash IS NOT NULL")
|
|
1000
|
+
.all(project.id)
|
|
1001
|
+
.map(r => r.content_hash));
|
|
1002
|
+
const transferred = [];
|
|
1003
|
+
const skipped = [];
|
|
1004
|
+
for (const rel of targetRels) {
|
|
1005
|
+
// Containment check — reject absolute paths, "..", null bytes.
|
|
1006
|
+
let abs;
|
|
1007
|
+
try {
|
|
1008
|
+
abs = assertPathInside(projectPath, rel);
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
skipped.push({ source_path: rel, reason: "outside_project" });
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
// Must exist and be a regular file (not a symlink).
|
|
1015
|
+
let stat;
|
|
1016
|
+
try {
|
|
1017
|
+
stat = lstatSync(abs);
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
stat = null;
|
|
1021
|
+
}
|
|
1022
|
+
if (!stat) {
|
|
1023
|
+
skipped.push({ source_path: rel, reason: "missing" });
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (stat.isSymbolicLink()) {
|
|
1027
|
+
skipped.push({ source_path: rel, reason: "symlink" });
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (!stat.isFile()) {
|
|
1031
|
+
skipped.push({ source_path: rel, reason: "missing" });
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
let content;
|
|
1035
|
+
try {
|
|
1036
|
+
content = readFileSync(abs, "utf8");
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
skipped.push({ source_path: rel, reason: "read_error" });
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
// Idempotency: skip if same content already exists in this project's KB.
|
|
1043
|
+
const hash = createHash("sha256").update(content, "utf8").digest("hex");
|
|
1044
|
+
if (existingHashes.has(hash)) {
|
|
1045
|
+
skipped.push({ source_path: rel, reason: "already_transferred_same_content" });
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
// Title derived from the relative path so files from different subdirs
|
|
1049
|
+
// don't collide (e.g. CLAUDE.md vs .cursor/rules/CLAUDE.md).
|
|
1050
|
+
// slugify() lowercases + replaces non-alphanumerics with dashes.
|
|
1051
|
+
const titleSource = rel.replace(/\.[^./]+$/, "");
|
|
1052
|
+
const title = titleSource || rel;
|
|
1053
|
+
try {
|
|
1054
|
+
const created = await createFile({
|
|
1055
|
+
title,
|
|
1056
|
+
content,
|
|
1057
|
+
destination: "kontexta",
|
|
1058
|
+
projectId: project.id,
|
|
1059
|
+
folder: "agent-context",
|
|
1060
|
+
dataDir,
|
|
1061
|
+
sourcePath: abs,
|
|
1062
|
+
});
|
|
1063
|
+
existingHashes.add(hash);
|
|
1064
|
+
transferred.push({
|
|
1065
|
+
source_path: rel,
|
|
1066
|
+
kb_id: created.id,
|
|
1067
|
+
kb_path: created.path,
|
|
1068
|
+
est_tokens: estimateTokensFromBuffer(Buffer.from(content, "utf8")),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
catch (e) {
|
|
1072
|
+
skipped.push({ source_path: rel, reason: `write_error: ${e?.message ?? String(e)}` });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
const next_action = transferred.length > 0
|
|
1076
|
+
? `Copied ${transferred.length} file(s) into Kontexta's KB. Originals in your repo are unchanged. To remove them yourself: ${transferred.map(t => `rm "${join(projectPath, t.source_path)}"`).join(" && ")}`
|
|
1077
|
+
: "No files were transferred. See skipped[] for reasons.";
|
|
1078
|
+
return {
|
|
1079
|
+
content: [{ type: "text", text: JSON.stringify({ transferred, skipped, next_action }, null, 2) }],
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
catch (e) {
|
|
1083
|
+
return {
|
|
1084
|
+
isError: true,
|
|
1085
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
server.tool("commit_backup", "SIDE-EFFECTFUL — TOUCHES THE NETWORK. Sync the project's KB data into its git backup directory, create a commit, and `git push` to `origin`. AUTH: relies on the local user's git credentials (SSH agent, credential helper, etc.) — there is no in-server auth. Kontexta does not rate-limit, but the remote may. Idempotent in steady state: a no-op commit is skipped, but the push still runs. Throws if the project has no configured backup repo or if push fails (network, auth, conflict). Returns `{success, copied_files_count, copied_paths}`. Use after a batch of KB writes to get changes off-machine.", {
|
|
1090
|
+
project_id: z.number().describe("Project ID"),
|
|
1091
|
+
}, async ({ project_id }) => {
|
|
1092
|
+
try {
|
|
1093
|
+
const copiedPaths = await syncBackup(project_id, dataDir);
|
|
1094
|
+
return {
|
|
1095
|
+
content: [
|
|
1096
|
+
{
|
|
1097
|
+
type: "text",
|
|
1098
|
+
text: JSON.stringify({
|
|
1099
|
+
success: true,
|
|
1100
|
+
copied_files_count: copiedPaths.length,
|
|
1101
|
+
copied_paths: copiedPaths,
|
|
1102
|
+
}, null, 2),
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
catch (e) {
|
|
1108
|
+
return {
|
|
1109
|
+
isError: true,
|
|
1110
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
server.tool("clip_url", "SIDE-EFFECTFUL — fetches an EXTERNAL URL and writes a NEW KB file. Downloads the page, extracts the main article via Readability, converts to markdown, and saves it as a new clipping. NOT idempotent / no de-dup — re-clipping the same URL creates a second file. AUTH: anonymous by default; pass `headers` (e.g. `{Cookie: 'session=...'}` or `{Authorization: 'Bearer ...'}`) to clip behind logins. Kontexta does not rate-limit but the upstream may throttle. On auth-required pages returns isError with `code: AUTH_REQUIRED`, optional `login_url`, and a hint to retry with `headers`. Returns `{file_id, path, title, source}`. Use to ingest external docs into the KB.", {
|
|
1115
|
+
url: z.string().url().describe("The URL to clip"),
|
|
1116
|
+
title: z.string().optional().describe("Optional title override (defaults to the page's <title>)"),
|
|
1117
|
+
headers: z
|
|
1118
|
+
.record(z.string())
|
|
1119
|
+
.optional()
|
|
1120
|
+
.describe("Optional HTTP headers to forward with the fetch (e.g. {\"Cookie\": \"session=...\"} or {\"Authorization\": \"Bearer ...\"}). Use to clip pages behind auth walls after AUTH_REQUIRED."),
|
|
1121
|
+
}, async ({ url, title, headers }) => {
|
|
1122
|
+
try {
|
|
1123
|
+
const file = await clipUrl({ url, title, dataDir, headers });
|
|
1124
|
+
return {
|
|
1125
|
+
content: [
|
|
1126
|
+
{
|
|
1127
|
+
type: "text",
|
|
1128
|
+
text: JSON.stringify({
|
|
1129
|
+
file_id: file.id,
|
|
1130
|
+
path: file.path,
|
|
1131
|
+
title: file.title,
|
|
1132
|
+
source: file.source_path,
|
|
1133
|
+
}, null, 2),
|
|
1134
|
+
},
|
|
1135
|
+
],
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
catch (e) {
|
|
1139
|
+
const code = e instanceof ClipError ? e.code : "INTERNAL_ERROR";
|
|
1140
|
+
const message = e.message ?? String(e);
|
|
1141
|
+
const payload = { code, message };
|
|
1142
|
+
if (e instanceof ClipError && code === "AUTH_REQUIRED") {
|
|
1143
|
+
payload.auth_required = true;
|
|
1144
|
+
if (e.details.loginUrl)
|
|
1145
|
+
payload.login_url = e.details.loginUrl;
|
|
1146
|
+
if (e.details.signal)
|
|
1147
|
+
payload.signal = e.details.signal;
|
|
1148
|
+
if (e.details.wwwAuthenticate)
|
|
1149
|
+
payload.www_authenticate = e.details.wwwAuthenticate;
|
|
1150
|
+
payload.hint = "Page requires authentication. Retry with the optional `headers` param (e.g. {\"Cookie\": \"...\"} or {\"Authorization\": \"Bearer ...\"}).";
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
isError: true,
|
|
1154
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
function repoDirForFile(file) {
|
|
1159
|
+
if (file.storage_type === "reference" && file.project_id) {
|
|
1160
|
+
const project = getDatabase()
|
|
1161
|
+
.prepare("SELECT path FROM projects WHERE id = ?")
|
|
1162
|
+
.get(file.project_id);
|
|
1163
|
+
if (project?.path)
|
|
1164
|
+
return project.path;
|
|
1165
|
+
}
|
|
1166
|
+
return dataDir;
|
|
1167
|
+
}
|
|
1168
|
+
server.tool("get_history", "Return the git commit history for one file (newest first), each entry with hash, message, date, and author. Reads the file's owning repo: the project's git repo for project files, the KB backup repo for KB files. Read-only; no side effects, auth, or rate limits. Returns `{file_id, path, history}`; an empty array means the file has not been committed yet. Use to understand a file's evolution before editing or restoring. Pair with `get_diff` to see exact line changes; use `restore_file` to roll back.", {
|
|
1169
|
+
file_id: z.number().describe("ID of the file"),
|
|
1170
|
+
}, async ({ file_id }) => {
|
|
1171
|
+
const file = readFile(file_id);
|
|
1172
|
+
const history = await getHistory(repoDirForFile(file), file.path);
|
|
1173
|
+
return {
|
|
1174
|
+
content: [
|
|
1175
|
+
{
|
|
1176
|
+
type: "text",
|
|
1177
|
+
text: JSON.stringify({ file_id, path: file.path, history }, null, 2),
|
|
1178
|
+
},
|
|
1179
|
+
],
|
|
1180
|
+
};
|
|
1181
|
+
});
|
|
1182
|
+
server.tool("get_diff", "Return the unified diff of one file between two commit hashes (typically obtained from `get_history` for the same file). Read-only; no side effects, auth, or rate limits. Order matters — `commit_a` is treated as the earlier side; reversing the args inverts the diff. Throws if either hash is unknown to the file's repo. Use after `get_history` to see WHAT changed, not just THAT it changed.", {
|
|
1183
|
+
file_id: z.number().describe("ID of the file"),
|
|
1184
|
+
commit_a: z.string().describe("Earlier commit hash (from get_history)"),
|
|
1185
|
+
commit_b: z.string().describe("Later commit hash (from get_history)"),
|
|
1186
|
+
}, async ({ file_id, commit_a, commit_b }) => {
|
|
1187
|
+
const file = readFile(file_id);
|
|
1188
|
+
const diff = await getDiff(repoDirForFile(file), file.path, commit_a, commit_b);
|
|
1189
|
+
return {
|
|
1190
|
+
content: [
|
|
1191
|
+
{
|
|
1192
|
+
type: "text",
|
|
1193
|
+
text: JSON.stringify({ file_id, path: file.path, commit_a, commit_b, diff }, null, 2),
|
|
1194
|
+
},
|
|
1195
|
+
],
|
|
1196
|
+
};
|
|
1197
|
+
});
|
|
1198
|
+
server.tool("restore_file", "DESTRUCTIVE. Overwrite a file's current on-disk content with the version recorded at a specific git commit, then re-index FTS. The hash MUST come from `get_history` for THIS file (foreign hashes throw). The current uncommitted content is lost unless it was already committed elsewhere. The file watcher may also pick up the change before this returns. No external auth or rate limits. Returns `{file_id, path, hash, success, message}`. Use only to undo accidental edits or recover a known-good version.", {
|
|
1199
|
+
file_id: z.number().describe("ID of the file"),
|
|
1200
|
+
hash: z.string().describe("Commit hash to restore from (from get_history)"),
|
|
1201
|
+
}, async ({ file_id, hash }) => {
|
|
1202
|
+
try {
|
|
1203
|
+
const file = readFile(file_id);
|
|
1204
|
+
const content = await restoreVersion(repoDirForFile(file), file.path, hash);
|
|
1205
|
+
return {
|
|
1206
|
+
content: [
|
|
1207
|
+
{
|
|
1208
|
+
type: "text",
|
|
1209
|
+
text: JSON.stringify({
|
|
1210
|
+
file_id,
|
|
1211
|
+
path: file.path,
|
|
1212
|
+
hash,
|
|
1213
|
+
success: true,
|
|
1214
|
+
message: `File restored to version ${hash.slice(0, 7)}`,
|
|
1215
|
+
}, null, 2),
|
|
1216
|
+
},
|
|
1217
|
+
],
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
catch (e) {
|
|
1221
|
+
return {
|
|
1222
|
+
isError: true,
|
|
1223
|
+
content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
server.tool("read_file_outline", "Return a flat list of markdown headings for one file (level, text, line, byteStart, byteEnd). Read-only; no side effects, auth, or rate limits. Use as a cheap probe before `read_section` or `update_file_section` so you don't spend tokens on the full body just to learn what sections exist. Empty outline means the file has no markdown headings (it may still have content — fall back to `read_file` or `read_file_lines`).", {
|
|
1228
|
+
file_id: z.number().describe("File ID"),
|
|
1229
|
+
}, async ({ file_id }) => {
|
|
1230
|
+
try {
|
|
1231
|
+
const file = readFile(file_id);
|
|
1232
|
+
const outline = parseOutline(file.content).map((n) => ({
|
|
1233
|
+
level: n.level,
|
|
1234
|
+
text: n.text,
|
|
1235
|
+
line: n.line,
|
|
1236
|
+
byteStart: n.byteStart,
|
|
1237
|
+
byteEnd: n.byteEnd,
|
|
1238
|
+
}));
|
|
1239
|
+
return {
|
|
1240
|
+
content: [
|
|
1241
|
+
{
|
|
1242
|
+
type: "text",
|
|
1243
|
+
text: JSON.stringify({ file_id, path: file.path, title: file.title, outline }, null, 2),
|
|
1244
|
+
},
|
|
1245
|
+
],
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
catch (e) {
|
|
1249
|
+
return {
|
|
1250
|
+
isError: true,
|
|
1251
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
server.tool("read_section", "Return the body of ONE heading (the heading line itself is excluded) plus level, line, size_bytes, and est_tokens. Heading match is case-insensitive but exact-string after trim — fuzzy / partial matches do NOT resolve. Returns isError if the heading is absent. Read-only; no side effects, auth, or rate limits. Pair with `read_file_outline` when you are unsure which headings exist; for non-heading line ranges use `read_file_lines`.", {
|
|
1256
|
+
file_id: z.number().describe("File ID"),
|
|
1257
|
+
heading: z.string().describe("Heading text to extract (case-insensitive)"),
|
|
1258
|
+
}, async ({ file_id, heading }) => {
|
|
1259
|
+
try {
|
|
1260
|
+
const file = readFile(file_id);
|
|
1261
|
+
const node = findSection(file.content, heading);
|
|
1262
|
+
if (!node) {
|
|
1263
|
+
return {
|
|
1264
|
+
isError: true,
|
|
1265
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Section not found: ${heading}` }, null, 2) }],
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const buf = Buffer.from(file.content, "utf8");
|
|
1269
|
+
const body = buf.subarray(node.contentStart, node.contentEnd).toString("utf8");
|
|
1270
|
+
return {
|
|
1271
|
+
content: [
|
|
1272
|
+
{
|
|
1273
|
+
type: "text",
|
|
1274
|
+
text: JSON.stringify({
|
|
1275
|
+
file_id,
|
|
1276
|
+
path: file.path,
|
|
1277
|
+
heading: node.text,
|
|
1278
|
+
level: node.level,
|
|
1279
|
+
line: node.line,
|
|
1280
|
+
content: body,
|
|
1281
|
+
size_bytes: Buffer.byteLength(body, "utf8"),
|
|
1282
|
+
est_tokens: estimateTokensFromBuffer(Buffer.from(body, "utf8")),
|
|
1283
|
+
}, null, 2),
|
|
1284
|
+
},
|
|
1285
|
+
],
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
catch (e) {
|
|
1289
|
+
return {
|
|
1290
|
+
isError: true,
|
|
1291
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
server.tool("update_file_section", "Surgical write — replace the body of ONE heading without touching siblings. The heading line itself is preserved verbatim; only its body is rewritten. Persists via the same path as `update_file` (writes to disk → FTS reindex → git commit). Throws if the heading does not exist (this tool will NOT create a new section — append the section text via `update_file` first). Heading match is case-insensitive exact-string. No external auth or rate limits. Returns the updated file metadata. Use to make targeted edits without re-sending the whole body; for full-file replacement use `update_file`.", {
|
|
1296
|
+
file_id: z.number().describe("File ID"),
|
|
1297
|
+
heading: z.string().describe("Heading whose body to replace (case-insensitive)"),
|
|
1298
|
+
content: z.string().describe("New body content (heading line is preserved automatically)"),
|
|
1299
|
+
}, async ({ file_id, heading, content }) => {
|
|
1300
|
+
try {
|
|
1301
|
+
const file = readFile(file_id);
|
|
1302
|
+
const updated = replaceSection(file.content, heading, content);
|
|
1303
|
+
const result = await updateFile(file_id, updated, dataDir);
|
|
1304
|
+
return {
|
|
1305
|
+
content: [{ type: "text", text: JSON.stringify(annotateTokens(result), null, 2) }],
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
catch (e) {
|
|
1309
|
+
return {
|
|
1310
|
+
isError: true,
|
|
1311
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
function resolveFolderBase(projectId) {
|
|
1316
|
+
if (projectId === undefined || projectId === null) {
|
|
1317
|
+
return join(dataDir, "knowledge");
|
|
1318
|
+
}
|
|
1319
|
+
const project = getDatabase()
|
|
1320
|
+
.prepare("SELECT path FROM projects WHERE id = ?")
|
|
1321
|
+
.get(projectId);
|
|
1322
|
+
if (!project?.path) {
|
|
1323
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
1324
|
+
}
|
|
1325
|
+
return project.path;
|
|
1326
|
+
}
|
|
1327
|
+
function validateFolderName(name) {
|
|
1328
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1329
|
+
throw new Error("name must be a non-empty string");
|
|
1330
|
+
}
|
|
1331
|
+
if (name.includes("\0"))
|
|
1332
|
+
throw new Error("name contains null byte");
|
|
1333
|
+
if (name.startsWith("/") || name.startsWith("\\")) {
|
|
1334
|
+
throw new Error("name must not start with a path separator");
|
|
1335
|
+
}
|
|
1336
|
+
if (name.split(/[/\\]/).some((seg) => seg === "..")) {
|
|
1337
|
+
throw new Error("name must not contain '..' segments");
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
server.tool("list_folders", "List folder paths under a project root (or the Knowledge Base when `project_id` is null/omitted). Returns `{folders: string[], base_path}` where `folders` are RELATIVE to `base_path`. Read-only; no side effects, auth, or rate limits. Throws if `project_id` references an unknown project. Use to discover where to drop a new file via `create_file`'s `folder` argument or to navigate vault structure; to actually create one use `create_folder`.", {
|
|
1341
|
+
project_id: z.number().nullable().optional().describe("Project ID. Pass null or omit to list KB folders."),
|
|
1342
|
+
}, async ({ project_id }) => {
|
|
1343
|
+
try {
|
|
1344
|
+
const base = resolveFolderBase(project_id);
|
|
1345
|
+
const folders = listProjectFolders(base);
|
|
1346
|
+
return {
|
|
1347
|
+
content: [{ type: "text", text: JSON.stringify({ folders, base_path: base }, null, 2) }],
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
catch (e) {
|
|
1351
|
+
return {
|
|
1352
|
+
isError: true,
|
|
1353
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
server.tool("create_folder", "Create a folder under a project root or the KB. Idempotent — creating an existing folder succeeds. Nested paths like `notes/inbox` create intermediates. REJECTS: empty names, null bytes, leading path separators, and any segment equal to `..` (the call returns isError, no folder is touched). Side effect: a directory is mkdir'd on disk; no DB rows are written until a file lands inside. No external auth or rate limits. Returns `{path, base_path}`.", {
|
|
1358
|
+
project_id: z.number().nullable().optional().describe("Project ID. Pass null or omit to create the folder under the KB."),
|
|
1359
|
+
name: z.string().describe("Folder name (relative; supports nested paths via '/')"),
|
|
1360
|
+
}, async ({ project_id, name }) => {
|
|
1361
|
+
try {
|
|
1362
|
+
validateFolderName(name);
|
|
1363
|
+
const base = resolveFolderBase(project_id);
|
|
1364
|
+
const path = createFolder(base, name);
|
|
1365
|
+
return {
|
|
1366
|
+
content: [{ type: "text", text: JSON.stringify({ path, base_path: base }, null, 2) }],
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
catch (e) {
|
|
1370
|
+
return {
|
|
1371
|
+
isError: true,
|
|
1372
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
server.tool("delete_folder", "DESTRUCTIVE — recursively delete a folder under the KB AND every file inside it (disk + FTS rows). REFUSES (returns isError) when `project_id` is supplied: deleting inside a registered project would race the file watcher and re-ingest the contents — remove project content via your editor instead. Same name validation as `create_folder`. Not recoverable from Kontexta after the call (only the git backup, if configured, retains it). No external auth or rate limits. Returns `{success: true}`.", {
|
|
1377
|
+
project_id: z.number().nullable().optional().describe("Project ID. Pass null or omit to delete from the KB. Project IDs are rejected."),
|
|
1378
|
+
name: z.string().describe("Folder name (relative)"),
|
|
1379
|
+
}, async ({ project_id, name }) => {
|
|
1380
|
+
try {
|
|
1381
|
+
validateFolderName(name);
|
|
1382
|
+
if (project_id !== undefined && project_id !== null) {
|
|
1383
|
+
return {
|
|
1384
|
+
isError: true,
|
|
1385
|
+
content: [
|
|
1386
|
+
{
|
|
1387
|
+
type: "text",
|
|
1388
|
+
text: JSON.stringify({
|
|
1389
|
+
error: "Cannot delete folders inside a registered project. The watcher would re-ingest the contents. Unregister the project or remove the folder from disk in your editor instead.",
|
|
1390
|
+
}, null, 2),
|
|
1391
|
+
},
|
|
1392
|
+
],
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
const base = resolveFolderBase(null);
|
|
1396
|
+
deleteFolder(base, name);
|
|
1397
|
+
return {
|
|
1398
|
+
content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }],
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
catch (e) {
|
|
1402
|
+
return {
|
|
1403
|
+
isError: true,
|
|
1404
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
server.tool("move_file", "Move/rename a file. Destination 'new_path' must be absolute and resolve INSIDE the file's owning project or global knowledge directory. Cross-project moves are rejected. Operates locally with no auth or limits. Parameters: 'file_id' is a valid file ID. 'new_path' is an absolute path.", {
|
|
1409
|
+
file_id: z.number().describe("File ID"),
|
|
1410
|
+
new_path: z.string().describe("Absolute destination path"),
|
|
1411
|
+
}, async ({ file_id, new_path }) => {
|
|
1412
|
+
try {
|
|
1413
|
+
if (typeof new_path !== "string" || new_path.length === 0) {
|
|
1414
|
+
throw new Error("new_path is required");
|
|
1415
|
+
}
|
|
1416
|
+
if (new_path.includes("\0"))
|
|
1417
|
+
throw new Error("new_path contains null byte");
|
|
1418
|
+
if (!isAbsolute(new_path))
|
|
1419
|
+
throw new Error("new_path must be absolute");
|
|
1420
|
+
const file = readFile(file_id);
|
|
1421
|
+
let base;
|
|
1422
|
+
if (file.storage_type === "reference" && file.project_id) {
|
|
1423
|
+
const project = getDatabase()
|
|
1424
|
+
.prepare("SELECT path FROM projects WHERE id = ?")
|
|
1425
|
+
.get(file.project_id);
|
|
1426
|
+
if (!project?.path)
|
|
1427
|
+
throw new Error(`Project not found for file ${file_id}`);
|
|
1428
|
+
base = project.path;
|
|
1429
|
+
}
|
|
1430
|
+
else {
|
|
1431
|
+
base = join(dataDir, "knowledge");
|
|
1432
|
+
}
|
|
1433
|
+
// Use realpath to follow symlinks before the containment check —
|
|
1434
|
+
// path.resolve() only normalises `.`/`..`, so a symlink inside the
|
|
1435
|
+
// vault pointing outside (e.g. knowledge/escape -> /etc) would let
|
|
1436
|
+
// moveFile write through it. realpath the dest's PARENT since the
|
|
1437
|
+
// destination itself doesn't exist yet.
|
|
1438
|
+
let baseResolved;
|
|
1439
|
+
try {
|
|
1440
|
+
baseResolved = realpathSync(resolve(base));
|
|
1441
|
+
}
|
|
1442
|
+
catch {
|
|
1443
|
+
throw new Error(`Base directory does not exist: ${base}`);
|
|
1444
|
+
}
|
|
1445
|
+
const destAbs = resolve(new_path);
|
|
1446
|
+
const destParent = dirname(destAbs);
|
|
1447
|
+
let destParentReal;
|
|
1448
|
+
try {
|
|
1449
|
+
destParentReal = realpathSync(destParent);
|
|
1450
|
+
}
|
|
1451
|
+
catch {
|
|
1452
|
+
throw new Error(`Destination parent directory does not exist: ${destParent}`);
|
|
1453
|
+
}
|
|
1454
|
+
const destResolved = join(destParentReal, destAbs.slice(destParent.length + (destParent.endsWith(sep) ? 0 : 1)));
|
|
1455
|
+
if (destResolved !== baseResolved && !destResolved.startsWith(baseResolved + sep)) {
|
|
1456
|
+
throw new Error(`new_path must be inside ${base}`);
|
|
1457
|
+
}
|
|
1458
|
+
// Defense-in-depth: also confirm the SOURCE lives under the same base.
|
|
1459
|
+
// If a project was re-registered to a new path after this file was
|
|
1460
|
+
// ingested, fileRecord.path can point outside the current base — and
|
|
1461
|
+
// without this check moveFile would relocate that orphan into the
|
|
1462
|
+
// project root.
|
|
1463
|
+
let srcResolved;
|
|
1464
|
+
try {
|
|
1465
|
+
srcResolved = realpathSync(resolve(file.path));
|
|
1466
|
+
}
|
|
1467
|
+
catch {
|
|
1468
|
+
srcResolved = resolve(file.path);
|
|
1469
|
+
}
|
|
1470
|
+
if (srcResolved !== baseResolved && !srcResolved.startsWith(baseResolved + sep)) {
|
|
1471
|
+
throw new Error(`source path ${file.path} is no longer inside ${base}; refusing to move`);
|
|
1472
|
+
}
|
|
1473
|
+
const updated = moveFile(file_id, new_path);
|
|
1474
|
+
return {
|
|
1475
|
+
content: [{ type: "text", text: JSON.stringify(annotateTokens(updated), null, 2) }],
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
catch (e) {
|
|
1479
|
+
return {
|
|
1480
|
+
isError: true,
|
|
1481
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
server.tool("find_related", "Find other files sharing tags with the given file, ranked by `shared_tag_count` descending. Read-only; no side effects, auth, or rate limits. Returns annotated file rows with `shared_tag_count` and `shared_tags`. Empty result means the file has no tags or no other file shares them — try `search`/`regex_search` for content-based discovery, or `suggest_tags` to bootstrap labels first. Default limit 10.", {
|
|
1486
|
+
file_id: z.number().describe("ID of the file to find relations for"),
|
|
1487
|
+
limit: z.number().optional().describe("Maximum number of related files to return (default 10)"),
|
|
1488
|
+
}, async ({ file_id, limit }) => {
|
|
1489
|
+
const related = findRelated(file_id, limit ?? 10);
|
|
1490
|
+
const annotated = related.map((r) => ({ ...annotateTokens(r), shared_tag_count: r.shared_tag_count, shared_tags: r.shared_tags }));
|
|
1491
|
+
return {
|
|
1492
|
+
content: [
|
|
1493
|
+
{
|
|
1494
|
+
type: "text",
|
|
1495
|
+
text: JSON.stringify({ file_id, related: annotated }, null, 2),
|
|
1496
|
+
},
|
|
1497
|
+
],
|
|
1498
|
+
};
|
|
1499
|
+
});
|
|
1500
|
+
server.tool("create_files", "Batch variant of `create_file` — create up to 200 markdown or mermaid files in one call. Each item follows the same rules (project_id required if destination is `project` or `kontexta`). SIDE EFFECTS: writes new files to disk and inserts FTS5 rows; missing folders are mkdir'd. Per-item failures are isolated to `errors[]` and the rest of the batch still commits — partial success is the norm, always inspect `error_count`. No external auth or rate limits. Returns `{created_count, error_count, created, errors}`. Use for bulk ingestion; for >200 items, page yourself. Pass format='mmd' on an item to create a Mermaid diagram file (.mmd); defaults to 'md'.", {
|
|
1501
|
+
files: z
|
|
1502
|
+
.array(z.object({
|
|
1503
|
+
title: z.string(),
|
|
1504
|
+
content: z.string(),
|
|
1505
|
+
destination: z.enum(["knowledge", "project", "kontexta"]),
|
|
1506
|
+
project_id: z.number().optional(),
|
|
1507
|
+
folder: z.string().optional(),
|
|
1508
|
+
tags: z.array(z.string()).optional(),
|
|
1509
|
+
format: z.enum(["md", "mmd"]).optional(),
|
|
1510
|
+
}))
|
|
1511
|
+
.min(1)
|
|
1512
|
+
.max(200)
|
|
1513
|
+
.describe("Files to create (max 200 per call)"),
|
|
1514
|
+
}, async ({ files }) => {
|
|
1515
|
+
const created = [];
|
|
1516
|
+
const errors = [];
|
|
1517
|
+
for (let i = 0; i < files.length; i++) {
|
|
1518
|
+
const f = files[i];
|
|
1519
|
+
try {
|
|
1520
|
+
const result = await createFile({
|
|
1521
|
+
title: f.title,
|
|
1522
|
+
content: f.content,
|
|
1523
|
+
destination: f.destination,
|
|
1524
|
+
projectId: f.project_id,
|
|
1525
|
+
folder: f.folder,
|
|
1526
|
+
tags: f.tags,
|
|
1527
|
+
dataDir,
|
|
1528
|
+
format: f.format,
|
|
1529
|
+
});
|
|
1530
|
+
created.push(annotateTokens(result));
|
|
1531
|
+
}
|
|
1532
|
+
catch (e) {
|
|
1533
|
+
errors.push({ index: i, title: f.title, error: e?.message ?? String(e) });
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return {
|
|
1537
|
+
content: [
|
|
1538
|
+
{
|
|
1539
|
+
type: "text",
|
|
1540
|
+
text: JSON.stringify({ created_count: created.length, error_count: errors.length, created, errors }, null, 2),
|
|
1541
|
+
},
|
|
1542
|
+
],
|
|
1543
|
+
};
|
|
1544
|
+
});
|
|
1545
|
+
server.tool("delete_files", "DESTRUCTIVE batch — delete up to 500 files by ID in one call. Same physical-deletion rules as `delete_file` (KB files unlinked from disk; project reference files only de-indexed). Per-ID failures isolated to `errors[]`; the batch keeps going — partial success is the norm. Not idempotent — unknown IDs surface as per-item errors. No external auth or rate limits. Returns `{deleted_count, error_count, deleted, errors}`. To preview the set before deleting, run `list_files` with the same filter and confirm the IDs.", {
|
|
1546
|
+
ids: z.array(z.number()).min(1).max(500).describe("File IDs to delete (max 500 per call)"),
|
|
1547
|
+
}, async ({ ids }) => {
|
|
1548
|
+
const deleted = [];
|
|
1549
|
+
const errors = [];
|
|
1550
|
+
for (const id of ids) {
|
|
1551
|
+
try {
|
|
1552
|
+
await deleteFile(id, dataDir);
|
|
1553
|
+
deleted.push(id);
|
|
1554
|
+
}
|
|
1555
|
+
catch (e) {
|
|
1556
|
+
errors.push({ id, error: e?.message ?? String(e) });
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
content: [
|
|
1561
|
+
{
|
|
1562
|
+
type: "text",
|
|
1563
|
+
text: JSON.stringify({ deleted_count: deleted.length, error_count: errors.length, deleted, errors }, null, 2),
|
|
1564
|
+
},
|
|
1565
|
+
],
|
|
1566
|
+
};
|
|
1567
|
+
});
|
|
1568
|
+
server.tool("tag_search_results", "Bulk-tag — run an FTS `search` and append `add_tags` to every matching file in one call. Side effect: each match gets `addTags` applied (additive, idempotent per tag); the matched files themselves are NOT modified beyond their tag links. Per-file failures isolated to `errors[]`. No external auth or rate limits. There is NO dry-run flag, so ALWAYS run `search` with the same query first to verify the match set before tagging. The `tags[]` filter requires existing tags to ALL match (it scopes the search; it does not control which tags get added). Returns `{matched_count, tagged_count, tags_applied, tagged_ids, errors}`.", {
|
|
1569
|
+
query: z.string().describe("Full-text search query"),
|
|
1570
|
+
add_tags: z.array(z.string()).min(1).describe("Tags to add to every matching file"),
|
|
1571
|
+
project_id: z.number().nullable().optional(),
|
|
1572
|
+
tags: z.array(z.string()).optional().describe("Filter — only matches that already carry ALL of these tags"),
|
|
1573
|
+
favorite: z.boolean().optional(),
|
|
1574
|
+
}, async ({ query, add_tags, project_id, tags, favorite }) => {
|
|
1575
|
+
const filters = { query };
|
|
1576
|
+
if (project_id !== undefined)
|
|
1577
|
+
filters.project_id = project_id;
|
|
1578
|
+
if (tags !== undefined)
|
|
1579
|
+
filters.tags = tags;
|
|
1580
|
+
if (favorite !== undefined)
|
|
1581
|
+
filters.favorite = favorite;
|
|
1582
|
+
let matches;
|
|
1583
|
+
try {
|
|
1584
|
+
matches = search(filters);
|
|
1585
|
+
}
|
|
1586
|
+
catch (e) {
|
|
1587
|
+
if (e instanceof FtsQueryError) {
|
|
1588
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1589
|
+
}
|
|
1590
|
+
throw e;
|
|
1591
|
+
}
|
|
1592
|
+
const tagged = [];
|
|
1593
|
+
const errors = [];
|
|
1594
|
+
for (const m of matches) {
|
|
1595
|
+
try {
|
|
1596
|
+
addTags(m.id, add_tags);
|
|
1597
|
+
tagged.push(m.id);
|
|
1598
|
+
}
|
|
1599
|
+
catch (e) {
|
|
1600
|
+
errors.push({ id: m.id, error: e?.message ?? String(e) });
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
content: [
|
|
1605
|
+
{
|
|
1606
|
+
type: "text",
|
|
1607
|
+
text: JSON.stringify({
|
|
1608
|
+
matched_count: matches.length,
|
|
1609
|
+
tagged_count: tagged.length,
|
|
1610
|
+
tags_applied: add_tags,
|
|
1611
|
+
tagged_ids: tagged,
|
|
1612
|
+
errors,
|
|
1613
|
+
}, null, 2),
|
|
1614
|
+
},
|
|
1615
|
+
],
|
|
1616
|
+
};
|
|
1617
|
+
});
|
|
1618
|
+
server.tool("read_file_by_path", "Look up a file by its absolute on-disk path and return the same shape as `read_file`. The path must EXACTLY match what Kontexta indexed — no symlink resolution, no path normalisation beyond what the OS does, no trailing-slash tolerance. Returns isError if no row matches (the file may exist on disk but not be indexed — try `refresh_index`). Read-only; no side effects, auth, or rate limits. Use when an agent has a path from its working directory but no file ID; if you have the ID, prefer `read_file`.", {
|
|
1619
|
+
path: z.string().describe("Absolute path on disk (must match the path stored in Kontexta)"),
|
|
1620
|
+
}, async ({ path }) => {
|
|
1621
|
+
try {
|
|
1622
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1623
|
+
throw new Error("path is required");
|
|
1624
|
+
}
|
|
1625
|
+
const row = getDatabase()
|
|
1626
|
+
.prepare("SELECT id FROM files WHERE path = ?")
|
|
1627
|
+
.get(path);
|
|
1628
|
+
if (!row) {
|
|
1629
|
+
return {
|
|
1630
|
+
isError: true,
|
|
1631
|
+
content: [
|
|
1632
|
+
{
|
|
1633
|
+
type: "text",
|
|
1634
|
+
text: JSON.stringify({ error: `No file indexed at path: ${path}` }, null, 2),
|
|
1635
|
+
},
|
|
1636
|
+
],
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
const result = readFile(row.id);
|
|
1640
|
+
return {
|
|
1641
|
+
content: [{ type: "text", text: JSON.stringify(annotateTokens(result), null, 2) }],
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
catch (e) {
|
|
1645
|
+
return {
|
|
1646
|
+
isError: true,
|
|
1647
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
server.tool("stats", "Aggregate counts for a scope: `file_count`, `untagged_count`, `favorite_count`, `top_tags`. With `project_id` omitted (everything), also returns `by_project` breakdown. `include_token_total: true` stat()s every matching file on disk to compute a body-size estimate — measurably slower on large vaults; default false. `project_id: null` = KB only; omit = all. Read-only; no side effects, auth, or rate limits. Use as a cheap dashboard or to spot untagged content for cleanup; for live disk-vs-index drift use `diff_against_disk`.", {
|
|
1652
|
+
project_id: z.number().nullable().optional().describe("Filter to a single project. Pass null for KB-only. Omit for everything."),
|
|
1653
|
+
top_tags: z.number().int().positive().max(100).optional().describe("How many top tags to return (default 10)"),
|
|
1654
|
+
include_token_total: z.boolean().optional().describe("If true, stat every matching file on disk to compute total est_tokens. Default false (cheap)."),
|
|
1655
|
+
}, async ({ project_id, top_tags, include_token_total }) => {
|
|
1656
|
+
try {
|
|
1657
|
+
const db = getDatabase();
|
|
1658
|
+
const limit = top_tags ?? 10;
|
|
1659
|
+
let scopeWhere = "";
|
|
1660
|
+
const scopeParams = [];
|
|
1661
|
+
if (project_id === null) {
|
|
1662
|
+
scopeWhere = "WHERE files.project_id IS NULL";
|
|
1663
|
+
}
|
|
1664
|
+
else if (typeof project_id === "number") {
|
|
1665
|
+
scopeWhere = "WHERE files.project_id = ?";
|
|
1666
|
+
scopeParams.push(project_id);
|
|
1667
|
+
}
|
|
1668
|
+
const fileCount = db
|
|
1669
|
+
.prepare(`SELECT COUNT(*) AS n FROM files ${scopeWhere}`)
|
|
1670
|
+
.get(...scopeParams).n;
|
|
1671
|
+
const untaggedCount = db
|
|
1672
|
+
.prepare(`SELECT COUNT(*) AS n FROM files ${scopeWhere}${scopeWhere ? " AND" : "WHERE"} files.id NOT IN (SELECT DISTINCT file_id FROM file_tags)`)
|
|
1673
|
+
.get(...scopeParams).n;
|
|
1674
|
+
const favoriteCount = db
|
|
1675
|
+
.prepare(`SELECT COUNT(*) AS n FROM files ${scopeWhere}${scopeWhere ? " AND" : "WHERE"} files.id IN (SELECT file_id FROM favorites)`)
|
|
1676
|
+
.get(...scopeParams).n;
|
|
1677
|
+
const topTags = db
|
|
1678
|
+
.prepare(`SELECT t.name, COUNT(*) AS count
|
|
1679
|
+
FROM file_tags ft
|
|
1680
|
+
JOIN tags t ON t.id = ft.tag_id
|
|
1681
|
+
JOIN files ON files.id = ft.file_id
|
|
1682
|
+
${scopeWhere}
|
|
1683
|
+
GROUP BY t.id
|
|
1684
|
+
ORDER BY count DESC, t.name ASC
|
|
1685
|
+
LIMIT ?`)
|
|
1686
|
+
.all(...scopeParams, limit);
|
|
1687
|
+
const byProject = project_id === undefined
|
|
1688
|
+
? db
|
|
1689
|
+
.prepare(`SELECT p.id, p.name, COUNT(files.id) AS files
|
|
1690
|
+
FROM projects p
|
|
1691
|
+
LEFT JOIN files ON files.project_id = p.id
|
|
1692
|
+
GROUP BY p.id
|
|
1693
|
+
ORDER BY files DESC, p.name ASC`)
|
|
1694
|
+
.all()
|
|
1695
|
+
: null;
|
|
1696
|
+
let totalEstTokens = null;
|
|
1697
|
+
if (include_token_total) {
|
|
1698
|
+
const rows = db
|
|
1699
|
+
.prepare(`SELECT path FROM files ${scopeWhere}`)
|
|
1700
|
+
.all(...scopeParams);
|
|
1701
|
+
let total = 0;
|
|
1702
|
+
for (const r of rows) {
|
|
1703
|
+
try {
|
|
1704
|
+
const sz = statSync(r.path).size;
|
|
1705
|
+
total += Math.max(1, Math.ceil(sz / 4));
|
|
1706
|
+
}
|
|
1707
|
+
catch { }
|
|
1708
|
+
}
|
|
1709
|
+
totalEstTokens = total;
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
content: [
|
|
1713
|
+
{
|
|
1714
|
+
type: "text",
|
|
1715
|
+
text: JSON.stringify({
|
|
1716
|
+
scope: project_id === undefined
|
|
1717
|
+
? "all"
|
|
1718
|
+
: project_id === null
|
|
1719
|
+
? "knowledge_base"
|
|
1720
|
+
: `project:${project_id}`,
|
|
1721
|
+
file_count: fileCount,
|
|
1722
|
+
untagged_count: untaggedCount,
|
|
1723
|
+
favorite_count: favoriteCount,
|
|
1724
|
+
top_tags: topTags,
|
|
1725
|
+
...(byProject ? { by_project: byProject } : {}),
|
|
1726
|
+
...(totalEstTokens !== null ? { total_est_tokens: totalEstTokens } : {}),
|
|
1727
|
+
rules_warning: getAgentRulesWarning(project_id),
|
|
1728
|
+
}, null, 2),
|
|
1729
|
+
},
|
|
1730
|
+
],
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
catch (e) {
|
|
1734
|
+
return {
|
|
1735
|
+
isError: true,
|
|
1736
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
server.tool("suggest_tags", "Propose tags for a file by mining the existing tag corpus via FTS — picks distinctive terms from the file (≥4 chars, stopword-filtered) and returns tags applied to other files that score high on those terms. No LLM, no network. Already-applied tags are excluded so the suggestions are net-new. Read-only; no side effects, auth, or rate limits. Returns `{file_id, path, existing_tags, suggestions: [{tag, score, sources}]}`. Empty suggestions = no distinctive terms or no overlap with the existing taxonomy yet — bootstrap with `add_tags` first. Default limit 10, max 50. Suggestions are NOT auto-applied.", {
|
|
1741
|
+
file_id: z.number().describe("File ID to suggest tags for"),
|
|
1742
|
+
limit: z.number().int().positive().max(50).optional().describe("Max suggestions to return (default 10)"),
|
|
1743
|
+
}, async ({ file_id, limit }) => {
|
|
1744
|
+
try {
|
|
1745
|
+
const k = limit ?? 10;
|
|
1746
|
+
const file = readFile(file_id);
|
|
1747
|
+
const db = getDatabase();
|
|
1748
|
+
const existing = new Set(db
|
|
1749
|
+
.prepare(`SELECT t.name FROM file_tags ft JOIN tags t ON t.id = ft.tag_id WHERE ft.file_id = ?`)
|
|
1750
|
+
.all(file_id).map((r) => r.name));
|
|
1751
|
+
const STOPWORDS = new Set([
|
|
1752
|
+
"the", "and", "for", "with", "from", "this", "that", "have", "into", "your",
|
|
1753
|
+
"more", "than", "then", "when", "what", "which", "there", "these", "those",
|
|
1754
|
+
"where", "while", "also", "been", "were", "would", "should", "could", "about",
|
|
1755
|
+
]);
|
|
1756
|
+
const tokens = (file.title + " " + file.content)
|
|
1757
|
+
.toLowerCase()
|
|
1758
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
1759
|
+
.split(/\s+/)
|
|
1760
|
+
.filter((t) => t.length >= 4 && !STOPWORDS.has(t));
|
|
1761
|
+
const seen = new Set();
|
|
1762
|
+
const distinctive = [];
|
|
1763
|
+
for (const t of tokens) {
|
|
1764
|
+
if (!seen.has(t)) {
|
|
1765
|
+
seen.add(t);
|
|
1766
|
+
distinctive.push(t);
|
|
1767
|
+
if (distinctive.length >= 40)
|
|
1768
|
+
break;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (distinctive.length === 0) {
|
|
1772
|
+
return {
|
|
1773
|
+
content: [{ type: "text", text: JSON.stringify({ suggestions: [], reason: "no distinctive terms in file" }, null, 2) }],
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
const ftsQuery = distinctive.map((t) => `"${t}"`).join(" OR ");
|
|
1777
|
+
const rows = db
|
|
1778
|
+
.prepare(`SELECT t.name, fts_index.rank
|
|
1779
|
+
FROM fts_index
|
|
1780
|
+
JOIN file_tags ft ON ft.file_id = fts_index.rowid
|
|
1781
|
+
JOIN tags t ON t.id = ft.tag_id
|
|
1782
|
+
WHERE fts_index MATCH ?
|
|
1783
|
+
AND ft.file_id != ?
|
|
1784
|
+
LIMIT 500`)
|
|
1785
|
+
.all(ftsQuery, file_id);
|
|
1786
|
+
const scores = new Map();
|
|
1787
|
+
for (const r of rows) {
|
|
1788
|
+
if (existing.has(r.name))
|
|
1789
|
+
continue;
|
|
1790
|
+
const w = -r.rank;
|
|
1791
|
+
const cur = scores.get(r.name);
|
|
1792
|
+
if (cur) {
|
|
1793
|
+
cur.score += w;
|
|
1794
|
+
cur.sources += 1;
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
scores.set(r.name, { score: w, sources: 1 });
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const suggestions = [...scores.entries()]
|
|
1801
|
+
.map(([name, { score, sources }]) => ({ tag: name, score: Number(score.toFixed(2)), sources }))
|
|
1802
|
+
.sort((a, b) => b.score - a.score || b.sources - a.sources || a.tag.localeCompare(b.tag))
|
|
1803
|
+
.slice(0, k);
|
|
1804
|
+
return {
|
|
1805
|
+
content: [
|
|
1806
|
+
{
|
|
1807
|
+
type: "text",
|
|
1808
|
+
text: JSON.stringify({ file_id, path: file.path, existing_tags: [...existing], suggestions }, null, 2),
|
|
1809
|
+
},
|
|
1810
|
+
],
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
catch (e) {
|
|
1814
|
+
return {
|
|
1815
|
+
isError: true,
|
|
1816
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
server.tool("diff_against_disk", "Diagnose drift between one file's disk content and its FTS index. Status is one of `in_sync`, `diverged`, `disk_unreadable`, or `no_index_row`. On divergence returns sizes, line counts, the first divergent line number, and the disk vs index sample for that line — NOT a full diff (use `get_diff` for full diffs between commits). Read-only; no side effects, auth, or rate limits. Use when search results look stale; if status is `diverged` or `no_index_row`, run `refresh_index` to fix.", {
|
|
1821
|
+
file_id: z.number().describe("File ID"),
|
|
1822
|
+
}, async ({ file_id }) => {
|
|
1823
|
+
try {
|
|
1824
|
+
const db = getDatabase();
|
|
1825
|
+
const row = db
|
|
1826
|
+
.prepare("SELECT path FROM files WHERE id = ?")
|
|
1827
|
+
.get(file_id);
|
|
1828
|
+
if (!row)
|
|
1829
|
+
throw new Error(`File not found: ${file_id}`);
|
|
1830
|
+
let diskContent = null;
|
|
1831
|
+
let diskError = null;
|
|
1832
|
+
try {
|
|
1833
|
+
statSync(row.path);
|
|
1834
|
+
diskContent = readFileSync(row.path, "utf8");
|
|
1835
|
+
}
|
|
1836
|
+
catch (e) {
|
|
1837
|
+
diskError = e?.message ?? String(e);
|
|
1838
|
+
}
|
|
1839
|
+
const ftsRow = db
|
|
1840
|
+
.prepare("SELECT content FROM fts_index WHERE rowid = ?")
|
|
1841
|
+
.get(file_id);
|
|
1842
|
+
const indexContent = ftsRow?.content ?? null;
|
|
1843
|
+
if (diskContent === null) {
|
|
1844
|
+
return {
|
|
1845
|
+
content: [
|
|
1846
|
+
{
|
|
1847
|
+
type: "text",
|
|
1848
|
+
text: JSON.stringify({
|
|
1849
|
+
file_id,
|
|
1850
|
+
path: row.path,
|
|
1851
|
+
status: "disk_unreadable",
|
|
1852
|
+
disk_error: diskError,
|
|
1853
|
+
index_size: indexContent ? Buffer.byteLength(indexContent, "utf8") : null,
|
|
1854
|
+
}, null, 2),
|
|
1855
|
+
},
|
|
1856
|
+
],
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
if (indexContent === null) {
|
|
1860
|
+
return {
|
|
1861
|
+
content: [
|
|
1862
|
+
{
|
|
1863
|
+
type: "text",
|
|
1864
|
+
text: JSON.stringify({
|
|
1865
|
+
file_id,
|
|
1866
|
+
path: row.path,
|
|
1867
|
+
status: "no_index_row",
|
|
1868
|
+
disk_size: Buffer.byteLength(diskContent, "utf8"),
|
|
1869
|
+
hint: "FTS index missing for this file. Touch it and let the watcher reingest, or run an explicit refresh.",
|
|
1870
|
+
}, null, 2),
|
|
1871
|
+
},
|
|
1872
|
+
],
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
if (diskContent === indexContent) {
|
|
1876
|
+
return {
|
|
1877
|
+
content: [
|
|
1878
|
+
{
|
|
1879
|
+
type: "text",
|
|
1880
|
+
text: JSON.stringify({
|
|
1881
|
+
file_id,
|
|
1882
|
+
path: row.path,
|
|
1883
|
+
status: "in_sync",
|
|
1884
|
+
size_bytes: Buffer.byteLength(diskContent, "utf8"),
|
|
1885
|
+
}, null, 2),
|
|
1886
|
+
},
|
|
1887
|
+
],
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
const diskLines = diskContent.split("\n");
|
|
1891
|
+
const idxLines = indexContent.split("\n");
|
|
1892
|
+
let firstDiffLine = -1;
|
|
1893
|
+
const maxLines = Math.max(diskLines.length, idxLines.length);
|
|
1894
|
+
for (let i = 0; i < maxLines; i++) {
|
|
1895
|
+
if (diskLines[i] !== idxLines[i]) {
|
|
1896
|
+
firstDiffLine = i + 1;
|
|
1897
|
+
break;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return {
|
|
1901
|
+
content: [
|
|
1902
|
+
{
|
|
1903
|
+
type: "text",
|
|
1904
|
+
text: JSON.stringify({
|
|
1905
|
+
file_id,
|
|
1906
|
+
path: row.path,
|
|
1907
|
+
status: "diverged",
|
|
1908
|
+
disk_size: Buffer.byteLength(diskContent, "utf8"),
|
|
1909
|
+
index_size: Buffer.byteLength(indexContent, "utf8"),
|
|
1910
|
+
disk_line_count: diskLines.length,
|
|
1911
|
+
index_line_count: idxLines.length,
|
|
1912
|
+
first_diff_line: firstDiffLine,
|
|
1913
|
+
disk_sample: firstDiffLine > 0 ? (diskLines[firstDiffLine - 1] ?? null) : null,
|
|
1914
|
+
index_sample: firstDiffLine > 0 ? (idxLines[firstDiffLine - 1] ?? null) : null,
|
|
1915
|
+
}, null, 2),
|
|
1916
|
+
},
|
|
1917
|
+
],
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
catch (e) {
|
|
1921
|
+
return {
|
|
1922
|
+
isError: true,
|
|
1923
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
server.tool("refresh_index", "Reconcile the FTS index against disk. For a project (`project_id` set), re-runs `discoverFiles`. For the KB (`project_id` null/omitted), walks `knowledge/`, ingests new .md files, reindexes any whose content hash drifted, and PRUNES rows for files no longer on disk. SIDE-EFFECTFUL: writes/updates/deletes file and FTS rows (the prune is destructive on stale index rows but never deletes files from disk). Idempotent — running twice is a near no-op. Skips files >5MB and standard junk dirs (`node_modules`, `.git`, `dist`, `build`, etc.). No external auth or rate limits. Returns `{scope, newly_indexed, refreshed, pruned}`. Use after editing files outside Kontexta, or when `diff_against_disk` reports drift.", {
|
|
1928
|
+
project_id: z.number().nullable().optional().describe("Project ID. Pass null or omit to reindex the Knowledge Base."),
|
|
1929
|
+
}, async ({ project_id }) => {
|
|
1930
|
+
try {
|
|
1931
|
+
const result = await refreshIndex(project_id ?? null, dataDir);
|
|
1932
|
+
return {
|
|
1933
|
+
content: [
|
|
1934
|
+
{
|
|
1935
|
+
type: "text",
|
|
1936
|
+
text: JSON.stringify(result, null, 2),
|
|
1937
|
+
},
|
|
1938
|
+
],
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
catch (e) {
|
|
1942
|
+
return {
|
|
1943
|
+
isError: true,
|
|
1944
|
+
content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }],
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
server.tool("whats_new", "List files created or modified since a checkpoint. `since` accepts ISO-8601 (`2025-01-15T00:00:00Z`) or relative durations (`1h`, `7d`, `2w`); invalid formats throw. Read-only; no side effects, auth, or rate limits. Returns annotated rows plus aggregate `total_est_tokens` so you can decide what to read next. CAVEAT: hard-deleted files are NOT surfaced — only mtime-driven changes. Defaults: include_tags=true, limit=200. `project_id: null` = KB only; omit = everything. Use at session start to catch up.", {
|
|
1949
|
+
since: z.string().describe("ISO 8601 timestamp or relative duration (e.g. \"1h\", \"7d\", \"2w\")."),
|
|
1950
|
+
project_id: z.number().nullable().optional().describe("Filter to a single project. Pass null for knowledge-base-only files. Omit for all."),
|
|
1951
|
+
include_tags: z.boolean().optional().describe("Attach tags[] to each file. Default true."),
|
|
1952
|
+
limit: z.number().optional().describe("Max files returned. Default 200."),
|
|
1953
|
+
}, async ({ since, project_id, include_tags, limit }) => {
|
|
1954
|
+
try {
|
|
1955
|
+
const opts = { since };
|
|
1956
|
+
if (project_id !== undefined)
|
|
1957
|
+
opts.project_id = project_id;
|
|
1958
|
+
if (include_tags !== undefined)
|
|
1959
|
+
opts.include_tags = include_tags;
|
|
1960
|
+
if (limit !== undefined)
|
|
1961
|
+
opts.limit = limit;
|
|
1962
|
+
const result = whatsNew(opts);
|
|
1963
|
+
const annotated = result.files.map(annotateTokens);
|
|
1964
|
+
const total_est_tokens = annotated.reduce((s, f) => s + (f.est_tokens ?? 0), 0);
|
|
1965
|
+
return {
|
|
1966
|
+
content: [
|
|
1967
|
+
{
|
|
1968
|
+
type: "text",
|
|
1969
|
+
text: JSON.stringify({
|
|
1970
|
+
since: result.since,
|
|
1971
|
+
until: result.until,
|
|
1972
|
+
count: result.count,
|
|
1973
|
+
total_est_tokens,
|
|
1974
|
+
files: annotated,
|
|
1975
|
+
}, null, 2),
|
|
1976
|
+
},
|
|
1977
|
+
],
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
catch (e) {
|
|
1981
|
+
return {
|
|
1982
|
+
isError: true,
|
|
1983
|
+
content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }],
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
server.tool("project_map", "Return a compact indented outline of folders, file titles, tags, and IDs in a single dense block — substantially fewer tokens than the equivalent `list_files` JSON for the same scope. Read-only; no side effects, auth, or rate limits. Capped at `max_lines` (default 5000); the response reports `est_tokens` and emits a `warning` field if it exceeds `KONTEXTA_PROJECT_TOKEN_WARN`. `project_id: null` = KB only; omit = everything. Defaults: include_tags=true, show_titles=true. Use to orient yourself in an unfamiliar vault or project; for keyword lookup use `search`.", {
|
|
1988
|
+
project_id: z.number().nullable().optional().describe("Restrict to a single project. Pass null for knowledge-base-only files. Omit for everything."),
|
|
1989
|
+
include_tags: z.boolean().optional().describe("Append #tags inline. Default true. Set false to shrink the outline."),
|
|
1990
|
+
show_titles: z.boolean().optional().describe("Show file titles instead of filenames. Default true."),
|
|
1991
|
+
max_lines: z.number().optional().describe("Hard cap on output lines (each line ≈ one folder or file). Default 5000."),
|
|
1992
|
+
}, async ({ project_id, include_tags, show_titles, max_lines }) => {
|
|
1993
|
+
const opts = { dataDir };
|
|
1994
|
+
if (project_id !== undefined)
|
|
1995
|
+
opts.project_id = project_id;
|
|
1996
|
+
if (include_tags !== undefined)
|
|
1997
|
+
opts.include_tags = include_tags;
|
|
1998
|
+
if (show_titles !== undefined)
|
|
1999
|
+
opts.show_titles = show_titles;
|
|
2000
|
+
if (max_lines !== undefined)
|
|
2001
|
+
opts.max_lines = max_lines;
|
|
2002
|
+
const result = projectMap(opts);
|
|
2003
|
+
const warning = tokenWarning(result.est_tokens);
|
|
2004
|
+
return {
|
|
2005
|
+
content: [
|
|
2006
|
+
{
|
|
2007
|
+
type: "text",
|
|
2008
|
+
text: JSON.stringify({
|
|
2009
|
+
stats: result.stats,
|
|
2010
|
+
est_tokens: result.est_tokens,
|
|
2011
|
+
outline: result.outline,
|
|
2012
|
+
...(warning ? { warning } : {}),
|
|
2013
|
+
rules_warning: getAgentRulesWarning(opts.project_id),
|
|
2014
|
+
}, null, 2),
|
|
2015
|
+
},
|
|
2016
|
+
],
|
|
2017
|
+
};
|
|
2018
|
+
});
|
|
2019
|
+
server.resource("All Projects", "kontexta://projects", {
|
|
2020
|
+
description: "List of all registered projects",
|
|
2021
|
+
mimeType: "application/json",
|
|
2022
|
+
}, async () => {
|
|
2023
|
+
const projects = listProjects();
|
|
2024
|
+
const enriched = projects.map((p) => {
|
|
2025
|
+
const contextFiles = p.path ? detectAgentContextFiles(p.path) : [];
|
|
2026
|
+
const statuses = p.path ? checkAgentRulesStatus(p.path, contextFiles) : [];
|
|
2027
|
+
const outdated = statuses.filter((s) => !s.upToDate);
|
|
2028
|
+
return {
|
|
2029
|
+
...p,
|
|
2030
|
+
agent_rules: {
|
|
2031
|
+
status: outdated.length > 0 ? "outdated" : contextFiles.length > 0 ? "up_to_date" : "none",
|
|
2032
|
+
files: statuses,
|
|
2033
|
+
latest_version: RULE_BLOCK_VERSION,
|
|
2034
|
+
},
|
|
2035
|
+
};
|
|
2036
|
+
});
|
|
2037
|
+
return {
|
|
2038
|
+
contents: [
|
|
2039
|
+
{
|
|
2040
|
+
uri: "kontexta://projects",
|
|
2041
|
+
mimeType: "application/json",
|
|
2042
|
+
text: JSON.stringify(enriched, null, 2),
|
|
2043
|
+
},
|
|
2044
|
+
],
|
|
2045
|
+
};
|
|
2046
|
+
});
|
|
2047
|
+
server.resource("File Content", new ResourceTemplate("kontexta://files/{id}", {
|
|
2048
|
+
list: undefined,
|
|
2049
|
+
}), {
|
|
2050
|
+
description: "Content of a specific file by ID",
|
|
2051
|
+
mimeType: "text/markdown",
|
|
2052
|
+
}, async (uri, variables) => {
|
|
2053
|
+
const idValue = Array.isArray(variables.id) ? variables.id[0] : variables.id;
|
|
2054
|
+
const id = parseInt(idValue, 10);
|
|
2055
|
+
const file = readFile(id);
|
|
2056
|
+
return {
|
|
2057
|
+
contents: [
|
|
2058
|
+
{
|
|
2059
|
+
uri: uri.toString(),
|
|
2060
|
+
mimeType: "text/markdown",
|
|
2061
|
+
text: file.content,
|
|
2062
|
+
},
|
|
2063
|
+
],
|
|
2064
|
+
};
|
|
2065
|
+
});
|
|
2066
|
+
server.tool("list_hands", "List every Hands command tool currently registered, with project scope, tool name, danger level, confirmation flag, and description. Hands tools come from per-project `kontexta.json` files loaded at register time. Read-only; no side effects, auth, or rate limits. Use to discover what side-effectful project commands the agent is permitted to run; for the `kontexta.json` schema see `describe_hands_schema`; reload after editing one with `reload_hands`.", {}, async () => {
|
|
2067
|
+
const items = handsRegistry.list();
|
|
2068
|
+
return { content: [{ type: "text", text: JSON.stringify({ hands: items }, null, 2) }] };
|
|
2069
|
+
});
|
|
2070
|
+
server.tool("reload_hands", "Re-scan every registered project's `kontexta.json` and rebuild the live Hands tool registry — newly-declared tools become callable immediately, removed tools disappear from `tools/list`. SIDE EFFECT is on the running MCP session's tool inventory only (no disk writes). Idempotent. No external auth or rate limits. Takes no parameters. Returns per-project load results (counts of registered/disabled tools and any validation warnings). Use after editing a `kontexta.json` mid-session; for the schema see `describe_hands_schema`.", {}, async () => {
|
|
2071
|
+
const projects = listProjects()
|
|
2072
|
+
.filter((p) => p.path && existsSync(p.path))
|
|
2073
|
+
.map((p) => ({ name: p.name, root: p.path }));
|
|
2074
|
+
const r = handsRegistry.reloadAll(projects);
|
|
2075
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
2076
|
+
});
|
|
2077
|
+
server.tool("confirm_hand", "Approve and EXECUTE a previously-issued Hands invocation by its single-use approval token. The token is returned by any confirm-required Hands tool; tokens expire after 60 seconds and CANNOT be reused. Side effect equals whatever the underlying Hand does — this can be highly destructive (running arbitrary shell commands, modifying files, etc.), so only call when the user has authorised the pending action. The token IS the auth (no external auth, no rate limits). Invalid, expired, or already-consumed tokens return an inert text response, NOT an error.", { token: z.string().describe("The approval token from the pending response") }, async ({ token }) => {
|
|
2078
|
+
const pending = handsRegistry.getConfirmStore().consume(token);
|
|
2079
|
+
if (!pending) {
|
|
2080
|
+
return { content: [{ type: "text", text: "Token invalid, expired, or already consumed." }] };
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
const result = await pending.execute();
|
|
2084
|
+
return { content: [{ type: "text", text: formatExecResult(pending.toolName, result) }] };
|
|
2085
|
+
}
|
|
2086
|
+
catch (e) {
|
|
2087
|
+
return { content: [{ type: "text", text: `Execution failed after approval: ${e?.message ?? e}` }] };
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
server.tool("describe_hands_schema", "Return the complete authoring reference for `kontexta.json`: JSON schema, validation rules, security guarantees, limitations, and an annotated example. Static document — does not read any project file or DB row. Read-only; no side effects, auth, or rate limits. Takes no parameters. Use when helping a user write or fix a `kontexta.json`; to see the loaded tools themselves use `list_hands`; to apply edits use `reload_hands`.", {}, async () => ({ content: [{ type: "text", text: buildSchemaDoc() }] }));
|
|
2091
|
+
server.tool("get_profile", "Return the user profile stored in the Knowledge Base. The profile helps AI agents understand the user's context, role, preferences, and goals. Read-only; no side effects, auth, or rate limits. Returns existence status, full content, list of missing required sections, and a hint for new users. Use at session start to understand who you're working with.", {}, async () => {
|
|
2092
|
+
const result = handleGetProfile(dataDir);
|
|
2093
|
+
return {
|
|
2094
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2095
|
+
};
|
|
2096
|
+
});
|
|
2097
|
+
async function main() {
|
|
2098
|
+
const transport = new StdioServerTransport();
|
|
2099
|
+
{
|
|
2100
|
+
const projects = listProjects()
|
|
2101
|
+
.filter((p) => p.path && existsSync(p.path))
|
|
2102
|
+
.map((p) => ({ name: p.name, root: p.path }));
|
|
2103
|
+
const r = handsRegistry.reloadAll(projects);
|
|
2104
|
+
console.error(`Kontexta Hands: loaded ${r.perProject.length} projects, registered ${r.totalRegistered} tools (${r.totalDisabled} disabled)`);
|
|
2105
|
+
}
|
|
2106
|
+
registerJournalTools(server);
|
|
2107
|
+
registerCommitUpgradesTool(server);
|
|
2108
|
+
registerHousekeepTool(server);
|
|
2109
|
+
registerCalendarTools(server);
|
|
2110
|
+
await server.connect(transport);
|
|
2111
|
+
console.error("Kontexta MCP server running on stdio");
|
|
2112
|
+
}
|
|
2113
|
+
main().catch((error) => {
|
|
2114
|
+
console.error("Fatal error:", error);
|
|
2115
|
+
process.exit(1);
|
|
2116
|
+
});
|
|
2117
|
+
//# sourceMappingURL=index.js.map
|