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,89 @@
1
+ import express from 'express';
2
+ import { enqueueTelemetryEvents, isTelemetryEnabled } from '../telemetry.js';
3
+
4
+ const router = express.Router();
5
+
6
+ const MAX_EVENT_TEXT_LENGTH = 10000;
7
+ const BLOCKED_EVENT_KEYS = new Set([
8
+ 'content',
9
+ 'contentLength',
10
+ 'prompt',
11
+ 'output',
12
+ 'message',
13
+ 'input_context',
14
+ 'output_context',
15
+ 'context',
16
+ ]);
17
+
18
+ const truncateText = (value, max = MAX_EVENT_TEXT_LENGTH) => {
19
+ if (typeof value !== 'string') {
20
+ return value;
21
+ }
22
+ if (value.length <= max) {
23
+ return value;
24
+ }
25
+ return value.slice(0, max);
26
+ };
27
+
28
+ const stripBlockedKeys = (value) => {
29
+ if (Array.isArray(value)) {
30
+ return value.map(stripBlockedKeys);
31
+ }
32
+
33
+ if (!value || typeof value !== 'object') {
34
+ return value;
35
+ }
36
+
37
+ return Object.fromEntries(
38
+ Object.entries(value)
39
+ .filter(([key]) => !BLOCKED_EVENT_KEYS.has(key))
40
+ .map(([key, nestedValue]) => [key, stripBlockedKeys(nestedValue)]),
41
+ );
42
+ };
43
+
44
+ const sanitizeEvent = (event, req) => {
45
+ if (!event || typeof event !== 'object') {
46
+ return null;
47
+ }
48
+
49
+ const ip =
50
+ req.headers['x-forwarded-for']?.toString().split(',')[0]?.trim() ||
51
+ req.socket?.remoteAddress ||
52
+ null;
53
+
54
+ return {
55
+ ...stripBlockedKeys(event),
56
+ name: truncateText(String(event.name || 'unknown_event'), 128),
57
+ source: truncateText(String(event.source || 'frontend'), 64),
58
+ data: event.data && typeof event.data === 'object' ? stripBlockedKeys(event.data) : null,
59
+ userId: req.user?.id ?? null,
60
+ username: req.user?.username ?? null,
61
+ ip,
62
+ userAgent: truncateText(req.headers['user-agent'] || '', 1024),
63
+ serverReceivedAt: new Date().toISOString(),
64
+ };
65
+ };
66
+
67
+ router.post('/events', (req, res) => {
68
+ const rawEvents = Array.isArray(req.body?.events)
69
+ ? req.body.events
70
+ : req.body && typeof req.body === 'object'
71
+ ? [req.body]
72
+ : [];
73
+
74
+ const events = rawEvents
75
+ .map((event) => sanitizeEvent(event, req))
76
+ .filter(Boolean);
77
+
78
+ if (events.length === 0) {
79
+ return res.status(400).json({ error: 'No valid telemetry events provided' });
80
+ }
81
+
82
+ const accepted = enqueueTelemetryEvents(events);
83
+ return res.status(202).json({
84
+ accepted,
85
+ enabled: isTelemetryEnabled(),
86
+ });
87
+ });
88
+
89
+ export default router;
@@ -0,0 +1,155 @@
1
+ import express from 'express';
2
+ import { userDb } from '../database/db.js';
3
+ import { authenticateToken } from '../middleware/auth.js';
4
+ import { getSystemGitConfig } from '../utils/gitConfig.js';
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+
8
+ const execAsync = promisify(exec);
9
+ const router = express.Router();
10
+
11
+ router.get('/git-config', authenticateToken, async (req, res) => {
12
+ try {
13
+ const userId = req.user.id;
14
+ let gitConfig = userDb.getGitConfig(userId);
15
+
16
+ // If database is empty, try to get from system git config
17
+ if (!gitConfig || (!gitConfig.git_name && !gitConfig.git_email)) {
18
+ const systemConfig = await getSystemGitConfig();
19
+
20
+ // If system has values, save them to database for this user
21
+ if (systemConfig.git_name || systemConfig.git_email) {
22
+ userDb.updateGitConfig(userId, systemConfig.git_name, systemConfig.git_email);
23
+ gitConfig = systemConfig;
24
+ console.log(`Auto-populated git config from system for user ${userId}: ${systemConfig.git_name} <${systemConfig.git_email}>`);
25
+ }
26
+ }
27
+
28
+ res.json({
29
+ success: true,
30
+ gitName: gitConfig?.git_name || null,
31
+ gitEmail: gitConfig?.git_email || null
32
+ });
33
+ } catch (error) {
34
+ console.error('Error getting git config:', error);
35
+ res.status(500).json({ error: 'Failed to get git configuration' });
36
+ }
37
+ });
38
+
39
+ // Apply git config globally via git config --global
40
+ router.post('/git-config', authenticateToken, async (req, res) => {
41
+ try {
42
+ const userId = req.user.id;
43
+ const { gitName, gitEmail } = req.body;
44
+
45
+ if (!gitName || !gitEmail) {
46
+ return res.status(400).json({ error: 'Git name and email are required' });
47
+ }
48
+
49
+ // Validate email format
50
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
51
+ if (!emailRegex.test(gitEmail)) {
52
+ return res.status(400).json({ error: 'Invalid email format' });
53
+ }
54
+
55
+ userDb.updateGitConfig(userId, gitName, gitEmail);
56
+
57
+ try {
58
+ await execAsync(`git config --global user.name "${gitName.replace(/"/g, '\\"')}"`);
59
+ await execAsync(`git config --global user.email "${gitEmail.replace(/"/g, '\\"')}"`);
60
+ console.log(`Applied git config globally: ${gitName} <${gitEmail}>`);
61
+ } catch (gitError) {
62
+ console.error('Error applying git config:', gitError);
63
+ }
64
+
65
+ res.json({
66
+ success: true,
67
+ gitName,
68
+ gitEmail
69
+ });
70
+ } catch (error) {
71
+ console.error('Error updating git config:', error);
72
+ res.status(500).json({ error: 'Failed to update git configuration' });
73
+ }
74
+ });
75
+
76
+ router.post('/complete-onboarding', authenticateToken, async (req, res) => {
77
+ try {
78
+ const userId = req.user.id;
79
+ userDb.completeOnboarding(userId);
80
+
81
+ res.json({
82
+ success: true,
83
+ message: 'Onboarding completed successfully'
84
+ });
85
+ } catch (error) {
86
+ console.error('Error completing onboarding:', error);
87
+ res.status(500).json({ error: 'Failed to complete onboarding' });
88
+ }
89
+ });
90
+
91
+ router.get('/profile', authenticateToken, async (req, res) => {
92
+ try {
93
+ const userId = req.user.id;
94
+ const profile = userDb.getProfile(userId);
95
+
96
+ res.json({
97
+ success: true,
98
+ profile: {
99
+ id: profile?.id,
100
+ username: profile?.username,
101
+ notificationEmail: profile?.notification_email || null,
102
+ }
103
+ });
104
+ } catch (error) {
105
+ console.error('Error getting user profile:', error);
106
+ res.status(500).json({ error: 'Failed to get user profile' });
107
+ }
108
+ });
109
+
110
+ router.put('/profile', authenticateToken, async (req, res) => {
111
+ try {
112
+ const userId = req.user.id;
113
+ const rawEmail = typeof req.body?.notificationEmail === 'string' ? req.body.notificationEmail.trim().toLowerCase() : '';
114
+
115
+ if (!rawEmail) {
116
+ return res.status(400).json({ error: 'Notification email is required' });
117
+ }
118
+
119
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
120
+ if (!emailRegex.test(rawEmail)) {
121
+ return res.status(400).json({ error: 'Invalid email format' });
122
+ }
123
+
124
+ const profile = userDb.updateProfile(userId, rawEmail);
125
+
126
+ res.json({
127
+ success: true,
128
+ profile: {
129
+ id: profile?.id,
130
+ username: profile?.username,
131
+ notificationEmail: profile?.notification_email || null,
132
+ }
133
+ });
134
+ } catch (error) {
135
+ console.error('Error updating user profile:', error);
136
+ res.status(500).json({ error: 'Failed to update user profile' });
137
+ }
138
+ });
139
+
140
+ router.get('/onboarding-status', authenticateToken, async (req, res) => {
141
+ try {
142
+ const userId = req.user.id;
143
+ const hasCompleted = userDb.hasCompletedOnboarding(userId);
144
+
145
+ res.json({
146
+ success: true,
147
+ hasCompletedOnboarding: hasCompleted
148
+ });
149
+ } catch (error) {
150
+ console.error('Error checking onboarding status:', error);
151
+ res.status(500).json({ error: 'Failed to check onboarding status' });
152
+ }
153
+ });
154
+
155
+ export default router;
@@ -0,0 +1,57 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname } from 'path';
5
+
6
+ // Local copy of prefix stripping since this is a standalone script
7
+ function stripInternalContextPrefix(text) {
8
+ if (typeof text !== 'string') return '';
9
+ let cleaned = text;
10
+
11
+ // 1. Match full [Context: ...] prefixes at the start of the string, including multiple ones
12
+ const fullPrefixPattern = /^\s*\[Context:[^\]]*\]\s*/i;
13
+ while (fullPrefixPattern.test(cleaned)) {
14
+ cleaned = cleaned.replace(fullPrefixPattern, '');
15
+ }
16
+
17
+ // 2. Match common truncated prefixes like "[Context: session-mode=..." or "[Context: Tre..."
18
+ const truncatedPrefixPattern = /^\s*\[Context:[^\]]*$/i;
19
+ if (truncatedPrefixPattern.test(cleaned)) {
20
+ return 'New Session';
21
+ }
22
+
23
+ return cleaned.trim() || 'New Session';
24
+ }
25
+
26
+ const __filename = fileURLToPath(import.meta.url);
27
+ const __dirname = dirname(__filename);
28
+ const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, '..', 'database', 'auth.db');
29
+
30
+ console.log('Opening database at:', DB_PATH);
31
+ const db = new Database(DB_PATH);
32
+
33
+ try {
34
+ const sessions = db.prepare('SELECT id, display_name FROM session_metadata').all();
35
+ console.log(`Found ${sessions.length} sessions in database.`);
36
+
37
+ let updatedCount = 0;
38
+ const updateStmt = db.prepare('UPDATE session_metadata SET display_name = ? WHERE id = ?');
39
+
40
+ const cleanup = db.transaction(() => {
41
+ for (const session of sessions) {
42
+ const cleanedName = stripInternalContextPrefix(session.display_name);
43
+ if (cleanedName !== session.display_name) {
44
+ console.log(`Cleaning session ${session.id}: "${session.display_name}" -> "${cleanedName}"`);
45
+ updateStmt.run(cleanedName, session.id);
46
+ updatedCount++;
47
+ }
48
+ }
49
+ });
50
+
51
+ cleanup();
52
+ console.log(`Cleanup complete. Updated ${updatedCount} sessions.`);
53
+ } catch (error) {
54
+ console.error('Error cleaning up database:', error.message);
55
+ } finally {
56
+ db.close();
57
+ }
@@ -0,0 +1,19 @@
1
+ """
2
+ 通用词集合 —— 被 scan_existing_notes.py 和 link_keywords.py 共享。
3
+ 这些词在自动提取关键词和自动链接时应被排除。
4
+ """
5
+
6
+ COMMON_WORDS = {
7
+ # 英语功能词
8
+ 'and', 'the', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
9
+ 'to', 'as', 'or', 'but', 'not', 'a', 'an', 'is', 'are', 'was', 'were',
10
+ 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did',
11
+ 'will', 'would', 'should', 'could', 'may', 'might', 'must',
12
+ 'can', 'need', 'use', 'using', 'via', 'through', 'over',
13
+ 'under', 'between', 'among', 'during', 'without', 'within',
14
+ 'this', 'that', 'these', 'those', 'it', 'its', 'they', 'their',
15
+ 'we', 'you', 'your', 'our', 'my', 'his', 'her',
16
+ # 在论文标题/摘要中过于常见、缺乏区分度的ML术语
17
+ 'model', 'learning', 'training', 'data', 'system', 'method',
18
+ 'approach', 'framework', 'network', 'algorithm', 'task',
19
+ }
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 关键词链接脚本
4
+ 用于 start-my-day skill,在文本中查找关键词并替换为 wikilink
5
+ """
6
+
7
+ import re
8
+ import json
9
+ import sys
10
+ import argparse
11
+ import logging
12
+ from typing import Dict, List, Set, Tuple
13
+
14
+ from common_words import COMMON_WORDS
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def parse_markdown_lines(content: str) -> List[Tuple[str, str, str, bool]]:
20
+ """
21
+ 将 markdown 内容解析为行列表,每行包含:原始行、行类型、行内容、是否在frontmatter中
22
+
23
+ 行类型:
24
+ - 'frontmatter': frontmatter 内容
25
+ - 'code': 代码块
26
+ - 'inline_code': 行内代码
27
+ - 'wikilink': 已存在的 wikilink
28
+ - 'image': 图片链接
29
+ - 'link': 普通链接
30
+ - 'heading': 标题行(以 # 开头)
31
+ - 'normal': 普通文本
32
+
33
+ Args:
34
+ content: markdown 内容
35
+
36
+ Returns:
37
+ 行列表:(原始行, 行类型, 行内容, 是否在frontmatter中)
38
+ """
39
+ lines = []
40
+ in_code_block = False
41
+ code_fence_char = None
42
+ in_frontmatter = False
43
+ frontmatter_count = 0
44
+
45
+ for line in content.split('\n'):
46
+ # 检查 frontmatter 开始/结束
47
+ if line.strip() == '---':
48
+ frontmatter_count += 1
49
+ if frontmatter_count == 1:
50
+ in_frontmatter = True
51
+ lines.append((line, 'frontmatter', line, True))
52
+ continue
53
+ elif frontmatter_count == 2:
54
+ in_frontmatter = False
55
+ lines.append((line, 'frontmatter', line, False))
56
+ continue
57
+
58
+ if in_frontmatter:
59
+ lines.append((line, 'frontmatter', line, True))
60
+ continue
61
+
62
+ # 检查代码块开始/结束
63
+ if line.strip().startswith('```'):
64
+ if not in_code_block:
65
+ in_code_block = True
66
+ code_fence_char = '```'
67
+ else:
68
+ in_code_block = False
69
+ code_fence_char = None
70
+ lines.append((line, 'code', line, False))
71
+ continue
72
+
73
+ if in_code_block:
74
+ lines.append((line, 'code', line, False))
75
+ continue
76
+
77
+ # 解析行类型
78
+ line_type = 'normal'
79
+ processed_content = line
80
+
81
+ # 检查是否是标题行
82
+ if line.strip().startswith('#'):
83
+ line_type = 'heading'
84
+ lines.append((line, 'heading', line, False))
85
+ continue
86
+
87
+ # 检查行内代码
88
+ inline_code_matches = list(re.finditer(r'`[^`]+`', line))
89
+ if inline_code_matches:
90
+ # 将行内代码替换为占位符
91
+ placeholders = []
92
+ for match in inline_code_matches:
93
+ placeholders.append(f'__CODE_{len(placeholders)}__')
94
+ processed_content = re.sub(r'`[^`]+`', lambda m: placeholders.pop(0), line)
95
+ line_type = 'inline_code'
96
+
97
+ # 检查图片(必须在 wikilink 之前,因为 ![[x]] 也包含 [[x]])
98
+ elif re.search(r'!\[\[.*?\]\]', line):
99
+ line_type = 'image'
100
+
101
+ # 检查 wikilink
102
+ elif re.search(r'\[\[.*?\]\]', line):
103
+ line_type = 'wikilink'
104
+
105
+ # 检查普通链接
106
+ elif re.search(r'\[.*?\]\(.*?\)', line):
107
+ line_type = 'link'
108
+
109
+ lines.append((line, line_type, processed_content, False))
110
+
111
+ return lines
112
+
113
+
114
+ def link_keywords_in_text(
115
+ text: str,
116
+ keyword_index: Dict[str, List[str]],
117
+ existing_wikilinks: Set[str]
118
+ ) -> str:
119
+ """
120
+ 在文本中链接关键词
121
+
122
+ Args:
123
+ text: 文本内容
124
+ keyword_index: 关键词索引
125
+ existing_wikilinks: 已存在的 wikilink 集合
126
+
127
+ Returns:
128
+ 处理后的文本
129
+ """
130
+ # 过滤掉通用词和过短/过长的关键词
131
+ filtered_keywords = {}
132
+ for keyword, paths in keyword_index.items():
133
+ keyword_lower = keyword.lower()
134
+ # 跳过通用词
135
+ if keyword_lower in COMMON_WORDS:
136
+ continue
137
+ # 跳过太短(<3字符)或太长(>30字符)的关键词
138
+ if len(keyword) < 3 or len(keyword) > 30:
139
+ continue
140
+ # 跳过纯数字
141
+ if keyword.isdigit():
142
+ continue
143
+ filtered_keywords[keyword] = paths
144
+
145
+ # 按关键词长度降序排序,优先匹配更长的关键词
146
+ sorted_keywords = sorted(
147
+ filtered_keywords.keys(),
148
+ key=lambda k: len(k),
149
+ reverse=True
150
+ )
151
+
152
+ result = text
153
+ matched_keywords = set()
154
+
155
+ for keyword in sorted_keywords:
156
+ # 跳过已匹配的关键词
157
+ if keyword in matched_keywords:
158
+ continue
159
+
160
+ # 查找所有匹配(不用 \b 单词边界,支持中文环境)
161
+ # 只匹配完整的关键词,避免匹配单词的一部分
162
+ pattern = r'(?<![a-zA-Z0-9_-])' + re.escape(keyword) + r'(?![a-zA-Z0-9_-])'
163
+
164
+ matches = list(re.finditer(pattern, result, re.IGNORECASE))
165
+
166
+ if matches:
167
+ # 获取笔记路径
168
+ note_paths = filtered_keywords[keyword]
169
+ if note_paths:
170
+ # 使用第一个笔记路径
171
+ note_path = note_paths[0]
172
+ # 构建 wikilink:路径|关键词,这样显示的是关键词,链接到具体笔记
173
+ wikilink = f'[[{note_path}|{keyword}]]'
174
+
175
+ # 替换所有匹配(从后往前替换,避免索引变化)
176
+ for match in reversed(matches):
177
+ start, end = match.span()
178
+
179
+ # 检查这个匹配是否已经在 wikilink 中
180
+ # 查找匹配位置最近的 [[ 和 ]]
181
+ bracket_before = result.rfind('[[', 0, start)
182
+ bracket_after = result.find(']]', end)
183
+
184
+ # 如果存在 [[ 在前且 ]] 在后,且这个匹配在它们之间,说明已经在 wikilink 中
185
+ if bracket_before != -1 and bracket_after != -1 and bracket_before < start and bracket_after > end:
186
+ # 这个匹配已经在 wikilink 中,跳过
187
+ continue
188
+
189
+ # 替换为 wikilink
190
+ result = result[:start] + wikilink + result[end:]
191
+
192
+ matched_keywords.add(keyword)
193
+
194
+ return result
195
+
196
+
197
+ def link_keywords_in_file(
198
+ input_file: str,
199
+ output_file: str,
200
+ keyword_index: Dict[str, List[str]]
201
+ ) -> None:
202
+ """
203
+ 处理输入文件并输出
204
+
205
+ Args:
206
+ input_file: 输入文件路径
207
+ output_file: 输出文件路径
208
+ keyword_index: 关键词索引
209
+ """
210
+ # 读取输入文件
211
+ with open(input_file, 'r', encoding='utf-8') as f:
212
+ content = f.read()
213
+
214
+ # 解析为行
215
+ lines = parse_markdown_lines(content)
216
+
217
+ # 收集已有的 wikilink
218
+ existing_wikilinks = set()
219
+ for original_line, line_type, _, _ in lines:
220
+ if line_type == 'wikilink':
221
+ # 提取 wikilink 中的内容
222
+ matches = re.findall(r'\[\[(.*?)\]\]', original_line)
223
+ for match in matches:
224
+ # 提取 wikilink 的路径部分
225
+ parts = match.split('|')
226
+ if parts:
227
+ existing_wikilinks.add(parts[0].lower())
228
+
229
+ # 处理每行
230
+ processed_lines = []
231
+ for original_line, line_type, line_content, in_frontmatter in lines:
232
+ if line_type in ['frontmatter', 'code', 'wikilink', 'image', 'link', 'heading']:
233
+ # 不处理 frontmatter、代码块、已有 wikilink、图片、链接、标题
234
+ processed_lines.append(original_line)
235
+ elif line_type == 'inline_code':
236
+ # 不替换行内代码中的内容
237
+ processed_lines.append(original_line)
238
+ else:
239
+ # 处理普通文本
240
+ processed_line = link_keywords_in_text(line_content, keyword_index, existing_wikilinks)
241
+ processed_lines.append(processed_line)
242
+
243
+ # 合并结果
244
+ result = '\n'.join(processed_lines)
245
+
246
+ # 写入输出文件
247
+ with open(output_file, 'w', encoding='utf-8') as f:
248
+ f.write(result)
249
+
250
+ # 统计信息
251
+ original_links = len(re.findall(r'\[\[.*?\]\]', content))
252
+ new_links = len(re.findall(r'\[\[.*?\]\]', result))
253
+ added_links = new_links - original_links
254
+
255
+ logger.info("Processed file: %s", input_file)
256
+ logger.info(" Original wikilinks: %d", original_links)
257
+ logger.info(" New wikilinks: %d", new_links)
258
+ logger.info(" Added wikilinks: %d", added_links)
259
+
260
+
261
+ def main():
262
+ """主函数"""
263
+ parser = argparse.ArgumentParser(description='Link keywords to existing notes')
264
+ parser.add_argument('--index', type=str, required=True,
265
+ help='Path to keyword index JSON file')
266
+ parser.add_argument('--input', type=str, required=True,
267
+ help='Input file path (markdown)')
268
+ parser.add_argument('--output', type=str, required=True,
269
+ help='Output file path (markdown)')
270
+
271
+ args = parser.parse_args()
272
+
273
+ # 读取关键词索引
274
+ with open(args.index, 'r', encoding='utf-8') as f:
275
+ index_data = json.load(f)
276
+
277
+ keyword_index = index_data.get('keyword_to_notes', {})
278
+
279
+ # 过滤通用词
280
+ logging.basicConfig(
281
+ level=logging.INFO,
282
+ format='%(asctime)s [%(levelname)s] %(message)s',
283
+ datefmt='%H:%M:%S',
284
+ stream=sys.stderr,
285
+ )
286
+
287
+ filtered_count = len([k for k in keyword_index if k.lower() in COMMON_WORDS])
288
+ logger.info("Loaded index with %d keywords", len(keyword_index))
289
+ if filtered_count > 0:
290
+ logger.info(" Filtered %d common words", filtered_count)
291
+
292
+ link_keywords_in_file(args.input, args.output, keyword_index)
293
+
294
+ logger.info("Output saved to: %s", args.output)
295
+
296
+
297
+ if __name__ == '__main__':
298
+ main()