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.
- package/LICENSE +1380 -0
- package/NOTICE +17 -0
- package/README.md +875 -0
- package/README.zh-CN.md +857 -0
- package/dist/api-docs.html +878 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
- package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
- package/dist/assets/arc-DOMBmC_A.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
- package/dist/assets/channel-BFOE7OyC.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
- package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
- package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
- package/dist/assets/clone-BulQSblH.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
- package/dist/assets/graph-Blm-emkR.js +1 -0
- package/dist/assets/index-B8Yutsnm.css +1 -0
- package/dist/assets/index-Cf6gyhc3.js +654 -0
- package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
- package/dist/assets/layout-Ct2kFIId.js +1 -0
- package/dist/assets/linear-CKG9EwIJ.js +1 -0
- package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
- package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
- package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
- package/dist/clear-cache.html +85 -0
- package/dist/convert-icons.md +53 -0
- package/dist/dr-claw.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/generate-icons.js +49 -0
- package/dist/icons/claude-ai-icon.svg +1 -0
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/icons/cursor.svg +1 -0
- package/dist/icons/file.svg +16997 -0
- package/dist/icons/generate-icons.md +19 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/news/arxiv.svg +1 -0
- package/dist/icons/news/huggingface.svg +8 -0
- package/dist/icons/news/x-black.png +0 -0
- package/dist/icons/news/x-white.png +0 -0
- package/dist/icons/news/xiaohongshu.png +0 -0
- package/dist/index.html +51 -0
- package/dist/leveraged-cognition.png +0 -0
- package/dist/manifest.json +61 -0
- package/dist/screenshots/chat.png +0 -0
- package/dist/screenshots/new_claw.png +0 -0
- package/dist/screenshots/news_dashboard.png +0 -0
- package/dist/screenshots/project_dashboard.png +0 -0
- package/dist/screenshots/research_lab.png +0 -0
- package/dist/screenshots/skill_library.png +0 -0
- package/dist/screenshots/task_list.png +0 -0
- package/dist/screenshots/terminal_example.png +0 -0
- package/dist/screenshots/terminal_example1.png +0 -0
- package/dist/sw.js +58 -0
- package/dist/wechat-group-qr.jpg +0 -0
- package/package.json +133 -0
- package/public/api-docs.html +878 -0
- package/public/clear-cache.html +85 -0
- package/public/convert-icons.md +53 -0
- package/public/dr-claw.png +0 -0
- package/public/favicon.png +0 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/codex-white.svg +3 -0
- package/public/icons/codex.svg +3 -0
- package/public/icons/cursor-white.svg +12 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/file.svg +16997 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/news/arxiv.svg +1 -0
- package/public/icons/news/huggingface.svg +8 -0
- package/public/icons/news/x-black.png +0 -0
- package/public/icons/news/x-white.png +0 -0
- package/public/icons/news/xiaohongshu.png +0 -0
- package/public/leveraged-cognition.png +0 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/chat.png +0 -0
- package/public/screenshots/new_claw.png +0 -0
- package/public/screenshots/news_dashboard.png +0 -0
- package/public/screenshots/project_dashboard.png +0 -0
- package/public/screenshots/research_lab.png +0 -0
- package/public/screenshots/skill_library.png +0 -0
- package/public/screenshots/task_list.png +0 -0
- package/public/screenshots/terminal_example.png +0 -0
- package/public/screenshots/terminal_example1.png +0 -0
- package/public/sw.js +58 -0
- package/public/wechat-group-qr.jpg +0 -0
- package/scripts/export-skills-catalog-v2.mjs +746 -0
- package/scripts/fix-node-pty.js +45 -0
- package/scripts/gemini_image_core.py +470 -0
- package/scripts/install-python-tools.js +64 -0
- package/server/AGENTS.md +73 -0
- package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
- package/server/__tests__/runtimePorts.test.mjs +115 -0
- package/server/__tests__/session-delete.test.mjs +166 -0
- package/server/bin/compute-node +70 -0
- package/server/claude-sdk.js +799 -0
- package/server/cli-chat.js +488 -0
- package/server/cli.js +354 -0
- package/server/compute-node.js +439 -0
- package/server/constants/config.js +5 -0
- package/server/cursor-cli.js +340 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +1852 -0
- package/server/database/init.sql +194 -0
- package/server/gemini-cli.js +1352 -0
- package/server/index.js +3154 -0
- package/server/load-env.js +71 -0
- package/server/local-gpu.js +860 -0
- package/server/middleware/auth.js +100 -0
- package/server/openai-codex.js +724 -0
- package/server/openrouter.js +890 -0
- package/server/project-token-usage.js +407 -0
- package/server/projects.js +3973 -0
- package/server/routes/agent.js +1272 -0
- package/server/routes/auth.js +128 -0
- package/server/routes/auto-research.js +558 -0
- package/server/routes/cli-auth.js +958 -0
- package/server/routes/codex.js +344 -0
- package/server/routes/commands.js +601 -0
- package/server/routes/compute.js +358 -0
- package/server/routes/cursor.js +795 -0
- package/server/routes/git.js +1348 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +618 -0
- package/server/routes/news.js +711 -0
- package/server/routes/projects.js +633 -0
- package/server/routes/references.js +338 -0
- package/server/routes/settings.js +285 -0
- package/server/routes/skills.js +914 -0
- package/server/routes/taskmaster.js +2566 -0
- package/server/routes/telemetry.js +89 -0
- package/server/routes/user.js +155 -0
- package/server/scripts/fix_session_names.js +57 -0
- package/server/scripts/research-news/common_words.py +19 -0
- package/server/scripts/research-news/link_keywords.py +298 -0
- package/server/scripts/research-news/scan_existing_notes.py +266 -0
- package/server/scripts/research-news/scoring_utils.py +227 -0
- package/server/scripts/research-news/search_arxiv.py +842 -0
- package/server/scripts/research-news/search_huggingface.py +438 -0
- package/server/scripts/research-news/search_x.py +490 -0
- package/server/scripts/research-news/search_xiaohongshu.py +610 -0
- package/server/taskmaster-templates/README.md +150 -0
- package/server/taskmaster-templates/README.zh-CN.md +150 -0
- package/server/taskmaster-templates/ai-research-dataset.json +230 -0
- package/server/taskmaster-templates/ai-research-method-model.json +230 -0
- package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
- package/server/telemetry.js +121 -0
- package/server/templates/AGENTS.md +103 -0
- package/server/templates/CLAUDE.md +104 -0
- package/server/templates/GEMINI.md +103 -0
- package/server/templates/cursor-project.md +108 -0
- package/server/templates/index.js +37 -0
- package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
- package/server/utils/auto-research-mailer.js +57 -0
- package/server/utils/cliResolution.js +170 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/cursorCommand.js +61 -0
- package/server/utils/geminiApiKey.js +28 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/imageAttachmentFiles.js +35 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/parsers/bibtex-parser.js +207 -0
- package/server/utils/permissions.js +100 -0
- package/server/utils/runtimePorts.js +212 -0
- package/server/utils/sessionFormatting.js +68 -0
- package/server/utils/sessionIndex.js +100 -0
- package/server/utils/sessionMode.js +95 -0
- package/server/utils/sessionTokenUsage.js +105 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/server/utils/zotero-client.js +136 -0
- package/shared/errorClassifier.js +102 -0
- package/shared/geminiThinkingSupport.d.ts +22 -0
- package/shared/geminiThinkingSupport.js +96 -0
- package/shared/geminiThoughtParser.d.ts +8 -0
- package/shared/geminiThoughtParser.js +30 -0
- 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
|
+
}
|
package/server/AGENTS.md
ADDED
|
@@ -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
|
+
```
|