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,45 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fix node-pty spawn-helper permissions on macOS.
4
+ *
5
+ * node-pty prebuilds can ship spawn-helper without execute permissions,
6
+ * which causes "posix_spawnp failed" when creating terminal sessions.
7
+ */
8
+
9
+ import { promises as fs } from "fs";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ async function fixSpawnHelper() {
17
+ if (process.platform !== "darwin") return;
18
+
19
+ const nodeModulesPath = path.join(
20
+ __dirname,
21
+ "..",
22
+ "node_modules",
23
+ "node-pty",
24
+ "prebuilds",
25
+ );
26
+ const darwinDirs = ["darwin-arm64", "darwin-x64"];
27
+
28
+ for (const dir of darwinDirs) {
29
+ const spawnHelperPath = path.join(nodeModulesPath, dir, "spawn-helper");
30
+
31
+ try {
32
+ await fs.access(spawnHelperPath);
33
+ await fs.chmod(spawnHelperPath, 0o755);
34
+ console.log(`[postinstall] Fixed permissions for ${spawnHelperPath}`);
35
+ } catch (err) {
36
+ if (err.code !== "ENOENT") {
37
+ console.warn(
38
+ `[postinstall] Warning: Could not fix ${spawnHelperPath}: ${err.message}`,
39
+ );
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ fixSpawnHelper().catch(console.error);
@@ -0,0 +1,470 @@
1
+ """
2
+ gemini_image_core.py — Distilled core Gemini API functions from PaperBanana.
3
+
4
+ Two essential capabilities:
5
+ 1. VLM: Send text (+ optional images) → get text back (planning, critique, etc.)
6
+ 2. Image Gen: Send text prompt → get image back
7
+
8
+ Model tiers (all work with free GOOGLE_API_KEY from aistudio.google.com):
9
+
10
+ PRESETS["free"] — gemini-2.5-flash + gemini-2.5-flash-image (fastest, free)
11
+ PRESETS["pro"] — gemini-3.1-pro-preview + gemini-3-pro-image-preview (best quality)
12
+ PRESETS["ultra"] — gemini-3.1-pro-preview + imagen-4.0-ultra (highest res images)
13
+ PRESETS["nano"] — gemini-2.5-flash + nano-banana-pro-preview (academic diagrams)
14
+
15
+ Usage:
16
+ from gemini_image_core import GeminiCore
17
+
18
+ # Free tier (default)
19
+ core = GeminiCore()
20
+
21
+ # Pro tier
22
+ core = GeminiCore.from_preset("pro")
23
+
24
+ # Ultra images + pro VLM
25
+ core = GeminiCore.from_preset("ultra")
26
+
27
+ # Generate an image from a text prompt
28
+ img = await core.generate_image("A NeurIPS-style diagram of a transformer")
29
+ img.save("output.png")
30
+
31
+ # Ask VLM to analyze/plan/critique (text-only)
32
+ response = await core.vlm("Describe a 2-panel layout for this method: ...")
33
+
34
+ # Ask VLM with an image (e.g., critique a generated diagram)
35
+ from PIL import Image
36
+ img = Image.open("diagram.png")
37
+ critique = await core.vlm("Critique this diagram for accuracy", images=[img])
38
+
39
+ # Full pipeline: prompt → plan → style → render → critique → refine
40
+ result = await core.pipeline(
41
+ source_context="Method description with LaTeX...",
42
+ caption="Figure 1. Overview of our method.",
43
+ iterations=2,
44
+ )
45
+ result["image"].save("figure1.png")
46
+
47
+ Requires:
48
+ pip install google-genai pillow tenacity
49
+ export GOOGLE_API_KEY="your-key" # free from https://aistudio.google.com/app/apikey
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import asyncio
55
+ import base64
56
+ import io
57
+ import json
58
+ import os
59
+ from pathlib import Path
60
+ from typing import Optional
61
+
62
+ from PIL import Image
63
+ from tenacity import retry, stop_after_attempt, wait_exponential
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Model presets
67
+ # ---------------------------------------------------------------------------
68
+
69
+ PRESETS = {
70
+ "free": {
71
+ "vlm_model": "gemini-2.5-flash",
72
+ "image_model": "gemini-2.5-flash-image",
73
+ "image_api": "generate_content", # uses generateContent with IMAGE modality
74
+ },
75
+ "pro": {
76
+ "vlm_model": "gemini-3.1-pro-preview",
77
+ "image_model": "gemini-3-pro-image-preview",
78
+ "image_api": "generate_content",
79
+ },
80
+ "ultra": {
81
+ "vlm_model": "gemini-3.1-pro-preview",
82
+ "image_model": "imagen-4.0-ultra-generate-001",
83
+ "image_api": "generate_images", # uses Imagen API (generate_images)
84
+ },
85
+ "nano": {
86
+ "vlm_model": "gemini-2.5-flash",
87
+ "image_model": "nano-banana-pro-preview",
88
+ "image_api": "generate_content",
89
+ },
90
+ }
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Core wrapper
94
+ # ---------------------------------------------------------------------------
95
+
96
+ class GeminiCore:
97
+ """Minimal wrapper around Google Gemini for VLM + image generation."""
98
+
99
+ def __init__(
100
+ self,
101
+ api_key: Optional[str] = None,
102
+ vlm_model: str = "gemini-2.5-flash",
103
+ image_model: str = "gemini-2.5-flash-image",
104
+ image_api: str = "generate_content",
105
+ ):
106
+ from google import genai
107
+
108
+ self.api_key = api_key or os.environ.get("GOOGLE_API_KEY")
109
+ if not self.api_key:
110
+ raise ValueError(
111
+ "No API key. Set GOOGLE_API_KEY env var or pass api_key=. "
112
+ "Free key: https://aistudio.google.com/app/apikey"
113
+ )
114
+ self.client = genai.Client(api_key=self.api_key)
115
+ self.vlm_model = vlm_model
116
+ self.image_model = image_model
117
+ self.image_api = image_api # "generate_content" or "generate_images"
118
+
119
+ @classmethod
120
+ def from_preset(cls, preset: str = "free", **kwargs) -> "GeminiCore":
121
+ """Create from a named preset: 'free', 'pro', 'ultra', 'nano'."""
122
+ if preset not in PRESETS:
123
+ raise ValueError(f"Unknown preset '{preset}'. Choose: {list(PRESETS)}")
124
+ cfg = {**PRESETS[preset], **kwargs}
125
+ return cls(**cfg)
126
+
127
+ # --- VLM: text (+images) → text ---
128
+
129
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential(min=2, max=60))
130
+ async def vlm(
131
+ self,
132
+ prompt: str,
133
+ images: Optional[list[Image.Image]] = None,
134
+ system_prompt: Optional[str] = None,
135
+ temperature: float = 0.7,
136
+ max_tokens: int = 4096,
137
+ json_mode: bool = False,
138
+ ) -> str:
139
+ """Send text (+ optional images) to Gemini VLM, get text back.
140
+
141
+ Args:
142
+ prompt: The user prompt.
143
+ images: Optional list of PIL Image objects to include.
144
+ system_prompt: Optional system instruction.
145
+ temperature: Sampling temperature (0.0-2.0).
146
+ max_tokens: Max output tokens.
147
+ json_mode: If True, request JSON response format.
148
+
149
+ Returns:
150
+ Model response text.
151
+ """
152
+ from google.genai import types
153
+
154
+ contents = []
155
+
156
+ # Add images first (if any)
157
+ if images:
158
+ for img in images:
159
+ buf = io.BytesIO()
160
+ img.save(buf, format="PNG")
161
+ contents.append(
162
+ types.Part.from_bytes(data=buf.getvalue(), mime_type="image/png")
163
+ )
164
+
165
+ # Add text prompt
166
+ contents.append(prompt)
167
+
168
+ config = types.GenerateContentConfig(
169
+ temperature=temperature,
170
+ max_output_tokens=max_tokens,
171
+ )
172
+ if system_prompt:
173
+ config.system_instruction = system_prompt
174
+ if json_mode:
175
+ config.response_mime_type = "application/json"
176
+
177
+ response = self.client.models.generate_content(
178
+ model=self.vlm_model,
179
+ contents=contents,
180
+ config=config,
181
+ )
182
+ return response.text
183
+
184
+ # --- Image Gen: text → image ---
185
+
186
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10))
187
+ async def generate_image(
188
+ self,
189
+ prompt: str,
190
+ width: int = 1792,
191
+ height: int = 1024,
192
+ seed: Optional[int] = None,
193
+ ) -> Image.Image:
194
+ """Generate an image from a text prompt.
195
+
196
+ Automatically routes to the correct API based on image_model:
197
+ - Gemini models (gemini-*, nano-banana-*): uses generate_content
198
+ - Imagen models (imagen-*): uses generate_images
199
+
200
+ Args:
201
+ prompt: Detailed description of the image to generate.
202
+ width: Target width (used for aspect ratio selection).
203
+ height: Target height (used for aspect ratio selection).
204
+ seed: Optional seed for reproducibility.
205
+
206
+ Returns:
207
+ PIL Image object.
208
+ """
209
+ if self.image_api == "generate_images":
210
+ return await self._generate_via_imagen(prompt, width, height, seed)
211
+ else:
212
+ return await self._generate_via_content(prompt, width, height)
213
+
214
+ async def _generate_via_content(
215
+ self, prompt: str, width: int, height: int
216
+ ) -> Image.Image:
217
+ """Image gen via generateContent API (Gemini native image models)."""
218
+ from google.genai import types
219
+
220
+ aspect = self._aspect_ratio(width, height)
221
+ size = self._resolution_tier(width, height)
222
+
223
+ config = types.GenerateContentConfig(
224
+ response_modalities=["IMAGE"],
225
+ image_config=types.ImageConfig(
226
+ aspect_ratio=aspect,
227
+ image_size=size,
228
+ ),
229
+ )
230
+
231
+ response = self.client.models.generate_content(
232
+ model=self.image_model,
233
+ contents=prompt,
234
+ config=config,
235
+ )
236
+
237
+ return self._extract_image_from_content(response)
238
+
239
+ async def _generate_via_imagen(
240
+ self, prompt: str, width: int, height: int, seed: Optional[int]
241
+ ) -> Image.Image:
242
+ """Image gen via Imagen API (generate_images endpoint)."""
243
+ from google.genai import types
244
+
245
+ config = types.GenerateImagesConfig(number_of_images=1)
246
+
247
+ response = self.client.models.generate_images(
248
+ model=self.image_model,
249
+ prompt=prompt,
250
+ config=config,
251
+ )
252
+
253
+ gi = response.generated_images[0]
254
+ # google-genai Image → PIL via internal _pil_image
255
+ if hasattr(gi.image, "_pil_image"):
256
+ return gi.image._pil_image
257
+ # Fallback: use image_bytes
258
+ if hasattr(gi.image, "image_bytes") and gi.image.image_bytes:
259
+ return Image.open(io.BytesIO(gi.image.image_bytes))
260
+ raise RuntimeError("Could not extract PIL image from Imagen response")
261
+
262
+ @staticmethod
263
+ def _extract_image_from_content(response) -> Image.Image:
264
+ """Extract PIL Image from a generateContent response."""
265
+ candidate = response.candidates[0]
266
+ part = candidate.content.parts[0]
267
+
268
+ genai_img = part.as_image()
269
+
270
+ # google-genai Image wraps PIL internally
271
+ if hasattr(genai_img, "_pil_image"):
272
+ return genai_img._pil_image
273
+ # Try image_bytes
274
+ if hasattr(genai_img, "image_bytes") and genai_img.image_bytes:
275
+ return Image.open(io.BytesIO(genai_img.image_bytes))
276
+ # Try as PIL directly (older SDK versions)
277
+ if isinstance(genai_img, Image.Image):
278
+ return genai_img
279
+ raise RuntimeError("Could not extract PIL image from response")
280
+
281
+ @staticmethod
282
+ def _aspect_ratio(width: int, height: int) -> str:
283
+ ratio = width / height
284
+ if ratio > 1.6:
285
+ return "16:9"
286
+ elif ratio > 1.3:
287
+ return "3:2"
288
+ elif ratio > 0.8:
289
+ return "1:1"
290
+ elif ratio > 0.55:
291
+ return "2:3"
292
+ else:
293
+ return "9:16"
294
+
295
+ @staticmethod
296
+ def _resolution_tier(width: int, height: int) -> str:
297
+ max_dim = max(width, height)
298
+ if max_dim <= 1024:
299
+ return "1K"
300
+ elif max_dim <= 2048:
301
+ return "2K"
302
+ else:
303
+ return "4K"
304
+
305
+ # --- Mini pipeline: plan → style → render → critique → refine ---
306
+
307
+ async def pipeline(
308
+ self,
309
+ source_context: str,
310
+ caption: str,
311
+ iterations: int = 2,
312
+ output_dir: Optional[str] = None,
313
+ ) -> dict:
314
+ """Full pipeline: plan → style → render → critique → refine.
315
+
316
+ Args:
317
+ source_context: Method description (the SOP prompt or raw text).
318
+ caption: Figure caption.
319
+ iterations: Max critique-refine iterations.
320
+ output_dir: Optional directory to save intermediate files.
321
+
322
+ Returns:
323
+ dict with keys: "image" (PIL Image), "description" (str),
324
+ "critique" (str or None), "iterations_used" (int).
325
+ """
326
+ if output_dir:
327
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
328
+
329
+ # Phase 1: Plan
330
+ plan = await self.vlm(
331
+ prompt=(
332
+ "I need a detailed visual description for a scientific diagram.\n\n"
333
+ f"## Methodology\n{source_context}\n\n"
334
+ f"## Caption\n{caption}\n\n"
335
+ "Describe every visual element: layout, boxes, arrows, colors, "
336
+ "labels, math notation, flow direction. Be specific about "
337
+ "spatial arrangement and styling. Use natural language for "
338
+ "colors (e.g., 'soft sky blue', not hex codes)."
339
+ ),
340
+ temperature=0.7,
341
+ )
342
+
343
+ # Phase 1: Style refinement
344
+ description = await self.vlm(
345
+ prompt=(
346
+ "You are a visual designer for top-tier AI conferences.\n\n"
347
+ "Refine this diagram description for professional NeurIPS/ICML "
348
+ "aesthetics. Use soft pastel tones, clean sans-serif labels, "
349
+ "rounded rectangles, and clear data flow. Preserve ALL content "
350
+ "and math. Output ONLY the refined description.\n\n"
351
+ f"## Description\n{plan}"
352
+ ),
353
+ temperature=0.5,
354
+ )
355
+
356
+ # Phase 2: Iterative render + critique
357
+ image = None
358
+ critique_text = None
359
+ iterations_used = 0
360
+
361
+ for i in range(iterations):
362
+ iterations_used = i + 1
363
+
364
+ # Render
365
+ render_prompt = (
366
+ "You are an expert scientific diagram illustrator. Generate a "
367
+ "high-quality diagram. Do not include figure titles in the image. "
368
+ "All text must be clear, readable English. Use EXACT label names "
369
+ f"from the description.\n\n{description}"
370
+ )
371
+ image = await self.generate_image(render_prompt)
372
+
373
+ if output_dir:
374
+ image.save(Path(output_dir) / f"iter_{i + 1}.png")
375
+
376
+ # Critique
377
+ critique_json = await self.vlm(
378
+ prompt=(
379
+ "Critique this scientific diagram.\n\n"
380
+ f"## Methodology\n{source_context}\n\n"
381
+ f"## Caption\n{caption}\n\n"
382
+ f"## Description\n{description}\n\n"
383
+ "Check: (1) fidelity to methodology, (2) typos/garbled text, "
384
+ "(3) missing elements, (4) visual clarity.\n\n"
385
+ 'Return JSON: {"critic_suggestions": ["..."], '
386
+ '"revised_description": "..." or null if publication-ready}'
387
+ ),
388
+ images=[image],
389
+ temperature=0.3,
390
+ json_mode=True,
391
+ )
392
+
393
+ try:
394
+ critique = json.loads(critique_json)
395
+ except json.JSONDecodeError:
396
+ break
397
+
398
+ critique_text = "; ".join(critique.get("critic_suggestions", []))
399
+
400
+ if not critique.get("revised_description"):
401
+ break
402
+
403
+ description = critique["revised_description"]
404
+
405
+ if output_dir and image:
406
+ image.save(Path(output_dir) / "final.png")
407
+
408
+ return {
409
+ "image": image,
410
+ "description": description,
411
+ "critique": critique_text,
412
+ "iterations_used": iterations_used,
413
+ }
414
+
415
+
416
+ # ---------------------------------------------------------------------------
417
+ # CLI convenience
418
+ # ---------------------------------------------------------------------------
419
+
420
+ def main():
421
+ """Quick CLI for image generation and pipelines.
422
+
423
+ Usage:
424
+ python gemini_image_core.py "prompt" [output.png] [--preset pro]
425
+ python gemini_image_core.py --pipeline prompt_file.md [output_dir] [--preset ultra]
426
+ python gemini_image_core.py --list-presets
427
+ """
428
+ import sys
429
+
430
+ args = [a for a in sys.argv[1:] if not a.startswith("--preset")]
431
+ preset = "free"
432
+ for i, a in enumerate(sys.argv):
433
+ if a == "--preset" and i + 1 < len(sys.argv):
434
+ preset = sys.argv[i + 1]
435
+
436
+ if not args or args[0] == "--help":
437
+ print("Usage:")
438
+ print(' python gemini_image_core.py "prompt" [output.png] [--preset pro]')
439
+ print(" python gemini_image_core.py --pipeline prompt.md [out_dir] [--preset ultra]")
440
+ print(" python gemini_image_core.py --list-presets")
441
+ print(f"\nPresets: {list(PRESETS.keys())}")
442
+ sys.exit(0)
443
+
444
+ if args[0] == "--list-presets":
445
+ for name, cfg in PRESETS.items():
446
+ print(f" {name:6s} VLM: {cfg['vlm_model']:30s} Image: {cfg['image_model']}")
447
+ sys.exit(0)
448
+
449
+ core = GeminiCore.from_preset(preset)
450
+ print(f"Using preset '{preset}': VLM={core.vlm_model}, Image={core.image_model}")
451
+
452
+ if args[0] == "--pipeline":
453
+ prompt_file = args[1]
454
+ out_dir = args[2] if len(args) > 2 else "./pipeline_output"
455
+ with open(prompt_file) as f:
456
+ text = f.read()
457
+ result = asyncio.run(
458
+ core.pipeline(source_context=text, caption="", output_dir=out_dir)
459
+ )
460
+ print(f"Done. {result['iterations_used']} iterations. Output: {out_dir}/final.png")
461
+ else:
462
+ prompt = args[0]
463
+ out_path = args[1] if len(args) > 1 else "output.png"
464
+ image = asyncio.run(core.generate_image(prompt))
465
+ image.save(out_path)
466
+ print(f"Saved to {out_path} ({image.size[0]}x{image.size[1]})")
467
+
468
+
469
+ if __name__ == "__main__":
470
+ main()
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Auto-install Python CLI tools needed by research-news scripts.
4
+ *
5
+ * Installs twitter-cli and xiaohongshu-cli via `uv tool install` (preferred)
6
+ * or `pipx install` (fallback). Failures are non-fatal — the tools will
7
+ * attempt auto-install again at runtime if needed.
8
+ */
9
+
10
+ import { execFileSync } from "child_process";
11
+
12
+ const TOOLS = [
13
+ { pkg: "twitter-cli", bin: "twitter" },
14
+ { pkg: "xiaohongshu-cli", bin: "xhs" },
15
+ ];
16
+
17
+ function which(cmd) {
18
+ try {
19
+ return execFileSync("which", [cmd], { encoding: "utf8" }).trim();
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function install(pkg, installer) {
26
+ const args =
27
+ installer === "uv"
28
+ ? ["tool", "install", pkg]
29
+ : ["install", pkg];
30
+ try {
31
+ execFileSync(installer, args, {
32
+ stdio: "pipe",
33
+ timeout: 120_000,
34
+ });
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ const installer = which("uv") ? "uv" : which("pipx") ? "pipx" : null;
42
+
43
+ if (!installer) {
44
+ console.log(
45
+ "[postinstall] Neither uv nor pipx found — skipping Python CLI tools. " +
46
+ "Install them later with: uv tool install twitter-cli xiaohongshu-cli",
47
+ );
48
+ process.exit(0);
49
+ }
50
+
51
+ for (const { pkg, bin } of TOOLS) {
52
+ if (which(bin)) {
53
+ console.log(`[postinstall] ${bin} already installed, skipping.`);
54
+ continue;
55
+ }
56
+ console.log(`[postinstall] Installing ${pkg} via ${installer}...`);
57
+ if (install(pkg, installer)) {
58
+ console.log(`[postinstall] ${pkg} installed successfully.`);
59
+ } else {
60
+ console.warn(
61
+ `[postinstall] Failed to install ${pkg}. You can install it manually: ${installer === "uv" ? "uv tool install" : "pipx install"} ${pkg}`,
62
+ );
63
+ }
64
+ }
@@ -0,0 +1,73 @@
1
+ # server/AGENTS.md — Backend Rules
2
+
3
+ This file applies when working in the `server/` directory.
4
+
5
+ ## Language
6
+
7
+ - **Plain JavaScript only.** No TypeScript, no type annotations, no `.ts` files.
8
+ - ES modules exclusively (`import`/`export`). **Never** use `require()`.
9
+ - All local imports must include the `.js` extension:
10
+ ```js
11
+ import { getProjects } from './projects.js';
12
+ import { authenticateToken } from './middleware/auth.js';
13
+ ```
14
+
15
+ ## Route file template
16
+
17
+ ```js
18
+ import express from 'express';
19
+
20
+ const router = express.Router();
21
+
22
+ router.get('/', async (req, res) => {
23
+ try {
24
+ // handler logic
25
+ res.json({ success: true });
26
+ } catch (error) {
27
+ console.error('[ERROR] Description:', error.message);
28
+ res.status(500).json({ error: error.message });
29
+ }
30
+ });
31
+
32
+ export default router;
33
+ ```
34
+
35
+ ## Route mounting
36
+
37
+ New routes **must** be registered in `server/index.js`:
38
+
39
+ ```js
40
+ import myRoutes from './routes/my-feature.js';
41
+ app.use('/api/my-feature', authenticateToken, myRoutes);
42
+ ```
43
+
44
+ Place the `app.use()` call with the other route registrations (lines ~391-431 in `server/index.js`).
45
+
46
+ ## Authentication
47
+
48
+ - Protected routes use `authenticateToken` middleware from `server/middleware/auth.js`.
49
+ - Public routes (`/api/auth/*`, `/health`) skip auth.
50
+
51
+ ## Error handling
52
+
53
+ - Wrap all async handlers in try/catch.
54
+ - Return proper HTTP status: 400, 403, 404, 500.
55
+ - Log with `console.error('[ERROR]', ...)`.
56
+
57
+ ## Path security
58
+
59
+ Always validate file paths:
60
+ ```js
61
+ const resolved = path.resolve(allowedRoot, userInput);
62
+ if (!resolved.startsWith(allowedRoot + path.sep)) {
63
+ return res.status(403).json({ error: 'Path must be under allowed root' });
64
+ }
65
+ ```
66
+
67
+ ## Model constants
68
+
69
+ Use `shared/modelConstants.js` — never hardcode model names or IDs.
70
+
71
+ ```js
72
+ import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../shared/modelConstants.js';
73
+ ```