dr-claw 1.1.2

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 (287) hide show
  1. package/LICENSE +1380 -0
  2. package/NOTICE +17 -0
  3. package/README.md +875 -0
  4. package/README.zh-CN.md +857 -0
  5. package/dist/api-docs.html +878 -0
  6. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  7. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  8. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  22. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  31. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  35. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  37. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  49. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  50. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  52. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  53. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  55. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  56. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  57. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  58. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  60. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  61. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  63. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  64. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  65. package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
  66. package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
  67. package/dist/assets/arc-DOMBmC_A.js +1 -0
  68. package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
  69. package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
  70. package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
  71. package/dist/assets/channel-BFOE7OyC.js +1 -0
  72. package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
  73. package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
  74. package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
  75. package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
  76. package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
  77. package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
  78. package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
  79. package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
  80. package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
  81. package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
  82. package/dist/assets/clone-BulQSblH.js +1 -0
  83. package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
  84. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  85. package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
  86. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  87. package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
  88. package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
  89. package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
  90. package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
  91. package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
  92. package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
  93. package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
  94. package/dist/assets/graph-Blm-emkR.js +1 -0
  95. package/dist/assets/index-B8Yutsnm.css +1 -0
  96. package/dist/assets/index-Cf6gyhc3.js +654 -0
  97. package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
  98. package/dist/assets/init-Gi6I4Gst.js +1 -0
  99. package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
  100. package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
  101. package/dist/assets/layout-Ct2kFIId.js +1 -0
  102. package/dist/assets/linear-CKG9EwIJ.js +1 -0
  103. package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
  104. package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
  105. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  106. package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
  107. package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
  108. package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
  109. package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
  110. package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
  111. package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
  112. package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
  113. package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
  114. package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
  115. package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
  116. package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
  117. package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
  118. package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
  119. package/dist/clear-cache.html +85 -0
  120. package/dist/convert-icons.md +53 -0
  121. package/dist/dr-claw.png +0 -0
  122. package/dist/favicon.png +0 -0
  123. package/dist/generate-icons.js +49 -0
  124. package/dist/icons/claude-ai-icon.svg +1 -0
  125. package/dist/icons/codex-white.svg +3 -0
  126. package/dist/icons/codex.svg +3 -0
  127. package/dist/icons/cursor-white.svg +12 -0
  128. package/dist/icons/cursor.svg +1 -0
  129. package/dist/icons/file.svg +16997 -0
  130. package/dist/icons/generate-icons.md +19 -0
  131. package/dist/icons/icon-128x128.png +0 -0
  132. package/dist/icons/icon-144x144.png +0 -0
  133. package/dist/icons/icon-152x152.png +0 -0
  134. package/dist/icons/icon-192x192.png +0 -0
  135. package/dist/icons/icon-384x384.png +0 -0
  136. package/dist/icons/icon-512x512.png +0 -0
  137. package/dist/icons/icon-72x72.png +0 -0
  138. package/dist/icons/icon-96x96.png +0 -0
  139. package/dist/icons/news/arxiv.svg +1 -0
  140. package/dist/icons/news/huggingface.svg +8 -0
  141. package/dist/icons/news/x-black.png +0 -0
  142. package/dist/icons/news/x-white.png +0 -0
  143. package/dist/icons/news/xiaohongshu.png +0 -0
  144. package/dist/index.html +51 -0
  145. package/dist/leveraged-cognition.png +0 -0
  146. package/dist/manifest.json +61 -0
  147. package/dist/screenshots/chat.png +0 -0
  148. package/dist/screenshots/new_claw.png +0 -0
  149. package/dist/screenshots/news_dashboard.png +0 -0
  150. package/dist/screenshots/project_dashboard.png +0 -0
  151. package/dist/screenshots/research_lab.png +0 -0
  152. package/dist/screenshots/skill_library.png +0 -0
  153. package/dist/screenshots/task_list.png +0 -0
  154. package/dist/screenshots/terminal_example.png +0 -0
  155. package/dist/screenshots/terminal_example1.png +0 -0
  156. package/dist/sw.js +58 -0
  157. package/dist/wechat-group-qr.jpg +0 -0
  158. package/package.json +133 -0
  159. package/public/api-docs.html +878 -0
  160. package/public/clear-cache.html +85 -0
  161. package/public/convert-icons.md +53 -0
  162. package/public/dr-claw.png +0 -0
  163. package/public/favicon.png +0 -0
  164. package/public/generate-icons.js +49 -0
  165. package/public/icons/claude-ai-icon.svg +1 -0
  166. package/public/icons/codex-white.svg +3 -0
  167. package/public/icons/codex.svg +3 -0
  168. package/public/icons/cursor-white.svg +12 -0
  169. package/public/icons/cursor.svg +1 -0
  170. package/public/icons/file.svg +16997 -0
  171. package/public/icons/generate-icons.md +19 -0
  172. package/public/icons/icon-128x128.png +0 -0
  173. package/public/icons/icon-144x144.png +0 -0
  174. package/public/icons/icon-152x152.png +0 -0
  175. package/public/icons/icon-192x192.png +0 -0
  176. package/public/icons/icon-384x384.png +0 -0
  177. package/public/icons/icon-512x512.png +0 -0
  178. package/public/icons/icon-72x72.png +0 -0
  179. package/public/icons/icon-96x96.png +0 -0
  180. package/public/icons/news/arxiv.svg +1 -0
  181. package/public/icons/news/huggingface.svg +8 -0
  182. package/public/icons/news/x-black.png +0 -0
  183. package/public/icons/news/x-white.png +0 -0
  184. package/public/icons/news/xiaohongshu.png +0 -0
  185. package/public/leveraged-cognition.png +0 -0
  186. package/public/manifest.json +61 -0
  187. package/public/screenshots/chat.png +0 -0
  188. package/public/screenshots/new_claw.png +0 -0
  189. package/public/screenshots/news_dashboard.png +0 -0
  190. package/public/screenshots/project_dashboard.png +0 -0
  191. package/public/screenshots/research_lab.png +0 -0
  192. package/public/screenshots/skill_library.png +0 -0
  193. package/public/screenshots/task_list.png +0 -0
  194. package/public/screenshots/terminal_example.png +0 -0
  195. package/public/screenshots/terminal_example1.png +0 -0
  196. package/public/sw.js +58 -0
  197. package/public/wechat-group-qr.jpg +0 -0
  198. package/scripts/export-skills-catalog-v2.mjs +746 -0
  199. package/scripts/fix-node-pty.js +45 -0
  200. package/scripts/gemini_image_core.py +470 -0
  201. package/scripts/install-python-tools.js +64 -0
  202. package/server/AGENTS.md +73 -0
  203. package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
  204. package/server/__tests__/runtimePorts.test.mjs +115 -0
  205. package/server/__tests__/session-delete.test.mjs +166 -0
  206. package/server/bin/compute-node +70 -0
  207. package/server/claude-sdk.js +799 -0
  208. package/server/cli-chat.js +488 -0
  209. package/server/cli.js +354 -0
  210. package/server/compute-node.js +439 -0
  211. package/server/constants/config.js +5 -0
  212. package/server/cursor-cli.js +340 -0
  213. package/server/database/auth.db +0 -0
  214. package/server/database/db.js +1852 -0
  215. package/server/database/init.sql +194 -0
  216. package/server/gemini-cli.js +1352 -0
  217. package/server/index.js +3154 -0
  218. package/server/load-env.js +71 -0
  219. package/server/local-gpu.js +860 -0
  220. package/server/middleware/auth.js +100 -0
  221. package/server/openai-codex.js +724 -0
  222. package/server/openrouter.js +890 -0
  223. package/server/project-token-usage.js +407 -0
  224. package/server/projects.js +3973 -0
  225. package/server/routes/agent.js +1272 -0
  226. package/server/routes/auth.js +128 -0
  227. package/server/routes/auto-research.js +558 -0
  228. package/server/routes/cli-auth.js +958 -0
  229. package/server/routes/codex.js +344 -0
  230. package/server/routes/commands.js +601 -0
  231. package/server/routes/compute.js +358 -0
  232. package/server/routes/cursor.js +795 -0
  233. package/server/routes/git.js +1348 -0
  234. package/server/routes/mcp-utils.js +48 -0
  235. package/server/routes/mcp.js +618 -0
  236. package/server/routes/news.js +711 -0
  237. package/server/routes/projects.js +633 -0
  238. package/server/routes/references.js +338 -0
  239. package/server/routes/settings.js +285 -0
  240. package/server/routes/skills.js +914 -0
  241. package/server/routes/taskmaster.js +2566 -0
  242. package/server/routes/telemetry.js +89 -0
  243. package/server/routes/user.js +155 -0
  244. package/server/scripts/fix_session_names.js +57 -0
  245. package/server/scripts/research-news/common_words.py +19 -0
  246. package/server/scripts/research-news/link_keywords.py +298 -0
  247. package/server/scripts/research-news/scan_existing_notes.py +266 -0
  248. package/server/scripts/research-news/scoring_utils.py +227 -0
  249. package/server/scripts/research-news/search_arxiv.py +842 -0
  250. package/server/scripts/research-news/search_huggingface.py +438 -0
  251. package/server/scripts/research-news/search_x.py +490 -0
  252. package/server/scripts/research-news/search_xiaohongshu.py +610 -0
  253. package/server/taskmaster-templates/README.md +150 -0
  254. package/server/taskmaster-templates/README.zh-CN.md +150 -0
  255. package/server/taskmaster-templates/ai-research-dataset.json +230 -0
  256. package/server/taskmaster-templates/ai-research-method-model.json +230 -0
  257. package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
  258. package/server/telemetry.js +121 -0
  259. package/server/templates/AGENTS.md +103 -0
  260. package/server/templates/CLAUDE.md +104 -0
  261. package/server/templates/GEMINI.md +103 -0
  262. package/server/templates/cursor-project.md +108 -0
  263. package/server/templates/index.js +37 -0
  264. package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
  265. package/server/utils/auto-research-mailer.js +57 -0
  266. package/server/utils/cliResolution.js +170 -0
  267. package/server/utils/commandParser.js +303 -0
  268. package/server/utils/cursorCommand.js +61 -0
  269. package/server/utils/geminiApiKey.js +28 -0
  270. package/server/utils/gitConfig.js +24 -0
  271. package/server/utils/imageAttachmentFiles.js +35 -0
  272. package/server/utils/mcp-detector.js +198 -0
  273. package/server/utils/parsers/bibtex-parser.js +207 -0
  274. package/server/utils/permissions.js +100 -0
  275. package/server/utils/runtimePorts.js +212 -0
  276. package/server/utils/sessionFormatting.js +68 -0
  277. package/server/utils/sessionIndex.js +100 -0
  278. package/server/utils/sessionMode.js +95 -0
  279. package/server/utils/sessionTokenUsage.js +105 -0
  280. package/server/utils/taskmaster-websocket.js +129 -0
  281. package/server/utils/zotero-client.js +136 -0
  282. package/shared/errorClassifier.js +102 -0
  283. package/shared/geminiThinkingSupport.d.ts +22 -0
  284. package/shared/geminiThinkingSupport.js +96 -0
  285. package/shared/geminiThoughtParser.d.ts +8 -0
  286. package/shared/geminiThoughtParser.js +30 -0
  287. package/shared/modelConstants.js +139 -0
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ HuggingFace Daily Papers search script.
4
+
5
+ Fetches papers from the HuggingFace Daily Papers API, scores them against
6
+ research interest configuration, and outputs filtered/ranked results in the
7
+ same JSON format used by search_arxiv.py.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import sys
13
+ import logging
14
+ import ssl
15
+ from datetime import datetime
16
+ from typing import Dict, List, Optional, Tuple
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ try:
21
+ import requests
22
+ HAS_REQUESTS = True
23
+ except ImportError:
24
+ HAS_REQUESTS = False
25
+
26
+ try:
27
+ import certifi
28
+ CERTIFI_CA_BUNDLE = certifi.where()
29
+ except ImportError:
30
+ CERTIFI_CA_BUNDLE = None
31
+
32
+ import urllib.request
33
+ import urllib.parse
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Import shared scoring utilities
37
+ # ---------------------------------------------------------------------------
38
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
39
+ from scoring_utils import (
40
+ SCORE_MAX,
41
+ calculate_relevance_score,
42
+ calculate_recency_score,
43
+ calculate_quality_score,
44
+ calculate_recommendation_score,
45
+ )
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # HuggingFace API configuration
49
+ # ---------------------------------------------------------------------------
50
+ HF_DAILY_PAPERS_URL = "https://huggingface.co/api/daily_papers"
51
+
52
+ # Popularity: 50+ upvotes = max score (SCORE_MAX)
53
+ HF_UPVOTES_FULL_SCORE = 50
54
+
55
+
56
+ def build_ssl_context() -> ssl.SSLContext:
57
+ if CERTIFI_CA_BUNDLE and os.path.exists(CERTIFI_CA_BUNDLE):
58
+ return ssl.create_default_context(cafile=CERTIFI_CA_BUNDLE)
59
+ return ssl.create_default_context()
60
+
61
+
62
+ def http_get_json(url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 30) -> List[Dict]:
63
+ headers = headers or {}
64
+
65
+ if HAS_REQUESTS:
66
+ request_kwargs = {
67
+ "headers": headers,
68
+ "timeout": timeout,
69
+ }
70
+ if CERTIFI_CA_BUNDLE and os.path.exists(CERTIFI_CA_BUNDLE):
71
+ request_kwargs["verify"] = CERTIFI_CA_BUNDLE
72
+ response = requests.get(url, **request_kwargs)
73
+ response.raise_for_status()
74
+ return response.json()
75
+
76
+ req = urllib.request.Request(url, headers=headers)
77
+ with urllib.request.urlopen(req, timeout=timeout, context=build_ssl_context()) as resp:
78
+ return json.loads(resp.read().decode("utf-8"))
79
+
80
+
81
+ def load_research_config(config_path: str) -> Dict:
82
+ """
83
+ Load research interest configuration from a YAML file.
84
+
85
+ Args:
86
+ config_path: Path to the YAML config file.
87
+
88
+ Returns:
89
+ Research configuration dictionary.
90
+ """
91
+ import json
92
+
93
+ try:
94
+ with open(config_path, "r", encoding="utf-8-sig") as f:
95
+ if config_path.endswith(".json"):
96
+ config = json.load(f)
97
+ else:
98
+ try:
99
+ import yaml
100
+ config = yaml.safe_load(f)
101
+ except ImportError:
102
+ config = json.load(f)
103
+ return config
104
+ except Exception as e:
105
+ logger.error("Error loading config: %s", e)
106
+ return {
107
+ "research_domains": {
108
+ "LLM": {
109
+ "keywords": [
110
+ "pre-training", "foundation model", "model architecture",
111
+ "large language model", "LLM", "transformer",
112
+ ],
113
+ "arxiv_categories": ["cs.AI", "cs.LG", "cs.CL"],
114
+ "priority": 5,
115
+ }
116
+ },
117
+ "excluded_keywords": ["3D", "review", "workshop", "survey"],
118
+ }
119
+
120
+
121
+ def fetch_daily_papers(max_retries: int = 3) -> List[Dict]:
122
+ """
123
+ Fetch papers from the HuggingFace Daily Papers API.
124
+
125
+ Args:
126
+ max_retries: Maximum number of retry attempts.
127
+
128
+ Returns:
129
+ Raw list of paper entries from the API.
130
+ """
131
+ headers = {"User-Agent": "ResearchNews-HFPaperFetcher/1.0"}
132
+
133
+ for attempt in range(max_retries):
134
+ try:
135
+ data = http_get_json(HF_DAILY_PAPERS_URL, headers=headers, timeout=30)
136
+
137
+ logger.info("[HF] Fetched %d daily paper entries", len(data))
138
+ return data
139
+
140
+ except Exception as e:
141
+ logger.warning("[HF] Error (attempt %d/%d): %s", attempt + 1, max_retries, e)
142
+ if attempt < max_retries - 1:
143
+ import time
144
+ wait_time = (2 ** attempt) * 2
145
+ logger.info("[HF] Retrying in %d seconds...", wait_time)
146
+ time.sleep(wait_time)
147
+ else:
148
+ logger.error("[HF] Failed after %d attempts", max_retries)
149
+ return []
150
+
151
+ return []
152
+
153
+
154
+ def normalize_paper(entry: Dict) -> Optional[Dict]:
155
+ """
156
+ Normalize a single HuggingFace daily-papers API entry into the internal
157
+ paper dict format used by scoring functions.
158
+
159
+ Args:
160
+ entry: A single entry from the HF daily papers response.
161
+
162
+ Returns:
163
+ Normalized paper dict, or None if essential fields are missing.
164
+ """
165
+ paper_data = entry.get("paper", {})
166
+
167
+ arxiv_id = paper_data.get("id")
168
+ title = paper_data.get("title")
169
+ summary = paper_data.get("summary")
170
+
171
+ if not title or not arxiv_id:
172
+ return None
173
+
174
+ # Authors: list of dicts with "name" key -> comma-separated string
175
+ raw_authors = paper_data.get("authors") or []
176
+ if isinstance(raw_authors, list):
177
+ author_names = [a.get("name", "") if isinstance(a, dict) else str(a) for a in raw_authors]
178
+ authors_str = ", ".join(n for n in author_names if n)
179
+ else:
180
+ authors_str = str(raw_authors)
181
+
182
+ # Published date
183
+ published_at = paper_data.get("publishedAt", "")
184
+ published_date = None
185
+ if published_at:
186
+ try:
187
+ published_date = datetime.fromisoformat(published_at.replace("Z", "+00:00"))
188
+ except (ValueError, TypeError):
189
+ pass
190
+
191
+ upvotes = paper_data.get("upvotes", 0) or 0
192
+
193
+ # Extra metadata from the envelope (not inside paper_data)
194
+ thumbnail = entry.get("thumbnail", "")
195
+ num_comments = entry.get("numComments", 0) or 0
196
+ submitted_by = entry.get("submittedBy", {}) or {}
197
+ organization = entry.get("organization") or paper_data.get("organization")
198
+
199
+ return {
200
+ "id": arxiv_id,
201
+ "title": title,
202
+ "summary": summary or "",
203
+ "authors_str": authors_str,
204
+ "published": published_at,
205
+ "published_date": published_date,
206
+ "upvotes": upvotes,
207
+ "thumbnail": thumbnail,
208
+ "num_comments": num_comments,
209
+ "submitted_by_name": submitted_by.get("fullname") or submitted_by.get("name", ""),
210
+ "submitted_by_avatar": submitted_by.get("avatarUrl", ""),
211
+ "organization": organization.get("name", "") if isinstance(organization, dict) else (organization or ""),
212
+ "categories": [],
213
+ "source": "huggingface",
214
+ }
215
+
216
+
217
+ def calculate_popularity_score(upvotes: int) -> float:
218
+ """
219
+ Calculate popularity score based on HuggingFace upvotes.
220
+
221
+ 50+ upvotes maps to the maximum score (SCORE_MAX).
222
+
223
+ Args:
224
+ upvotes: Number of upvotes on HuggingFace.
225
+
226
+ Returns:
227
+ Popularity score in [0, SCORE_MAX].
228
+ """
229
+ if upvotes <= 0:
230
+ return 0.0
231
+ return min(upvotes / HF_UPVOTES_FULL_SCORE * SCORE_MAX, SCORE_MAX)
232
+
233
+
234
+ def score_papers(
235
+ papers: List[Dict],
236
+ config: Optional[Dict] = None,
237
+ ) -> Tuple[List[Dict], int]:
238
+ """
239
+ Score papers, optionally filtering by research configuration.
240
+
241
+ If config has research_domains, papers are filtered by relevance (unmatched
242
+ papers are excluded). If config is None or has no domains, all papers are
243
+ kept and scored by recency, popularity, and quality only.
244
+
245
+ Args:
246
+ papers: Normalized paper dicts.
247
+ config: Research interest configuration (optional).
248
+
249
+ Returns:
250
+ (scored_papers sorted by final_score descending, total_filtered count)
251
+ """
252
+ domains = (config or {}).get("research_domains", {})
253
+ excluded_keywords = (config or {}).get("excluded_keywords", [])
254
+ has_domains = bool(domains)
255
+
256
+ scored: List[Dict] = []
257
+ total_filtered = 0
258
+
259
+ for paper in papers:
260
+ # Relevance
261
+ if has_domains:
262
+ relevance, matched_domain, matched_keywords = calculate_relevance_score(
263
+ paper, domains, excluded_keywords
264
+ )
265
+ if relevance == 0:
266
+ total_filtered += 1
267
+ continue
268
+ else:
269
+ # No filtering — give all papers a baseline relevance
270
+ relevance = 1.0
271
+ matched_domain = "daily_papers"
272
+ matched_keywords = []
273
+
274
+ # Recency
275
+ recency = calculate_recency_score(paper.get("published_date"))
276
+
277
+ # Popularity (HF upvotes)
278
+ popularity = calculate_popularity_score(paper.get("upvotes", 0))
279
+
280
+ # Quality (abstract-based heuristics)
281
+ summary = paper.get("summary", "")
282
+ quality = calculate_quality_score(summary)
283
+
284
+ # Final composite score
285
+ final_score = calculate_recommendation_score(
286
+ relevance, recency, popularity, quality
287
+ )
288
+
289
+ arxiv_id = paper["id"]
290
+
291
+ scored.append({
292
+ "id": arxiv_id,
293
+ "title": paper["title"],
294
+ "authors": paper.get("authors_str", ""),
295
+ "abstract": paper.get("summary", ""),
296
+ "published": paper.get("published", ""),
297
+ "categories": paper.get("categories", []),
298
+ "relevance_score": round(relevance, 2),
299
+ "recency_score": round(recency, 2),
300
+ "popularity_score": round(popularity, 2),
301
+ "quality_score": round(quality, 2),
302
+ "final_score": final_score,
303
+ "matched_domain": matched_domain,
304
+ "matched_keywords": matched_keywords,
305
+ "link": f"https://huggingface.co/papers/{arxiv_id}",
306
+ "pdf_link": f"https://arxiv.org/pdf/{arxiv_id}.pdf",
307
+ "source": "huggingface",
308
+ "media_urls": [paper["thumbnail"]] if paper.get("thumbnail") else [],
309
+ "engagement": {
310
+ "likes": paper.get("upvotes", 0),
311
+ "comments": paper.get("num_comments", 0),
312
+ },
313
+ "submitted_by": paper.get("submitted_by_name", ""),
314
+ "organization": paper.get("organization", ""),
315
+ })
316
+
317
+ scored.sort(key=lambda x: x["final_score"], reverse=True)
318
+ return scored, total_filtered
319
+
320
+
321
+ def main():
322
+ """Main entry point."""
323
+ import argparse
324
+
325
+ default_config = os.environ.get("OBSIDIAN_VAULT_PATH", "")
326
+ if default_config:
327
+ default_config = os.path.join(
328
+ default_config, "99_System", "Config", "research_interests.yaml"
329
+ )
330
+
331
+ parser = argparse.ArgumentParser(
332
+ description="Fetch and score HuggingFace Daily Papers"
333
+ )
334
+ parser.add_argument(
335
+ "--config",
336
+ type=str,
337
+ default=default_config or None,
338
+ help="Path to research interests YAML config file",
339
+ )
340
+ parser.add_argument(
341
+ "--output",
342
+ type=str,
343
+ default="hf_daily_papers.json",
344
+ help="Output JSON file path",
345
+ )
346
+ parser.add_argument(
347
+ "--top-n",
348
+ type=int,
349
+ default=10,
350
+ help="Number of top papers to return",
351
+ )
352
+
353
+ args = parser.parse_args()
354
+
355
+ logging.basicConfig(
356
+ level=logging.INFO,
357
+ format="%(asctime)s [%(levelname)s] %(message)s",
358
+ datefmt="%H:%M:%S",
359
+ stream=sys.stderr,
360
+ )
361
+
362
+ # Config is optional — without it we show all daily papers
363
+ config = None
364
+ if args.config:
365
+ logger.info("Loading config from: %s", args.config)
366
+ config = load_research_config(args.config)
367
+ else:
368
+ logger.info("No config provided — showing all HuggingFace Daily Papers")
369
+
370
+ # Fetch daily papers from HuggingFace
371
+ logger.info("Fetching HuggingFace Daily Papers...")
372
+ raw_entries = fetch_daily_papers()
373
+
374
+ if not raw_entries:
375
+ logger.warning("No papers returned from HuggingFace API")
376
+ output = {
377
+ "top_papers": [],
378
+ "total_found": 0,
379
+ "total_filtered": 0,
380
+ "search_date": datetime.now().strftime("%Y-%m-%d"),
381
+ }
382
+ with open(args.output, "w", encoding="utf-8") as f:
383
+ json.dump(output, f, ensure_ascii=False, indent=2)
384
+ print(json.dumps(output, ensure_ascii=True, indent=2))
385
+ return 0
386
+
387
+ # Normalize entries
388
+ papers = []
389
+ for entry in raw_entries:
390
+ normalized = normalize_paper(entry)
391
+ if normalized:
392
+ papers.append(normalized)
393
+
394
+ logger.info("Normalized %d papers from %d raw entries", len(papers), len(raw_entries))
395
+
396
+ # Score (and optionally filter if config has domains)
397
+ scored_papers, total_filtered = score_papers(papers, config)
398
+
399
+ logger.info(
400
+ "Scored %d papers (%d filtered out by relevance/exclusion)",
401
+ len(scored_papers),
402
+ total_filtered,
403
+ )
404
+
405
+ # Take top N
406
+ top_papers = scored_papers[: args.top_n]
407
+
408
+ # Build output
409
+ output = {
410
+ "top_papers": top_papers,
411
+ "total_found": len(papers),
412
+ "total_filtered": total_filtered,
413
+ "search_date": datetime.now().strftime("%Y-%m-%d"),
414
+ }
415
+
416
+ # Save to file
417
+ with open(args.output, "w", encoding="utf-8") as f:
418
+ json.dump(output, f, ensure_ascii=False, indent=2, default=str)
419
+
420
+ logger.info("Results saved to: %s", args.output)
421
+ logger.info("Top %d papers:", len(top_papers))
422
+ for i, p in enumerate(top_papers, 1):
423
+ logger.info(
424
+ " %d. %s... (Score: %s, Upvotes-based popularity: %s)",
425
+ i,
426
+ p["title"][:60],
427
+ p["final_score"],
428
+ p["popularity_score"],
429
+ )
430
+
431
+ # Also output to stdout
432
+ print(json.dumps(output, ensure_ascii=True, indent=2, default=str))
433
+
434
+ return 0
435
+
436
+
437
+ if __name__ == "__main__":
438
+ sys.exit(main())