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,338 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import multer from 'multer';
|
|
6
|
+
import { referencesDb } from '../database/db.js';
|
|
7
|
+
import { getZoteroClient } from '../utils/zotero-client.js';
|
|
8
|
+
import { parseBibtex } from '../utils/parsers/bibtex-parser.js';
|
|
9
|
+
|
|
10
|
+
const router = express.Router();
|
|
11
|
+
|
|
12
|
+
// Multer for BibTeX file upload (in-memory, max 5 MB)
|
|
13
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 } });
|
|
14
|
+
|
|
15
|
+
// PDF cache directory
|
|
16
|
+
const PDF_CACHE_DIR = path.join(os.homedir(), '.dr-claw', 'references', 'pdfs');
|
|
17
|
+
|
|
18
|
+
function ensurePdfCacheDir() {
|
|
19
|
+
if (!fs.existsSync(PDF_CACHE_DIR)) {
|
|
20
|
+
fs.mkdirSync(PDF_CACHE_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// IMPORTANT: All literal/multi-segment routes MUST come before /:id
|
|
26
|
+
// to avoid Express matching "tags", "project", "zotero" etc. as an :id param.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** GET /api/references — list user references (paginated, searchable) */
|
|
30
|
+
router.get('/', async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const { search, tags, limit, offset } = req.query;
|
|
33
|
+
const parsedTags = tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : undefined;
|
|
34
|
+
const refs = referencesDb.getUserReferences(req.user.id, {
|
|
35
|
+
search: search || undefined,
|
|
36
|
+
tags: parsedTags,
|
|
37
|
+
limit: parseInt(limit) || 50,
|
|
38
|
+
offset: parseInt(offset) || 0,
|
|
39
|
+
});
|
|
40
|
+
res.json({ references: refs });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error fetching references:', error);
|
|
43
|
+
res.status(500).json({ error: 'Failed to fetch references' });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** GET /api/references/tags — all user tags */
|
|
48
|
+
router.get('/tags', async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const tags = referencesDb.getTags(req.user.id);
|
|
51
|
+
res.json({ tags });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Error fetching tags:', error);
|
|
54
|
+
res.status(500).json({ error: 'Failed to fetch tags' });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** GET /api/references/zotero/status — check Zotero connectivity */
|
|
59
|
+
router.get('/zotero/status', async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const { mode, localRunning, localApiDisabled } = await getZoteroClient();
|
|
62
|
+
res.json({
|
|
63
|
+
connected: mode !== null,
|
|
64
|
+
mode,
|
|
65
|
+
localAvailable: mode === 'local',
|
|
66
|
+
localRunning,
|
|
67
|
+
localApiDisabled,
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Error checking Zotero status:', error);
|
|
71
|
+
res.json({ connected: false, mode: null, localAvailable: false, localRunning: false, localApiDisabled: false });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** GET /api/references/zotero/collections — list Zotero collections */
|
|
76
|
+
router.get('/zotero/collections', async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const { client, mode } = await getZoteroClient();
|
|
79
|
+
if (!client) {
|
|
80
|
+
return res.status(503).json({ error: 'Zotero not available' });
|
|
81
|
+
}
|
|
82
|
+
const libraries = await client.getLibraries();
|
|
83
|
+
const collections = await client.getCollections(libraries[0]?.id);
|
|
84
|
+
res.json({ collections, mode });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Error fetching Zotero collections:', error);
|
|
87
|
+
res.status(500).json({ error: 'Failed to fetch Zotero collections' });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** GET /api/references/zotero/items — browse Zotero items without importing */
|
|
92
|
+
router.get('/zotero/items', async (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const { client } = await getZoteroClient();
|
|
95
|
+
if (!client) {
|
|
96
|
+
return res.status(503).json({ error: 'Zotero not available' });
|
|
97
|
+
}
|
|
98
|
+
const { collectionKey, limit, start } = req.query;
|
|
99
|
+
if (collectionKey && !/^[A-Za-z0-9]+$/.test(collectionKey)) {
|
|
100
|
+
return res.status(400).json({ error: 'Invalid collectionKey format' });
|
|
101
|
+
}
|
|
102
|
+
const libraries = await client.getLibraries();
|
|
103
|
+
const libraryId = libraries[0]?.id;
|
|
104
|
+
const items = await client.getItems(libraryId, {
|
|
105
|
+
collectionKey: collectionKey || undefined,
|
|
106
|
+
limit: parseInt(limit) || 100,
|
|
107
|
+
start: parseInt(start) || 0,
|
|
108
|
+
});
|
|
109
|
+
// Strip rawData before sending to client
|
|
110
|
+
const mapped = items.map(({ rawData, ...rest }) => rest);
|
|
111
|
+
res.json({ items: mapped });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Error browsing Zotero items:', error);
|
|
114
|
+
res.status(500).json({ error: 'Failed to browse Zotero items' });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** POST /api/references/sync/zotero — sync from Zotero */
|
|
119
|
+
router.post('/sync/zotero', async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const { client, mode, localApiDisabled } = await getZoteroClient();
|
|
122
|
+
if (!client) {
|
|
123
|
+
const error = localApiDisabled
|
|
124
|
+
? 'Zotero is running but the local API is disabled. Enable it in Zotero → Settings → Advanced → Allow other applications to communicate with Zotero.'
|
|
125
|
+
: 'Zotero desktop is not running. Start the Zotero app and try again.';
|
|
126
|
+
return res.status(503).json({ error });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { collectionKey, projectName, sourceIds } = req.body || {};
|
|
130
|
+
if (collectionKey && !/^[A-Za-z0-9]+$/.test(collectionKey)) {
|
|
131
|
+
return res.status(400).json({ error: 'Invalid collectionKey format' });
|
|
132
|
+
}
|
|
133
|
+
const libraries = await client.getLibraries();
|
|
134
|
+
const libraryId = libraries[0]?.id;
|
|
135
|
+
|
|
136
|
+
// Fetch all items (paginated)
|
|
137
|
+
let allItems = [];
|
|
138
|
+
let start = 0;
|
|
139
|
+
const pageSize = 100;
|
|
140
|
+
while (true) {
|
|
141
|
+
const batch = await client.getItems(libraryId, { collectionKey, limit: pageSize, start });
|
|
142
|
+
allItems.push(...batch);
|
|
143
|
+
if (batch.length < pageSize) break;
|
|
144
|
+
start += pageSize;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Filter by sourceIds if provided (selective import)
|
|
148
|
+
if (sourceIds?.length > 0) {
|
|
149
|
+
const allowed = new Set(sourceIds);
|
|
150
|
+
allItems = allItems.filter(item => allowed.has(item.sourceId));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const ids = referencesDb.syncFromZotero(req.user.id, allItems);
|
|
154
|
+
let linked = 0;
|
|
155
|
+
if (projectName && ids.length > 0) {
|
|
156
|
+
linked = referencesDb.bulkLinkIds(projectName, ids);
|
|
157
|
+
}
|
|
158
|
+
res.json({ success: true, synced: ids.length, linked, mode, total: allItems.length });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error syncing Zotero:', error);
|
|
161
|
+
const msg = error.message || 'Unknown error';
|
|
162
|
+
if (msg.includes('ECONNREFUSED')) {
|
|
163
|
+
return res.status(503).json({ error: 'Cannot connect to Zotero desktop. Is it running?' });
|
|
164
|
+
}
|
|
165
|
+
if (error.name === 'AbortError' || msg.includes('timeout')) {
|
|
166
|
+
return res.status(504).json({ error: 'Zotero connection timed out.' });
|
|
167
|
+
}
|
|
168
|
+
if (msg.includes('Zotero API')) {
|
|
169
|
+
return res.status(502).json({ error: msg });
|
|
170
|
+
}
|
|
171
|
+
res.status(500).json({ error: `Sync failed: ${msg}` });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/** POST /api/references/import/bibtex — import BibTeX file */
|
|
176
|
+
router.post('/import/bibtex', upload.single('file'), async (req, res) => {
|
|
177
|
+
try {
|
|
178
|
+
if (!req.file) {
|
|
179
|
+
return res.status(400).json({ error: 'No file provided' });
|
|
180
|
+
}
|
|
181
|
+
const content = req.file.buffer.toString('utf-8');
|
|
182
|
+
const entries = parseBibtex(content);
|
|
183
|
+
if (entries.length === 0) {
|
|
184
|
+
return res.status(400).json({ error: 'No valid BibTeX entries found' });
|
|
185
|
+
}
|
|
186
|
+
const ids = referencesDb.importReferences(req.user.id, entries, 'bibtex');
|
|
187
|
+
const projectName = req.body?.projectName;
|
|
188
|
+
let linked = 0;
|
|
189
|
+
if (projectName && ids.length > 0) {
|
|
190
|
+
linked = referencesDb.bulkLinkIds(projectName, ids);
|
|
191
|
+
}
|
|
192
|
+
res.json({ success: true, imported: ids.length, linked, total: entries.length });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error('Error importing BibTeX:', error);
|
|
195
|
+
const msg = error.message || 'Unknown error';
|
|
196
|
+
res.status(500).json({ error: `Import failed: ${msg}` });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Project ↔ Reference linking (multi-segment — must come before /:id)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/** GET /api/references/project/:projectName — references linked to a project */
|
|
205
|
+
router.get('/project/:projectName', async (req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
const refs = referencesDb.getProjectReferences(req.params.projectName, req.user.id);
|
|
208
|
+
res.json({ references: refs });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error('Error fetching project references:', error);
|
|
211
|
+
res.status(500).json({ error: 'Failed to fetch project references' });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/** POST /api/references/project/:projectName/:id — link reference to project */
|
|
216
|
+
router.post('/project/:projectName/:id', async (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const linked = referencesDb.linkToProject(req.params.projectName, req.params.id, req.user.id);
|
|
219
|
+
if (!linked) {
|
|
220
|
+
return res.status(404).json({ error: 'Reference not found' });
|
|
221
|
+
}
|
|
222
|
+
res.json({ success: true });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('Error linking reference:', error);
|
|
225
|
+
res.status(500).json({ error: 'Failed to link reference to project' });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/** DELETE /api/references/project/:projectName/:id — unlink reference from project */
|
|
230
|
+
router.delete('/project/:projectName/:id', async (req, res) => {
|
|
231
|
+
try {
|
|
232
|
+
const removed = referencesDb.unlinkFromProject(req.params.projectName, req.params.id, req.user.id);
|
|
233
|
+
if (!removed) {
|
|
234
|
+
return res.status(404).json({ error: 'Link not found' });
|
|
235
|
+
}
|
|
236
|
+
res.json({ success: true });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Error unlinking reference:', error);
|
|
239
|
+
res.status(500).json({ error: 'Failed to unlink reference from project' });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Single-item routes (parameterized — must come LAST)
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/** POST /api/references/bulk-delete — delete multiple references */
|
|
248
|
+
router.post('/bulk-delete', async (req, res) => {
|
|
249
|
+
try {
|
|
250
|
+
const { ids } = req.body || {};
|
|
251
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
252
|
+
return res.status(400).json({ error: 'ids array is required' });
|
|
253
|
+
}
|
|
254
|
+
if (ids.length > 500) {
|
|
255
|
+
return res.status(400).json({ error: 'Cannot delete more than 500 references at once' });
|
|
256
|
+
}
|
|
257
|
+
const deleted = referencesDb.bulkDeleteReferences(req.user.id, ids);
|
|
258
|
+
res.json({ success: true, deleted });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('Error bulk-deleting references:', error);
|
|
261
|
+
res.status(500).json({ error: 'Failed to delete references' });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/** GET /api/references/:id/pdf — serve cached or fetch PDF */
|
|
266
|
+
router.get('/:id/pdf', async (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
ensurePdfCacheDir();
|
|
269
|
+
const ref = referencesDb.getReference(req.params.id, req.user.id);
|
|
270
|
+
if (!ref) {
|
|
271
|
+
return res.status(404).json({ error: 'Reference not found' });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Sanitize ID for filesystem path and verify no traversal
|
|
275
|
+
const safeId = ref.id.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
276
|
+
const pdfPath = path.join(PDF_CACHE_DIR, `${safeId}.pdf`);
|
|
277
|
+
const resolvedPath = path.resolve(pdfPath);
|
|
278
|
+
if (!resolvedPath.startsWith(path.resolve(PDF_CACHE_DIR))) {
|
|
279
|
+
return res.status(400).json({ error: 'Invalid reference ID' });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Serve from cache if available
|
|
283
|
+
if (fs.existsSync(pdfPath)) {
|
|
284
|
+
res.setHeader('Content-Type', 'application/pdf');
|
|
285
|
+
return fs.createReadStream(pdfPath).pipe(res);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Try to fetch from Zotero
|
|
289
|
+
if (ref.source === 'zotero' && ref.source_id) {
|
|
290
|
+
const { client } = await getZoteroClient();
|
|
291
|
+
if (client) {
|
|
292
|
+
const libraries = await client.getLibraries();
|
|
293
|
+
const pdfBuffer = await client.getItemPdf(libraries[0]?.id, ref.source_id);
|
|
294
|
+
if (pdfBuffer) {
|
|
295
|
+
fs.writeFileSync(pdfPath, pdfBuffer);
|
|
296
|
+
referencesDb.setPdfCached(ref.id, true);
|
|
297
|
+
res.setHeader('Content-Type', 'application/pdf');
|
|
298
|
+
return res.send(pdfBuffer);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
res.status(404).json({ error: 'PDF not available' });
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error('Error fetching PDF:', error);
|
|
306
|
+
res.status(500).json({ error: 'Failed to fetch PDF' });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
/** GET /api/references/:id — single reference detail */
|
|
311
|
+
router.get('/:id', async (req, res) => {
|
|
312
|
+
try {
|
|
313
|
+
const ref = referencesDb.getReference(req.params.id, req.user.id);
|
|
314
|
+
if (!ref) {
|
|
315
|
+
return res.status(404).json({ error: 'Reference not found' });
|
|
316
|
+
}
|
|
317
|
+
res.json({ reference: ref });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('Error fetching reference:', error);
|
|
320
|
+
res.status(500).json({ error: 'Failed to fetch reference' });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
/** DELETE /api/references/:id — delete a reference */
|
|
325
|
+
router.delete('/:id', async (req, res) => {
|
|
326
|
+
try {
|
|
327
|
+
const deleted = referencesDb.deleteReference(req.user.id, req.params.id);
|
|
328
|
+
if (!deleted) {
|
|
329
|
+
return res.status(404).json({ error: 'Reference not found' });
|
|
330
|
+
}
|
|
331
|
+
res.json({ success: true });
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error('Error deleting reference:', error);
|
|
334
|
+
res.status(500).json({ error: 'Failed to delete reference' });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
export default router;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { apiKeysDb, appSettingsDb, credentialsDb } from '../database/db.js';
|
|
3
|
+
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const AUTO_RESEARCH_SENDER_EMAIL_KEY = 'auto_research_sender_email';
|
|
6
|
+
const AUTO_RESEARCH_RESEND_API_KEY = 'auto_research_resend_api_key';
|
|
7
|
+
|
|
8
|
+
// ===============================
|
|
9
|
+
// API Keys Management
|
|
10
|
+
// ===============================
|
|
11
|
+
|
|
12
|
+
// Get all API keys for the authenticated user
|
|
13
|
+
router.get('/api-keys', async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const apiKeys = apiKeysDb.getApiKeys(req.user.id);
|
|
16
|
+
// Don't send the full API key in the list for security
|
|
17
|
+
const sanitizedKeys = apiKeys.map(key => ({
|
|
18
|
+
...key,
|
|
19
|
+
api_key: key.api_key.substring(0, 10) + '...'
|
|
20
|
+
}));
|
|
21
|
+
res.json({ apiKeys: sanitizedKeys });
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error fetching API keys:', error);
|
|
24
|
+
res.status(500).json({ error: 'Failed to fetch API keys' });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Create a new API key
|
|
29
|
+
router.post('/api-keys', async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const { keyName } = req.body;
|
|
32
|
+
|
|
33
|
+
if (!keyName || !keyName.trim()) {
|
|
34
|
+
return res.status(400).json({ error: 'Key name is required' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = apiKeysDb.createApiKey(req.user.id, keyName.trim());
|
|
38
|
+
res.json({
|
|
39
|
+
success: true,
|
|
40
|
+
apiKey: result
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Error creating API key:', error);
|
|
44
|
+
res.status(500).json({ error: 'Failed to create API key' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Delete an API key
|
|
49
|
+
router.delete('/api-keys/:keyId', async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const { keyId } = req.params;
|
|
52
|
+
const success = apiKeysDb.deleteApiKey(req.user.id, parseInt(keyId));
|
|
53
|
+
|
|
54
|
+
if (success) {
|
|
55
|
+
res.json({ success: true });
|
|
56
|
+
} else {
|
|
57
|
+
res.status(404).json({ error: 'API key not found' });
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error deleting API key:', error);
|
|
61
|
+
res.status(500).json({ error: 'Failed to delete API key' });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Toggle API key active status
|
|
66
|
+
router.patch('/api-keys/:keyId/toggle', async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const { keyId } = req.params;
|
|
69
|
+
const { isActive } = req.body;
|
|
70
|
+
|
|
71
|
+
if (typeof isActive !== 'boolean') {
|
|
72
|
+
return res.status(400).json({ error: 'isActive must be a boolean' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const success = apiKeysDb.toggleApiKey(req.user.id, parseInt(keyId), isActive);
|
|
76
|
+
|
|
77
|
+
if (success) {
|
|
78
|
+
res.json({ success: true });
|
|
79
|
+
} else {
|
|
80
|
+
res.status(404).json({ error: 'API key not found' });
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error toggling API key:', error);
|
|
84
|
+
res.status(500).json({ error: 'Failed to toggle API key' });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ===============================
|
|
89
|
+
// Generic Credentials Management
|
|
90
|
+
// ===============================
|
|
91
|
+
|
|
92
|
+
// Get all credentials for the authenticated user (optionally filtered by type)
|
|
93
|
+
router.get('/credentials', async (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const { type } = req.query;
|
|
96
|
+
const credentials = credentialsDb.getCredentials(req.user.id, type || null);
|
|
97
|
+
// Don't send the actual credential values for security
|
|
98
|
+
res.json({ credentials });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Error fetching credentials:', error);
|
|
101
|
+
res.status(500).json({ error: 'Failed to fetch credentials' });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Create a new credential
|
|
106
|
+
router.post('/credentials', async (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
const { credentialName, credentialType, credentialValue, description } = req.body;
|
|
109
|
+
|
|
110
|
+
if (!credentialName || !credentialName.trim()) {
|
|
111
|
+
return res.status(400).json({ error: 'Credential name is required' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!credentialType || !credentialType.trim()) {
|
|
115
|
+
return res.status(400).json({ error: 'Credential type is required' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!credentialValue || !credentialValue.trim()) {
|
|
119
|
+
return res.status(400).json({ error: 'Credential value is required' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = credentialsDb.createCredential(
|
|
123
|
+
req.user.id,
|
|
124
|
+
credentialName.trim(),
|
|
125
|
+
credentialType.trim(),
|
|
126
|
+
credentialValue.trim(),
|
|
127
|
+
description?.trim() || null
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
res.json({
|
|
131
|
+
success: true,
|
|
132
|
+
credential: result
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('Error creating credential:', error);
|
|
136
|
+
res.status(500).json({ error: 'Failed to create credential' });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Delete a credential
|
|
141
|
+
router.delete('/credentials/:credentialId', async (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const { credentialId } = req.params;
|
|
144
|
+
const success = credentialsDb.deleteCredential(req.user.id, parseInt(credentialId));
|
|
145
|
+
|
|
146
|
+
if (success) {
|
|
147
|
+
res.json({ success: true });
|
|
148
|
+
} else {
|
|
149
|
+
res.status(404).json({ error: 'Credential not found' });
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Error deleting credential:', error);
|
|
153
|
+
res.status(500).json({ error: 'Failed to delete credential' });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Toggle credential active status
|
|
158
|
+
router.patch('/credentials/:credentialId/toggle', async (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
const { credentialId } = req.params;
|
|
161
|
+
const { isActive } = req.body;
|
|
162
|
+
|
|
163
|
+
if (typeof isActive !== 'boolean') {
|
|
164
|
+
return res.status(400).json({ error: 'isActive must be a boolean' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const success = credentialsDb.toggleCredential(req.user.id, parseInt(credentialId), isActive);
|
|
168
|
+
|
|
169
|
+
if (success) {
|
|
170
|
+
res.json({ success: true });
|
|
171
|
+
} else {
|
|
172
|
+
res.status(404).json({ error: 'Credential not found' });
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error('Error toggling credential:', error);
|
|
176
|
+
res.status(500).json({ error: 'Failed to toggle credential' });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
router.get('/auto-research-email', async (req, res) => {
|
|
181
|
+
try {
|
|
182
|
+
res.json({
|
|
183
|
+
senderEmail: appSettingsDb.get(AUTO_RESEARCH_SENDER_EMAIL_KEY),
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('Error fetching Auto Research sender email:', error);
|
|
187
|
+
res.status(500).json({ error: 'Failed to fetch Auto Research sender email' });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
router.put('/auto-research-email', async (req, res) => {
|
|
192
|
+
try {
|
|
193
|
+
const rawEmail = typeof req.body?.senderEmail === 'string' ? req.body.senderEmail.trim().toLowerCase() : '';
|
|
194
|
+
if (!rawEmail) {
|
|
195
|
+
return res.status(400).json({ error: 'Sender email is required' });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
199
|
+
if (!emailRegex.test(rawEmail)) {
|
|
200
|
+
return res.status(400).json({ error: 'Invalid email format' });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
appSettingsDb.set(AUTO_RESEARCH_SENDER_EMAIL_KEY, rawEmail);
|
|
204
|
+
res.json({ success: true, senderEmail: rawEmail });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('Error saving Auto Research sender email:', error);
|
|
207
|
+
res.status(500).json({ error: 'Failed to save Auto Research sender email' });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
router.get('/auto-research-resend-key', async (req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
const apiKey = appSettingsDb.get(AUTO_RESEARCH_RESEND_API_KEY);
|
|
214
|
+
res.json({
|
|
215
|
+
configured: Boolean(apiKey),
|
|
216
|
+
maskedKey: apiKey ? `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}` : null,
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error('Error fetching Auto Research Resend key:', error);
|
|
220
|
+
res.status(500).json({ error: 'Failed to fetch Auto Research Resend key' });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
router.put('/auto-research-resend-key', async (req, res) => {
|
|
225
|
+
try {
|
|
226
|
+
const rawKey = typeof req.body?.apiKey === 'string' ? req.body.apiKey.trim() : '';
|
|
227
|
+
if (!rawKey) {
|
|
228
|
+
return res.status(400).json({ error: 'Resend API key is required' });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
appSettingsDb.set(AUTO_RESEARCH_RESEND_API_KEY, rawKey);
|
|
232
|
+
res.json({
|
|
233
|
+
success: true,
|
|
234
|
+
configured: true,
|
|
235
|
+
maskedKey: `${rawKey.slice(0, 6)}...${rawKey.slice(-4)}`,
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Error saving Auto Research Resend key:', error);
|
|
239
|
+
res.status(500).json({ error: 'Failed to save Auto Research Resend key' });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ===============================
|
|
244
|
+
// OpenRouter Models (cached proxy)
|
|
245
|
+
// ===============================
|
|
246
|
+
|
|
247
|
+
let openrouterModelsCache = { data: null, fetchedAt: 0 };
|
|
248
|
+
const OPENROUTER_CACHE_TTL = 1000 * 60 * 30; // 30 minutes
|
|
249
|
+
|
|
250
|
+
router.get('/openrouter-models', async (_req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
if (openrouterModelsCache.data && now - openrouterModelsCache.fetchedAt < OPENROUTER_CACHE_TTL) {
|
|
254
|
+
return res.json(openrouterModelsCache.data);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const response = await fetch(
|
|
258
|
+
'https://openrouter.ai/api/v1/models?output_modalities=text&supported_parameters=tools',
|
|
259
|
+
{ headers: { 'HTTP-Referer': 'https://github.com/OpenLAIR/dr-claw', 'X-Title': 'Dr. Claw' } }
|
|
260
|
+
);
|
|
261
|
+
if (!response.ok) throw new Error(`OpenRouter API returned ${response.status}`);
|
|
262
|
+
|
|
263
|
+
const json = await response.json();
|
|
264
|
+
const models = (json.data || [])
|
|
265
|
+
.filter((m) => m.id && m.name)
|
|
266
|
+
.map((m) => ({
|
|
267
|
+
value: m.id,
|
|
268
|
+
label: m.name,
|
|
269
|
+
contextLength: m.context_length || null,
|
|
270
|
+
pricing: m.pricing || null,
|
|
271
|
+
}))
|
|
272
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
273
|
+
|
|
274
|
+
openrouterModelsCache = { data: { models }, fetchedAt: now };
|
|
275
|
+
res.json({ models });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error('Error fetching OpenRouter models:', error);
|
|
278
|
+
if (openrouterModelsCache.data) {
|
|
279
|
+
return res.json(openrouterModelsCache.data);
|
|
280
|
+
}
|
|
281
|
+
res.status(502).json({ error: 'Failed to fetch OpenRouter models' });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
export default router;
|