rdstudio 0.2.0__py3-none-any.whl

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.
Files changed (251) hide show
  1. rdstudio/__init__.py +3 -0
  2. rdstudio/_node/rdstudio.mjs +59351 -0
  3. rdstudio/brief.py +46 -0
  4. rdstudio/build.py +218 -0
  5. rdstudio/classify.py +131 -0
  6. rdstudio/cli.py +369 -0
  7. rdstudio/config.py +115 -0
  8. rdstudio/gitlog.py +92 -0
  9. rdstudio/launcher.py +63 -0
  10. rdstudio/learner.py +187 -0
  11. rdstudio/mcp_server.py +313 -0
  12. rdstudio/okf.py +775 -0
  13. rdstudio/procedures.py +233 -0
  14. rdstudio/references.py +228 -0
  15. rdstudio/reports.py +75 -0
  16. rdstudio/scaffold.py +227 -0
  17. rdstudio/scopes.py +162 -0
  18. rdstudio/search.py +149 -0
  19. rdstudio/serve.py +214 -0
  20. rdstudio/store.py +161 -0
  21. rdstudio/templates/agents/critic.md +20 -0
  22. rdstudio/templates/agents/librarian.md +21 -0
  23. rdstudio/templates/agents/searcher.md +21 -0
  24. rdstudio/templates/agents_section.md +31 -0
  25. rdstudio/templates/claude_section.md +3 -0
  26. rdstudio/templates/knowledge/overview.md +20 -0
  27. rdstudio/templates/knowledge/tasks/bootstrap-knowledge-base.md +45 -0
  28. rdstudio/templates/skills/decision/SKILL.md +47 -0
  29. rdstudio/templates/skills/handoff/SKILL.md +18 -0
  30. rdstudio/templates/skills/ingest-ref/SKILL.md +32 -0
  31. rdstudio/templates/skills/lint-okf/SKILL.md +15 -0
  32. rdstudio/templates/skills/promote/SKILL.md +25 -0
  33. rdstudio/templates/skills/question/SKILL.md +33 -0
  34. rdstudio/templates/skills/record-okf/SKILL.md +61 -0
  35. rdstudio/templates/skills/report/SKILL.md +46 -0
  36. rdstudio/templates/skills/report/template.html +74 -0
  37. rdstudio/templates/skills/search-okf/SKILL.md +36 -0
  38. rdstudio/templates/skills/task/SKILL.md +51 -0
  39. rdstudio/web/app.js +140 -0
  40. rdstudio/web/fonts.css +48 -0
  41. rdstudio/web/icon-192.png +0 -0
  42. rdstudio/web/icon-512.png +0 -0
  43. rdstudio/web/index.html +109 -0
  44. rdstudio/web/js/data.js +125 -0
  45. rdstudio/web/js/diagrams.js +116 -0
  46. rdstudio/web/js/graph.js +396 -0
  47. rdstudio/web/js/knowledge.js +212 -0
  48. rdstudio/web/js/layout-worker.js +15 -0
  49. rdstudio/web/js/layout.js +226 -0
  50. rdstudio/web/js/learn.js +83 -0
  51. rdstudio/web/js/map.js +1293 -0
  52. rdstudio/web/js/markdown.js +173 -0
  53. rdstudio/web/js/pages.js +219 -0
  54. rdstudio/web/js/procedures.js +262 -0
  55. rdstudio/web/js/settings.js +95 -0
  56. rdstudio/web/js/util.js +99 -0
  57. rdstudio/web/report.css +35 -0
  58. rdstudio/web/report.js +104 -0
  59. rdstudio/web/style.css +566 -0
  60. rdstudio/web/sw.js +166 -0
  61. rdstudio/web/themes/cyber.css +139 -0
  62. rdstudio/web/themes/map.css +133 -0
  63. rdstudio/web/themes/notebook.css +134 -0
  64. rdstudio/web/themes/space.css +135 -0
  65. rdstudio/web/themes/studio.css +110 -0
  66. rdstudio/web/vendor/d3.min.js +2 -0
  67. rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-ext-wght-italic.woff2 +0 -0
  68. rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-ext-wght-normal.woff2 +0 -0
  69. rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-wght-italic.woff2 +0 -0
  70. rdstudio/web/vendor/fonts/atkinson-hyperlegible-next-latin-wght-normal.woff2 +0 -0
  71. rdstudio/web/vendor/fonts/caveat-latin-wght-normal.woff2 +0 -0
  72. rdstudio/web/vendor/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  73. rdstudio/web/vendor/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  74. rdstudio/web/vendor/fonts/ibm-plex-sans-latin-wght-italic.woff2 +0 -0
  75. rdstudio/web/vendor/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
  76. rdstudio/web/vendor/fonts/inter-latin-opsz-italic.woff2 +0 -0
  77. rdstudio/web/vendor/fonts/inter-latin-opsz-normal.woff2 +0 -0
  78. rdstudio/web/vendor/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
  79. rdstudio/web/vendor/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  80. rdstudio/web/vendor/fonts/literata-latin-ext-opsz-italic.woff2 +0 -0
  81. rdstudio/web/vendor/fonts/literata-latin-ext-opsz-normal.woff2 +0 -0
  82. rdstudio/web/vendor/fonts/literata-latin-opsz-italic.woff2 +0 -0
  83. rdstudio/web/vendor/fonts/literata-latin-opsz-normal.woff2 +0 -0
  84. rdstudio/web/vendor/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  85. rdstudio/web/vendor/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  86. rdstudio/web/vendor/fonts/source-serif-4-latin-opsz-italic.woff2 +0 -0
  87. rdstudio/web/vendor/fonts/source-serif-4-latin-opsz-normal.woff2 +0 -0
  88. rdstudio/web/vendor/hljs/github-dark.min.css +10 -0
  89. rdstudio/web/vendor/hljs/github.min.css +10 -0
  90. rdstudio/web/vendor/hljs/highlight.min.js +1262 -0
  91. rdstudio/web/vendor/hljs/julia.min.js +18 -0
  92. rdstudio/web/vendor/hljs/latex.min.js +33 -0
  93. rdstudio/web/vendor/hljs/matlab.min.js +15 -0
  94. rdstudio/web/vendor/katex/auto-render.min.js +1 -0
  95. rdstudio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  96. rdstudio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  97. rdstudio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  98. rdstudio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  99. rdstudio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  100. rdstudio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  101. rdstudio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  102. rdstudio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  103. rdstudio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  104. rdstudio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  105. rdstudio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  106. rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  107. rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  108. rdstudio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  109. rdstudio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  110. rdstudio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  111. rdstudio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  112. rdstudio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  113. rdstudio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  114. rdstudio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  115. rdstudio/web/vendor/katex/katex.min.css +1 -0
  116. rdstudio/web/vendor/katex/katex.min.js +1 -0
  117. rdstudio/web/vendor/licenses/atkinson-hyperlegible-next-OFL.txt +93 -0
  118. rdstudio/web/vendor/licenses/caveat-OFL.txt +93 -0
  119. rdstudio/web/vendor/licenses/d3-7.9.0.txt +13 -0
  120. rdstudio/web/vendor/licenses/highlightjs-cdn-assets-11.12.0.txt +29 -0
  121. rdstudio/web/vendor/licenses/ibm-plex-mono-OFL.txt +93 -0
  122. rdstudio/web/vendor/licenses/ibm-plex-sans-OFL.txt +93 -0
  123. rdstudio/web/vendor/licenses/inter-OFL.txt +93 -0
  124. rdstudio/web/vendor/licenses/jetbrains-mono-OFL.txt +93 -0
  125. rdstudio/web/vendor/licenses/katex-0.18.7.txt +21 -0
  126. rdstudio/web/vendor/licenses/literata-OFL.txt +93 -0
  127. rdstudio/web/vendor/licenses/markdown-it-15.0.2.txt +22 -0
  128. rdstudio/web/vendor/licenses/markdown-it-footnote-4.0.0.txt +22 -0
  129. rdstudio/web/vendor/licenses/markdown-it-texmath-1.0.0.txt +21 -0
  130. rdstudio/web/vendor/licenses/mermaid-12.0.0.txt +21 -0
  131. rdstudio/web/vendor/licenses/source-sans-3-OFL.txt +93 -0
  132. rdstudio/web/vendor/licenses/source-serif-4-OFL.txt +93 -0
  133. rdstudio/web/vendor/licenses/vega-6.4.0.txt +27 -0
  134. rdstudio/web/vendor/licenses/vega-embed-7.2.0.txt +27 -0
  135. rdstudio/web/vendor/licenses/vega-lite-6.4.3.txt +27 -0
  136. rdstudio/web/vendor/markdown-it-footnote.min.js +2 -0
  137. rdstudio/web/vendor/markdown-it.min.js +14 -0
  138. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-DGLNOSUI.mjs +1 -0
  139. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-HF4J73XS.mjs +1 -0
  140. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-IU6H5EVY.mjs +36 -0
  141. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-FSX6DT2V.mjs +140 -0
  142. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-YNRVYILN.mjs +38 -0
  143. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-22SYA5HA.mjs +1 -0
  144. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-2FTYKNOQ.mjs +1 -0
  145. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-36VM3LGA.mjs +1 -0
  146. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SEI7E7V.mjs +1 -0
  147. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-3T2MNPEU.mjs +1 -0
  148. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-44AU3KAP.mjs +174 -0
  149. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-4MZOTL6B.mjs +1 -0
  150. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-52IMQVNL.mjs +1 -0
  151. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-5NYYYZGL.mjs +1 -0
  152. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-BOZ75642.mjs +15 -0
  153. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-CVBX6EWK.mjs +1 -0
  154. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-D36F7FYG.mjs +14 -0
  155. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-DEGLBX6O.mjs +213 -0
  156. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-F6PIGZHG.mjs +1 -0
  157. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-FE53TMRB.mjs +133 -0
  158. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-FNR2RGIF.mjs +1 -0
  159. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDOATIN3.mjs +32 -0
  160. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7BX327W.mjs +2 -0
  161. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-HI3EPXIC.mjs +2 -0
  162. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-JCBDXJ5W.mjs +1 -0
  163. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-JGXDHUYS.mjs +1 -0
  164. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-L2B47AAZ.mjs +1 -0
  165. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-LIEGGFIX.mjs +1 -0
  166. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-LLWYPJEZ.mjs +1 -0
  167. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MFYRHTYG.mjs +1 -0
  168. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MIE3LDYP.mjs +1 -0
  169. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MLQVGYRW.mjs +1 -0
  170. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-MNT3HHAC.mjs +1 -0
  171. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-NRRTYJAG.mjs +1 -0
  172. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-OCMJQKYE.mjs +10 -0
  173. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-PKJCTQEK.mjs +339 -0
  174. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QADHU5CF.mjs +2 -0
  175. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QVIEFLGF.mjs +1 -0
  176. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-QX7LGXZU.mjs +1 -0
  177. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-R2KH53MI.mjs +1 -0
  178. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-RXM7Z7NK.mjs +2 -0
  179. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-S4TE5DX6.mjs +1 -0
  180. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-SDIBIOXT.mjs +1 -0
  181. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TDFULVG6.mjs +1 -0
  182. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TECWHJ6D.mjs +62 -0
  183. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-TINYC7RT.mjs +70 -0
  184. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-V3KJR4XU.mjs +1 -0
  185. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTMXW4ES.mjs +1 -0
  186. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-Y5NT6LW5.mjs +88 -0
  187. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-Y5O5OPKC.mjs +3 -0
  188. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-YZG4BXY6.mjs +1 -0
  189. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZZ52EIBP.mjs +1 -0
  190. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-VV4INRHB.mjs +217 -0
  191. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-MSIMRU2B.mjs +1 -0
  192. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-KMPDV6GS.mjs +1 -0
  193. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-KTGLXFXK.mjs +62 -0
  194. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/dagre-QJJCSHY4.mjs +4 -0
  195. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-2ZIM67RP.mjs +3 -0
  196. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-FYHH5O6R.mjs +24 -0
  197. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-LYLDACMS.mjs +200 -0
  198. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-TYT3HRBC.mjs +24 -0
  199. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-XVIN6YVD.mjs +30 -0
  200. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/diagram-ZO2NLLYC.mjs +41 -0
  201. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-TU3FGH35.mjs +1 -0
  202. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/elk-GHAXNSLK.mjs +27 -0
  203. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-XID3VV23.mjs +99 -0
  204. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-P5QJSWSK.mjs +1 -0
  205. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-WPHQAKTU.mjs +1 -0
  206. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-QACGSKG6.mjs +292 -0
  207. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-V5NEQQYP.mjs +1 -0
  208. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-EB76O4HC.mjs +106 -0
  209. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-IGBHH2L7.mjs +1 -0
  210. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-343ZKIDN.mjs +2 -0
  211. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-UEEU5D75.mjs +70 -0
  212. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-4QTZOVLF.mjs +139 -0
  213. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-VEJL4RG2.mjs +89 -0
  214. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/katex-QVRI4OIV.mjs +257 -0
  215. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-C5K5ZT7T.mjs +96 -0
  216. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-PJTKDECJ.mjs +1 -0
  217. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-FHIILVJB.mjs +1 -0
  218. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-U3JFQXZN.mjs +1 -0
  219. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-S76BU6V7.mjs +39 -0
  220. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-HZBLWUZE.mjs +7 -0
  221. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-7T2UW32D.mjs +1 -0
  222. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-DZY7ARDL.mjs +1 -0
  223. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-LWS535ZK.mjs +1 -0
  224. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-EA3S4ZFM.mjs +1 -0
  225. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-DUKZONPX.mjs +1 -0
  226. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-R6RP3MQ2.mjs +1 -0
  227. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SGBABQV6.mjs +84 -0
  228. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-IM6M5BOW.mjs +40 -0
  229. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-FGJOIFJ2.mjs +169 -0
  230. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-ERV5D3W5.mjs +1 -0
  231. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-6KDGCZ5J.mjs +291 -0
  232. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-OJBUSBEF.mjs +1 -0
  233. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-LNXYTHDW.mjs +8 -0
  234. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-7G6YQRTY.mjs +120 -0
  235. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-AABPVERA.mjs +1 -0
  236. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-F2JSY3V3.mjs +1 -0
  237. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/usecaseDiagram-6Y6TMLBO.mjs +383 -0
  238. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-6RODYPCK.mjs +34 -0
  239. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-EIT5NWVN.mjs +1 -0
  240. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-RFZKLAPA.mjs +78 -0
  241. rdstudio/web/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-TWWI2QJZ.mjs +7 -0
  242. rdstudio/web/vendor/mermaid/mermaid.esm.min.mjs +14 -0
  243. rdstudio/web/vendor/texmath.js +368 -0
  244. rdstudio/web/vendor/vega/vega-embed.min.js +7 -0
  245. rdstudio/web/vendor/vega/vega-lite.min.js +2 -0
  246. rdstudio/web/vendor/vega/vega.min.js +1 -0
  247. rdstudio-0.2.0.dist-info/METADATA +201 -0
  248. rdstudio-0.2.0.dist-info/RECORD +251 -0
  249. rdstudio-0.2.0.dist-info/WHEEL +4 -0
  250. rdstudio-0.2.0.dist-info/entry_points.txt +3 -0
  251. rdstudio-0.2.0.dist-info/licenses/LICENSE +21 -0
rdstudio/learner.py ADDED
@@ -0,0 +1,187 @@
1
+ """The private learner record: what a person has done to learn a project.
2
+
3
+ Kept apart from the project (``~/.local/share/rdstudio/learners/<project>/`` by
4
+ default, or under ``[learner] path`` in the user config), one JSON event per
5
+ line in ``record.jsonl``, only ever appended. Off until ``[learner] enabled``.
6
+
7
+ Every event has an ``id``: a ULID (a millisecond timestamp, then randomness, in
8
+ 26 sortable characters), and the ``device`` that wrote it. Records from several
9
+ devices merge as a union by id, in any order and as often as needed; sorting by
10
+ id puts them in time order. Corrections are new events that name the id they
11
+ correct, so nothing is ever rewritten.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import os
19
+ import re
20
+ import secrets
21
+ import subprocess
22
+ import threading
23
+ import time
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from .config import Config, _read_toml, user_config_path
28
+ from .okf import iso, now, to_datetime
29
+
30
+ KINDS = ("autodidactic", "interactive", "ai")
31
+ MAX_EVENT_BYTES = 64 * 1024
32
+ _CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
33
+ ID_RE = re.compile(r"^[0-9A-HJKMNP-TV-Z]{26}$")
34
+ DEVICE_RE = re.compile(r"^[a-z0-9]{4,16}$")
35
+
36
+
37
+ class LearnerError(ValueError):
38
+ pass
39
+
40
+
41
+ def settings() -> dict[str, Any]:
42
+ return _read_toml(user_config_path()).get("learner", {})
43
+
44
+
45
+ def enabled(cfg: Config) -> bool:
46
+ return bool(settings().get("enabled", False)) and cfg.is_project
47
+
48
+
49
+ def project_id(root: Path) -> str:
50
+ """The repository's first root commit, the same in every clone; outside git,
51
+ a hash of the path."""
52
+ try:
53
+ out = subprocess.run(["git", "rev-list", "--max-parents=0", "HEAD"], cwd=root,
54
+ capture_output=True, text=True, timeout=10)
55
+ roots = sorted(out.stdout.split())
56
+ if out.returncode == 0 and roots:
57
+ return roots[0][:16]
58
+ except (OSError, subprocess.SubprocessError):
59
+ pass
60
+ return "path-" + hashlib.sha256(str(root.resolve()).encode()).hexdigest()[:12]
61
+
62
+
63
+ def learners_root() -> Path:
64
+ base = settings().get("path")
65
+ if base:
66
+ return Path(base).expanduser()
67
+ data = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
68
+ return Path(data) / "rdstudio" / "learners"
69
+
70
+
71
+ def record_dir(cfg: Config) -> Path:
72
+ return learners_root() / project_id(cfg.root)
73
+
74
+
75
+ def device_id() -> str:
76
+ """This machine's id in learner records, made once and kept beside them."""
77
+ path = learners_root() / "device"
78
+ try:
79
+ dev = path.read_text(encoding="utf-8").strip()
80
+ if DEVICE_RE.match(dev):
81
+ return dev
82
+ except OSError:
83
+ pass
84
+ dev = secrets.token_hex(4)
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+ path.write_text(dev + "\n", encoding="utf-8")
87
+ return dev
88
+
89
+
90
+ def _encode(n: int, length: int) -> str:
91
+ return "".join(_CROCKFORD[(n >> (5 * i)) & 31] for i in reversed(range(length)))
92
+
93
+
94
+ _last = [0, 0] # the last id's time and random part, for ids made in the same millisecond
95
+ _lock = threading.Lock()
96
+
97
+
98
+ def new_id(ms: int | None = None) -> str:
99
+ """A ULID. Within one millisecond the random part counts up, so ids from one
100
+ device sort in the order they were made."""
101
+ ms = time.time_ns() // 1_000_000 if ms is None else ms
102
+ with _lock:
103
+ rand = _last[1] + 1 if ms == _last[0] else secrets.randbits(80)
104
+ _last[:] = [ms, rand]
105
+ return _encode(ms, 10) + _encode(rand & ((1 << 80) - 1), 16)
106
+
107
+
108
+ def legacy_id(line: str, at: object) -> str:
109
+ """A fixed id for an event written before events had ids: its time, and a
110
+ hash of the line in place of randomness, so every device derives the same one."""
111
+ when = to_datetime(at)
112
+ ms = int(when.timestamp() * 1000) if when else 0
113
+ return _encode(ms, 10) + _encode(int(hashlib.sha256(line.encode()).hexdigest()[:20], 16), 16)
114
+
115
+
116
+ def content_hash(body: str) -> str:
117
+ """The version of a note an event refers to."""
118
+ return hashlib.sha256(body.strip().encode("utf-8")).hexdigest()[:12]
119
+
120
+
121
+ def _clean(event: dict[str, Any]) -> dict[str, Any]:
122
+ if not isinstance(event, dict):
123
+ raise LearnerError("an event is a JSON object")
124
+ name = event.get("event")
125
+ if not isinstance(name, str) or not name or len(name) > 40:
126
+ raise LearnerError("an event needs an 'event' name")
127
+ kind = event.get("kind")
128
+ if kind is not None and kind not in KINDS:
129
+ raise LearnerError(f"'kind' is one of {', '.join(KINDS)}")
130
+ eid = event.get("id")
131
+ if eid is not None and not (isinstance(eid, str) and ID_RE.match(eid)):
132
+ raise LearnerError("'id' is a ULID (26 characters)") # given when made offline
133
+ dev = event.get("device")
134
+ if dev is not None and not (isinstance(dev, str) and DEVICE_RE.match(dev)):
135
+ raise LearnerError("'device' is 4 to 16 lowercase letters and digits")
136
+ rest = {k: v for k, v in event.items() if k not in ("at", "id", "device")}
137
+ out = {"id": eid or new_id(), "at": iso(now()), "device": dev or device_id(), **rest}
138
+ if len(json.dumps(out, ensure_ascii=False, separators=(",", ":")).encode()) > MAX_EVENT_BYTES:
139
+ raise LearnerError("event too large")
140
+ return out
141
+
142
+
143
+ def append(cfg: Config, event: dict[str, Any]) -> dict[str, Any]:
144
+ """Add one event to the record and return it as stored."""
145
+ out = _clean(event)
146
+ path = record_dir(cfg) / "record.jsonl"
147
+ path.parent.mkdir(parents=True, exist_ok=True)
148
+ with path.open("a", encoding="utf-8") as fh:
149
+ fh.write(json.dumps(out, ensure_ascii=False, separators=(",", ":")) + "\n")
150
+ return out
151
+
152
+
153
+ def read(path: Path) -> list[dict[str, Any]]:
154
+ """The events in one record file, in the order written, each once (a retried
155
+ write can repeat one); lines that do not parse are skipped."""
156
+ if not path.is_file():
157
+ return []
158
+ out, seen = [], set()
159
+ for line in path.read_text(encoding="utf-8").splitlines():
160
+ try:
161
+ e = json.loads(line)
162
+ except ValueError:
163
+ continue
164
+ if not isinstance(e, dict):
165
+ continue
166
+ if not isinstance(e.get("id"), str):
167
+ e = {"id": legacy_id(line, e.get("at")), **e}
168
+ if e["id"] not in seen:
169
+ seen.add(e["id"])
170
+ out.append(e)
171
+ return out
172
+
173
+
174
+ def merge(*records: list[dict[str, Any]]) -> list[dict[str, Any]]:
175
+ """The union of several devices' records, by id, in time order. The same in
176
+ any order and however often it is repeated."""
177
+ by_id: dict[str, dict[str, Any]] = {}
178
+ for record in records:
179
+ for e in record:
180
+ by_id.setdefault(e["id"], e)
181
+ return [by_id[k] for k in sorted(by_id)]
182
+
183
+
184
+ def events(cfg: Config, *, concept: str | None = None) -> list[dict[str, Any]]:
185
+ """Every event, oldest first."""
186
+ out = read(record_dir(cfg) / "record.jsonl")
187
+ return [e for e in out if concept is None or e.get("concept") == concept]
rdstudio/mcp_server.py ADDED
@@ -0,0 +1,313 @@
1
+ """Local stdio MCP server over the project knowledge bundle.
2
+
3
+ Every tool returns compact text so an agent spends as little context as
4
+ possible: search returns ids and one-line snippets, outline returns headings,
5
+ and read can return a single section.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from mcp.server.mcpserver import MCPServer
14
+
15
+ from . import __version__, classify, procedures, references, scopes
16
+ from .brief import brief as project_brief
17
+ from .config import Config
18
+ from .okf import Bundle, dump_frontmatter, headings, jsonable, section
19
+ from .search import Index
20
+ from .store import StoreError, record as store_record
21
+
22
+ INSTRUCTIONS = """\
23
+ Project knowledge base (OKF markdown bundle). Retrieve progressively:
24
+ search -> outline -> read(section). Prefer reading one section over a whole
25
+ concept. Before recording, search for an existing concept to update rather
26
+ than creating a duplicate. Record decisions, answered questions, findings and
27
+ procedures as they happen. Mark an edit significant=false only for trivial or
28
+ dictated changes; significant edits flag human-reviewed concepts for re-review.
29
+ Never claim human verification; only humans verify (rdstudio verify)."""
30
+
31
+
32
+ def _fmt(obj: Any) -> str:
33
+ return json.dumps(jsonable(obj), ensure_ascii=False, indent=1)
34
+
35
+
36
+ def create_server(cfg: Config) -> MCPServer:
37
+ server = MCPServer("rdstudio", instructions=INSTRUCTIONS, version=__version__)
38
+ classifier = classify.from_config(cfg.raw)
39
+
40
+ def bundle() -> Bundle:
41
+ return Bundle.load(cfg.knowledge_dir)
42
+
43
+ def locate(ref: str) -> tuple[str, Config, Bundle, str | None]:
44
+ """Resolve "id" (project) or "global:id" (global knowledge base)."""
45
+ if ref.startswith("global:"):
46
+ g = scopes.global_config(cfg)
47
+ if g is None:
48
+ raise scopes.ScopeError("no global knowledge base")
49
+ b = Bundle.load(g.knowledge_dir)
50
+ return "global", g, b, b.resolve_id(ref[len("global:"):])
51
+ b = bundle()
52
+ return "project", cfg, b, b.resolve_id(ref)
53
+
54
+ @server.tool()
55
+ def brief() -> str:
56
+ """A short orientation to the project knowledge base: what exists, active
57
+ tasks, what awaits the developer and recent commits. Call it at the start
58
+ of a session unless one was already provided."""
59
+ return project_brief(cfg)
60
+
61
+ @server.tool()
62
+ def search(query: str, type: str | None = None, tags: list[str] | None = None,
63
+ under: str | None = None, limit: int = 8, scope: str = "project") -> str:
64
+ """Keyword (BM25) search over the knowledge base. Returns concept ids, titles,
65
+ descriptions and a one-line snippet. Filter by concept type (e.g. "Decision"),
66
+ tags (all must match) or a directory prefix (e.g. "design"). scope: "project"
67
+ (default), "global" (the developer's cross-project knowledge base) or "all";
68
+ global ids are prefixed "global:"."""
69
+ try:
70
+ targets = scopes.scoped(cfg, scope)
71
+ except scopes.ScopeError as exc:
72
+ return str(exc)
73
+ limit = max(1, min(limit, 25))
74
+ results = []
75
+ for name, c in targets:
76
+ for h in Index(Bundle.load(c.knowledge_dir)).search(query, limit=limit, type=type, tags=tags, under=under):
77
+ d = h.as_dict()
78
+ if name == "global":
79
+ d["id"] = "global:" + d["id"]
80
+ results.append(d)
81
+ results.sort(key=lambda d: -d["score"])
82
+ if not results:
83
+ return "No matches. Try other words, drop filters, or list a directory."
84
+ return _fmt(results[:limit])
85
+
86
+ @server.tool()
87
+ def outline(id: str) -> str:
88
+ """Metadata and heading outline of one concept, without its body. Use it to
89
+ decide which section to read. Prefix the id with "global:" for the global base."""
90
+ try:
91
+ _, _, b, cid = locate(id)
92
+ except scopes.ScopeError as exc:
93
+ return str(exc)
94
+ if cid is None:
95
+ return f"No concept {id!r}."
96
+ c = b.concepts[cid]
97
+ return _fmt({
98
+ "id": c.id, "title": c.title, "type": c.type, "description": c.description,
99
+ "tags": c.tags, "status": c.status, "trust": c.trust,
100
+ "verification_stale": c.verification_stale,
101
+ "generated": c.meta.get("generated"),
102
+ "chars": len(c.body),
103
+ "headings": ["#" * h.level + " " + h.text for h in headings(c.body)],
104
+ "links_to": sorted({l.target for l in c.links if not l.broken}),
105
+ "linked_from": b.backlinks(cid),
106
+ })
107
+
108
+ @server.tool()
109
+ def read(id: str, section_heading: str | None = None, frontmatter: bool = False) -> str:
110
+ """Read a concept's body, or only the section under one heading (matched by
111
+ text, case-insensitive). Set frontmatter=true to include the YAML metadata.
112
+ Prefix the id with "global:" for the global base."""
113
+ try:
114
+ _, _, b, cid = locate(id)
115
+ except scopes.ScopeError as exc:
116
+ return str(exc)
117
+ if cid is None:
118
+ return f"No concept {id!r}."
119
+ c = b.concepts[cid]
120
+ text = c.body
121
+ if section_heading:
122
+ text = section(c.body, section_heading)
123
+ if text is None:
124
+ options = ", ".join(h.text for h in headings(c.body)) or "none"
125
+ return f"No section {section_heading!r} in {cid}. Headings: {options}."
126
+ if frontmatter:
127
+ return f"---\n{dump_frontmatter(c.meta)}---\n\n{text}"
128
+ return f"# {c.title} ({cid})\n\n{text}" if not section_heading else text
129
+
130
+ @server.tool()
131
+ def list_concepts(directory: str = "", scope: str = "project") -> str:
132
+ """List a directory of the knowledge base: its concepts (id, type, title,
133
+ description) and subdirectories. Use "" for the root. scope: "project" or
134
+ "global"."""
135
+ try:
136
+ b = Bundle.load(scopes.scoped(cfg, scope)[-1][1].knowledge_dir) if scope == "global" else bundle()
137
+ except scopes.ScopeError as exc:
138
+ return str(exc)
139
+ d = b.directories.get(directory.strip("/"))
140
+ if d is None:
141
+ return f"No directory {directory!r}. Top level: {sorted(b.directories[''].children)}"
142
+ return _fmt({
143
+ "directory": d.id,
144
+ "subdirectories": sorted(d.children),
145
+ "concepts": [
146
+ {"id": cid, "type": b.concepts[cid].type, "title": b.concepts[cid].title,
147
+ "description": b.concepts[cid].description}
148
+ for cid in sorted(d.concepts)
149
+ ],
150
+ })
151
+
152
+ @server.tool()
153
+ def record(id: str, type: str | None = None, title: str | None = None,
154
+ description: str | None = None, tags: list[str] | None = None,
155
+ body: str | None = None, section_heading: str | None = None,
156
+ append: str | None = None, significant: bool | None = None,
157
+ meta: dict[str, Any] | None = None, actor: str | None = None,
158
+ scope: str = "project") -> str:
159
+ """Create or update a concept at `id` (path without .md, e.g.
160
+ "decisions/activation-function"). New concepts need `type`, and should have a
161
+ title and one-sentence description. `body` replaces the whole body, or only the
162
+ section under `section_heading`; `append` adds to the end. Existing frontmatter
163
+ is preserved; `meta` sets extra keys (null deletes). Links between concepts use
164
+ bundle-absolute paths like [text](/design/model.md). `significant`: true for a
165
+ meaningful change, false for trivial or dictated edits (keeps provenance and
166
+ review state), or omit it to let rdstudio judge from the change. scope="global"
167
+ writes to the developer's global knowledge base: only for knowledge that is
168
+ not specific to this project, and only when the developer asked for it."""
169
+ updates: dict[str, Any] = dict(meta or {})
170
+ for key, value in (("type", type), ("title", title), ("description", description), ("tags", tags)):
171
+ if value is not None:
172
+ updates[key] = value
173
+ try:
174
+ target = cfg if scope == "project" else scopes.scoped(cfg, "global")[0][1]
175
+ result = store_record(
176
+ target.knowledge_dir, id.removeprefix("global:"), actor=actor or cfg.agent, body=body,
177
+ meta=updates, section=section_heading, append=append, significant=significant,
178
+ classifier=classifier,
179
+ )
180
+ except (StoreError, scopes.ScopeError) as exc:
181
+ return f"Not recorded: {exc}"
182
+ b = Bundle.load(target.knowledge_dir)
183
+ b.write_indexes()
184
+ issues = [f"{i.level}: {i.message}" for i in b.lint() if i.path == result.path]
185
+ out = result.as_dict()
186
+ if issues:
187
+ out["issues"] = issues
188
+ return _fmt(out)
189
+
190
+ @server.tool()
191
+ def backlinks(id: str) -> str:
192
+ """Concepts that link to the given concept."""
193
+ b = bundle()
194
+ cid = b.resolve_id(id)
195
+ if cid is None:
196
+ return f"No concept {id!r}."
197
+ return _fmt([{"id": x, "title": b.concepts[x].title} for x in b.backlinks(cid)])
198
+
199
+ @server.tool()
200
+ def study_path(id: str) -> str:
201
+ """What to read before a concept: everything it requires (links titled
202
+ "requires"), directly or through a chain, in reading order. ``depth`` is the
203
+ longest chain of prerequisites below each concept."""
204
+ b = bundle()
205
+ cid = b.resolve_id(id)
206
+ if cid is None:
207
+ return f"No concept {id!r}."
208
+ order = b.prerequisite_order()
209
+ return _fmt([{"id": x, "title": b.concepts[x].title, "depth": order[x]["depth"]}
210
+ for x in [*b.prerequisites(cid), cid]])
211
+
212
+ @server.tool()
213
+ def review_queue(limit: int = 10) -> str:
214
+ """What needs human attention: concepts changed since human review, open
215
+ questions, unverified concepts, and format errors."""
216
+ b = bundle()
217
+ cs = list(b.concepts.values())
218
+ brief = lambda c: {"id": c.id, "title": c.title, "type": c.type} # noqa: E731
219
+ questions = [c for c in cs if c.type.lower() == "question" and "answered" not in c.tags]
220
+ return _fmt({
221
+ "changed_since_review": [brief(c) for c in cs if c.verification_stale][:limit],
222
+ "open_questions": [brief(c) for c in questions][:limit],
223
+ "unverified_count": sum(c.trust == "unverified" for c in cs),
224
+ "errors": [f"{i.path}: {i.message}" for i in b.lint() if i.level == "error"][:limit],
225
+ })
226
+
227
+ @server.tool()
228
+ def procedure_next(procedure: str, step: str | None = None, hops: int = 2) -> str:
229
+ """Guidance for a recorded procedure (type: Procedure). Give the step you just
230
+ completed (node id or label); returns the steps reachable within `hops`
231
+ transitions, with each transition's condition, guidance and pitfalls. With no
232
+ step, starts at the beginning. Use it to follow a procedure step by step
233
+ without loading the whole graph."""
234
+ b = bundle()
235
+ cid = b.resolve_id(procedure) or b.resolve_id("procedures/" + procedure)
236
+ if cid is None or not procedures.is_procedure(b.concepts[cid]):
237
+ names = [c.id for c in b.concepts.values() if procedures.is_procedure(c)]
238
+ return f"No procedure {procedure!r}. Procedures: {names or 'none recorded'}."
239
+ c = b.concepts[cid]
240
+ graph = procedures.graph_of(c)
241
+ node = graph.match(step)
242
+ matched_by = "exact"
243
+ if node is None and step:
244
+ steps = {nid: str(attrs.get("label", nid)) for nid, attrs in graph.nodes.items()}
245
+ decision = classifier.choose("procedure_step", list(steps), {"description": step, "steps": steps})
246
+ node, matched_by = decision.choice, f"{decision.backend} (confidence {decision.confidence})"
247
+ if node is None:
248
+ out = procedures.describe(c, graph, None)
249
+ out["note"] = f"Step {step!r} is not in this procedure; showing the whole graph."
250
+ return _fmt(out)
251
+ out = procedures.describe(c, graph.neighbourhood(node, max(1, min(hops, 4))), node)
252
+ if matched_by != "exact":
253
+ out["matched_by"] = matched_by
254
+ return _fmt(out)
255
+
256
+ @server.tool()
257
+ def procedure_propose(procedure: str, edits: list[dict[str, Any]], rationale: str,
258
+ actor: str | None = None) -> str:
259
+ """Propose changes to a procedure's graph for the developer to accept or reject.
260
+ Each edit: {"op": add_node|update_node|delete_node|add_edge|update_edge|delete_edge,
261
+ ...}. Nodes take id and label; edges take from, to, relation (LEADS_TO, TRIGGERS,
262
+ PROVIDES_INPUT_FOR, CONVERGES_TO) and condition, guidance, pitfalls. Explain
263
+ in `rationale` what went wrong or right that motivates the change. Check the
264
+ procedure's rejected proposals first (read it with frontmatter=true) and do not
265
+ repeat them."""
266
+ b = bundle()
267
+ cid = b.resolve_id(procedure) or b.resolve_id("procedures/" + procedure)
268
+ if cid is None:
269
+ return f"No procedure {procedure!r}."
270
+ try:
271
+ return _fmt(procedures.propose(cfg.knowledge_dir, cid, edits=edits, rationale=rationale,
272
+ actor=actor or cfg.agent))
273
+ except procedures.ProcedureError as exc:
274
+ return f"Not proposed: {exc}"
275
+
276
+ @server.tool()
277
+ def promote(id: str, as_id: str | None = None, keep: bool = False) -> str:
278
+ """Move a project concept into the developer's global knowledge base (for
279
+ knowledge that applies beyond this project). Only do this when the developer
280
+ has agreed. keep=true copies instead of moving. Refuses to move a concept other
281
+ project concepts link to unless keep=true."""
282
+ try:
283
+ return _fmt(scopes.promote(cfg, id, as_id=as_id, keep=keep))
284
+ except (scopes.ScopeError, StoreError) as exc:
285
+ return f"Not promoted: {exc}"
286
+
287
+ if references.enabled(cfg):
288
+ @server.tool()
289
+ def ref_search(query: str, limit: int = 8) -> str:
290
+ """Search the bibliography (papis library) by title, authors, tags and
291
+ abstract. Returns citekeys (`ref`), metadata, whether a PDF is attached, and
292
+ the id of the Reference concept for notes."""
293
+ try:
294
+ hits = references.search(cfg, query, limit=max(1, min(limit, 25)))
295
+ except references.ReferenceError as exc:
296
+ return str(exc)
297
+ return _fmt(hits) if hits else "No matching references."
298
+
299
+ @server.tool()
300
+ def ref_text(ref: str, pages: str | None = None, query: str | None = None) -> str:
301
+ """Read part of a reference's PDF as plain text: `pages` like "1" or "3-4,7",
302
+ or `query` to get the two pages that best match. Read the pages you need
303
+ rather than the whole paper."""
304
+ try:
305
+ return references.text(cfg, ref, pages=pages, query=query)
306
+ except (references.ReferenceError, ValueError) as exc:
307
+ return str(exc)
308
+
309
+ return server
310
+
311
+
312
+ def run(cfg: Config) -> None:
313
+ create_server(cfg).run()