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,439 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import pty from 'node-pty';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), '.openclaw');
|
|
9
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'compute-node.json');
|
|
10
|
+
|
|
11
|
+
// ─── ID generation ───
|
|
12
|
+
|
|
13
|
+
function generateId(hint) {
|
|
14
|
+
const base = hint.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase();
|
|
15
|
+
const short = base.slice(0, 20) || 'node';
|
|
16
|
+
return `${short}-${crypto.randomBytes(3).toString('hex')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Config storage (multi-node) ───
|
|
20
|
+
|
|
21
|
+
async function loadRawConfig() {
|
|
22
|
+
try {
|
|
23
|
+
const data = await fs.readFile(CONFIG_FILE, 'utf8');
|
|
24
|
+
return JSON.parse(data);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return { nodes: [], activeNodeId: null };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function migrateIfNeeded(data) {
|
|
31
|
+
if (data.host && !data.nodes) {
|
|
32
|
+
// Old single-node format → migrate
|
|
33
|
+
const id = generateId(data.host);
|
|
34
|
+
const node = {
|
|
35
|
+
id,
|
|
36
|
+
name: data.host,
|
|
37
|
+
host: data.host,
|
|
38
|
+
user: data.user,
|
|
39
|
+
workDir: data.workDir || '~',
|
|
40
|
+
type: 'direct',
|
|
41
|
+
};
|
|
42
|
+
if (data.keyPath) node.keyPath = data.keyPath;
|
|
43
|
+
if (data.password) node.password = data.password;
|
|
44
|
+
const migrated = { nodes: [node], activeNodeId: id };
|
|
45
|
+
await saveRawConfig(migrated);
|
|
46
|
+
return migrated;
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function saveRawConfig(config) {
|
|
52
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
53
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Public config API ───
|
|
57
|
+
|
|
58
|
+
export async function loadAllNodes() {
|
|
59
|
+
const raw = await loadRawConfig();
|
|
60
|
+
const config = await migrateIfNeeded(raw);
|
|
61
|
+
return config;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function loadNodeConfig(nodeId) {
|
|
65
|
+
const config = await loadAllNodes();
|
|
66
|
+
const node = config.nodes.find(n => n.id === nodeId);
|
|
67
|
+
if (!node) throw new Error(`Node "${nodeId}" not found`);
|
|
68
|
+
return node;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getActiveNode() {
|
|
72
|
+
const config = await loadAllNodes();
|
|
73
|
+
if (!config.activeNodeId || config.nodes.length === 0) return null;
|
|
74
|
+
return config.nodes.find(n => n.id === config.activeNodeId) || config.nodes[0] || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function saveNode(nodeConfig) {
|
|
78
|
+
const config = await loadAllNodes();
|
|
79
|
+
const idx = config.nodes.findIndex(n => n.id === nodeConfig.id);
|
|
80
|
+
if (idx >= 0) {
|
|
81
|
+
config.nodes[idx] = nodeConfig;
|
|
82
|
+
} else {
|
|
83
|
+
config.nodes.push(nodeConfig);
|
|
84
|
+
}
|
|
85
|
+
if (!config.activeNodeId || config.nodes.length === 1) {
|
|
86
|
+
config.activeNodeId = nodeConfig.id;
|
|
87
|
+
}
|
|
88
|
+
await saveRawConfig(config);
|
|
89
|
+
return nodeConfig;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function deleteNode(nodeId) {
|
|
93
|
+
const config = await loadAllNodes();
|
|
94
|
+
config.nodes = config.nodes.filter(n => n.id !== nodeId);
|
|
95
|
+
if (config.activeNodeId === nodeId) {
|
|
96
|
+
config.activeNodeId = config.nodes.length > 0 ? config.nodes[0].id : null;
|
|
97
|
+
}
|
|
98
|
+
await saveRawConfig(config);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function setActiveNode(nodeId) {
|
|
102
|
+
const config = await loadAllNodes();
|
|
103
|
+
const node = config.nodes.find(n => n.id === nodeId);
|
|
104
|
+
if (!node) throw new Error(`Node "${nodeId}" not found`);
|
|
105
|
+
config.activeNodeId = nodeId;
|
|
106
|
+
await saveRawConfig(config);
|
|
107
|
+
return node;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Backward-compatible: returns active node config (flat object)
|
|
111
|
+
export async function loadConfig() {
|
|
112
|
+
return await getActiveNode() || {};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function isComputeConfigured() {
|
|
116
|
+
try {
|
|
117
|
+
const node = await getActiveNode();
|
|
118
|
+
return !!(node && node.host && node.user && (node.keyPath || node.password));
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Shell execution helpers ───
|
|
125
|
+
|
|
126
|
+
function execLocal(command, args, options = {}) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const proc = spawn(command, args, { ...options, shell: true });
|
|
129
|
+
let stdout = '';
|
|
130
|
+
let stderr = '';
|
|
131
|
+
|
|
132
|
+
proc.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
133
|
+
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
134
|
+
|
|
135
|
+
proc.on('close', (code) => {
|
|
136
|
+
if (code === 0) resolve(stdout.trim());
|
|
137
|
+
else reject(new Error(`Command failed (code ${code}): ${stderr || stdout}`));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function execWithPassword(command, password, timeoutMs = 60000) {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
let output = '';
|
|
145
|
+
let passwordSent = false;
|
|
146
|
+
let finished = false;
|
|
147
|
+
|
|
148
|
+
const proc = pty.spawn('bash', ['-c', command], {
|
|
149
|
+
name: 'xterm',
|
|
150
|
+
cols: 200,
|
|
151
|
+
rows: 50,
|
|
152
|
+
cwd: os.homedir(),
|
|
153
|
+
env: { ...process.env, TERM: 'xterm' }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
if (!finished) {
|
|
158
|
+
finished = true;
|
|
159
|
+
proc.kill();
|
|
160
|
+
reject(new Error('Command timed out'));
|
|
161
|
+
}
|
|
162
|
+
}, timeoutMs);
|
|
163
|
+
|
|
164
|
+
proc.onData((data) => {
|
|
165
|
+
const text = data.toString();
|
|
166
|
+
output += text;
|
|
167
|
+
|
|
168
|
+
if (!passwordSent && /[Pp]assword[:\s]*$/.test(output.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''))) {
|
|
169
|
+
passwordSent = true;
|
|
170
|
+
proc.write(password + '\n');
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
proc.onExit(({ exitCode }) => {
|
|
175
|
+
if (finished) return;
|
|
176
|
+
finished = true;
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
|
|
179
|
+
let cleanOutput = output
|
|
180
|
+
.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
|
|
181
|
+
.replace(/\r\n/g, '\n')
|
|
182
|
+
.replace(/\r/g, '\n');
|
|
183
|
+
|
|
184
|
+
const lines = cleanOutput.split('\n');
|
|
185
|
+
const filtered = lines.filter(line => {
|
|
186
|
+
const trimmed = line.trim();
|
|
187
|
+
return trimmed &&
|
|
188
|
+
!/^[Pp]assword[:\s]*$/.test(trimmed) &&
|
|
189
|
+
trimmed !== password;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const resultLines = filtered.slice(1);
|
|
193
|
+
const result = resultLines.join('\n').trim();
|
|
194
|
+
|
|
195
|
+
if (exitCode === 0) {
|
|
196
|
+
resolve(result);
|
|
197
|
+
} else {
|
|
198
|
+
reject(new Error(`Command failed (code ${exitCode}): ${result}`));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Execute SSH command on a specific node
|
|
205
|
+
async function execSsh(nodeConfig, remoteCmd) {
|
|
206
|
+
const port = nodeConfig.port || 22;
|
|
207
|
+
const sshBase = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 -p ${port}`;
|
|
208
|
+
|
|
209
|
+
if (nodeConfig.keyPath) {
|
|
210
|
+
const cmd = `${sshBase} -i ${nodeConfig.keyPath} ${nodeConfig.user}@${nodeConfig.host} ${JSON.stringify(remoteCmd)}`;
|
|
211
|
+
return await execLocal(cmd);
|
|
212
|
+
} else if (nodeConfig.password) {
|
|
213
|
+
const cmd = `${sshBase} ${nodeConfig.user}@${nodeConfig.host} ${JSON.stringify(remoteCmd)}`;
|
|
214
|
+
return await execWithPassword(cmd, nodeConfig.password);
|
|
215
|
+
} else {
|
|
216
|
+
throw new Error('No authentication method configured (need SSH key or password)');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Execute rsync on a specific node
|
|
221
|
+
async function execRsync(nodeConfig, src, dst, excludes = '') {
|
|
222
|
+
const port = nodeConfig.port || 22;
|
|
223
|
+
const sshCmd = nodeConfig.keyPath
|
|
224
|
+
? `ssh -o StrictHostKeyChecking=no -p ${port} -i ${nodeConfig.keyPath}`
|
|
225
|
+
: `ssh -o StrictHostKeyChecking=no -p ${port}`;
|
|
226
|
+
|
|
227
|
+
const cmd = `rsync -avz ${excludes} -e "${sshCmd}" ${src} ${dst}`;
|
|
228
|
+
|
|
229
|
+
if (nodeConfig.keyPath) {
|
|
230
|
+
return await execLocal(cmd);
|
|
231
|
+
} else if (nodeConfig.password) {
|
|
232
|
+
return await execWithPassword(cmd, nodeConfig.password, 120000);
|
|
233
|
+
} else {
|
|
234
|
+
throw new Error('No authentication method configured');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getProjectName(cwd) {
|
|
239
|
+
return path.basename(cwd);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── Helper: resolve node config from optional nodeId ───
|
|
243
|
+
|
|
244
|
+
async function resolveNode(nodeId) {
|
|
245
|
+
if (nodeId) {
|
|
246
|
+
return await loadNodeConfig(nodeId);
|
|
247
|
+
}
|
|
248
|
+
const active = await getActiveNode();
|
|
249
|
+
if (!active) throw new Error('No compute node configured. Please add a node first.');
|
|
250
|
+
return active;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Main ComputeNode API ───
|
|
254
|
+
|
|
255
|
+
export const ComputeNode = {
|
|
256
|
+
// Configure / save a node
|
|
257
|
+
async configure({ id, name, host, user, key, password, workDir = '~', type = 'direct', slurm, port }) {
|
|
258
|
+
const nodeId = id || generateId(host);
|
|
259
|
+
const node = {
|
|
260
|
+
id: nodeId,
|
|
261
|
+
name: name || host,
|
|
262
|
+
host,
|
|
263
|
+
user,
|
|
264
|
+
port: port || 22,
|
|
265
|
+
workDir,
|
|
266
|
+
type,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (key) {
|
|
270
|
+
if (key.includes('BEGIN')) {
|
|
271
|
+
const keyPath = path.join(os.homedir(), '.ssh', `compute_${nodeId}_key`);
|
|
272
|
+
await fs.mkdir(path.dirname(keyPath), { recursive: true });
|
|
273
|
+
await fs.writeFile(keyPath, key + '\n', { mode: 0o600 });
|
|
274
|
+
node.keyPath = keyPath;
|
|
275
|
+
} else {
|
|
276
|
+
node.keyPath = key;
|
|
277
|
+
}
|
|
278
|
+
} else if (password) {
|
|
279
|
+
node.password = password;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (type === 'slurm' && slurm) {
|
|
283
|
+
node.slurm = slurm;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await saveNode(node);
|
|
287
|
+
return `Configuration saved for ${node.user}@${node.host} (${nodeId})`;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// Sync code up/down
|
|
291
|
+
async sync({ nodeId, direction = 'up', files = [], cwd }) {
|
|
292
|
+
const config = await resolveNode(nodeId);
|
|
293
|
+
|
|
294
|
+
const projectName = getProjectName(cwd);
|
|
295
|
+
const remoteBase = config.workDir.endsWith('/') ? config.workDir : config.workDir + '/';
|
|
296
|
+
const remotePath = `${remoteBase}${projectName}/`;
|
|
297
|
+
|
|
298
|
+
if (direction === 'up') {
|
|
299
|
+
await execSsh(config, `mkdir -p ${remotePath}`);
|
|
300
|
+
const excludes = "--exclude '.git' --exclude 'node_modules' --exclude '__pycache__' --exclude '*.pyc' --exclude '.DS_Store'";
|
|
301
|
+
return await execRsync(config, `${cwd}/`, `${config.user}@${config.host}:${remotePath}`, excludes);
|
|
302
|
+
} else {
|
|
303
|
+
const filesToSync = files.length > 0 ? files.join(' ') : 'logs/ checkpoints/ results/';
|
|
304
|
+
return await execRsync(config, `${config.user}@${config.host}:${remotePath}{${filesToSync}}`, `${cwd}/`);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// Run a command on a node
|
|
309
|
+
async run({ nodeId, command, cwd, skipSync = false }) {
|
|
310
|
+
const config = await resolveNode(nodeId);
|
|
311
|
+
|
|
312
|
+
if (cwd && !skipSync) {
|
|
313
|
+
const projectName = getProjectName(cwd);
|
|
314
|
+
const remoteBase = config.workDir.endsWith('/') ? config.workDir : config.workDir + '/';
|
|
315
|
+
const remotePath = `${remoteBase}${projectName}/`;
|
|
316
|
+
|
|
317
|
+
await this.sync({ nodeId: config.id, direction: 'up', cwd });
|
|
318
|
+
return await execSsh(config, `cd ${remotePath} && ${command}`);
|
|
319
|
+
} else {
|
|
320
|
+
return await execSsh(config, command);
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// ─── Slurm-specific methods ───
|
|
325
|
+
|
|
326
|
+
// Get partition info
|
|
327
|
+
async sinfo({ nodeId }) {
|
|
328
|
+
const config = await resolveNode(nodeId);
|
|
329
|
+
if (config.type !== 'slurm') throw new Error('Node is not a Slurm cluster');
|
|
330
|
+
const output = await execSsh(config, 'sinfo --format="%P %a %l %D %G" --noheader');
|
|
331
|
+
// Parse into structured data
|
|
332
|
+
const partitions = output.split('\n').filter(Boolean).map(line => {
|
|
333
|
+
const parts = line.trim().split(/\s+/);
|
|
334
|
+
const name = parts[0]?.replace('*', '') || '';
|
|
335
|
+
const isDefault = parts[0]?.endsWith('*') || false;
|
|
336
|
+
return {
|
|
337
|
+
name,
|
|
338
|
+
isDefault,
|
|
339
|
+
available: parts[1] || '',
|
|
340
|
+
timeLimit: parts[2] || '',
|
|
341
|
+
nodes: parts[3] || '',
|
|
342
|
+
gres: parts[4] || '',
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
return partitions;
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
// Get job queue
|
|
349
|
+
async squeue({ nodeId }) {
|
|
350
|
+
const config = await resolveNode(nodeId);
|
|
351
|
+
if (config.type !== 'slurm') throw new Error('Node is not a Slurm cluster');
|
|
352
|
+
const output = await execSsh(config, `squeue -u ${config.user} --format="%i %j %P %T %M %l %D %R" --noheader`);
|
|
353
|
+
if (!output.trim()) return [];
|
|
354
|
+
const jobs = output.split('\n').filter(Boolean).map(line => {
|
|
355
|
+
const parts = line.trim().split(/\s+/);
|
|
356
|
+
return {
|
|
357
|
+
jobId: parts[0] || '',
|
|
358
|
+
name: parts[1] || '',
|
|
359
|
+
partition: parts[2] || '',
|
|
360
|
+
state: parts[3] || '',
|
|
361
|
+
elapsed: parts[4] || '',
|
|
362
|
+
timeLimit: parts[5] || '',
|
|
363
|
+
nodes: parts[6] || '',
|
|
364
|
+
reason: parts.slice(7).join(' ') || '',
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
return jobs;
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
// Interactive GPU allocation (salloc + srun)
|
|
371
|
+
async salloc({ nodeId, partition, time, gpus, account, command }) {
|
|
372
|
+
const config = await resolveNode(nodeId);
|
|
373
|
+
if (config.type !== 'slurm') throw new Error('Node is not a Slurm cluster');
|
|
374
|
+
|
|
375
|
+
const defaults = config.slurm || {};
|
|
376
|
+
const p = partition || defaults.defaultPartition;
|
|
377
|
+
const t = time || defaults.defaultTime || '00:30:00';
|
|
378
|
+
const g = gpus ?? defaults.defaultGpus ?? 1;
|
|
379
|
+
const a = account || defaults.defaultAccount;
|
|
380
|
+
|
|
381
|
+
let sallocCmd = 'salloc';
|
|
382
|
+
if (p) sallocCmd += ` --partition=${p}`;
|
|
383
|
+
sallocCmd += ` --time=${t}`;
|
|
384
|
+
sallocCmd += ` --gres=gpu:${g}`;
|
|
385
|
+
if (a) sallocCmd += ` -A ${a}`;
|
|
386
|
+
|
|
387
|
+
if (command) {
|
|
388
|
+
sallocCmd += ` srun ${command}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return await execSsh(config, sallocCmd);
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// Submit batch job
|
|
395
|
+
async sbatch({ nodeId, rawScript, script, partition, time, gpus, account, jobName }) {
|
|
396
|
+
const config = await resolveNode(nodeId);
|
|
397
|
+
if (config.type !== 'slurm') throw new Error('Node is not a Slurm cluster');
|
|
398
|
+
|
|
399
|
+
let sbatchScript;
|
|
400
|
+
|
|
401
|
+
if (rawScript) {
|
|
402
|
+
// User provided the full script with #SBATCH directives — use as-is
|
|
403
|
+
sbatchScript = rawScript;
|
|
404
|
+
} else {
|
|
405
|
+
// Auto-generate headers + append user script body
|
|
406
|
+
const defaults = config.slurm || {};
|
|
407
|
+
const p = partition || defaults.defaultPartition;
|
|
408
|
+
const t = time || defaults.defaultTime || '02:00:00';
|
|
409
|
+
const g = gpus ?? defaults.defaultGpus ?? 1;
|
|
410
|
+
const a = account || defaults.defaultAccount;
|
|
411
|
+
const name = jobName || 'dr-claw-job';
|
|
412
|
+
|
|
413
|
+
sbatchScript = '#!/bin/bash\n';
|
|
414
|
+
sbatchScript += `#SBATCH --job-name=${name}\n`;
|
|
415
|
+
if (p) sbatchScript += `#SBATCH --partition=${p}\n`;
|
|
416
|
+
sbatchScript += `#SBATCH --time=${t}\n`;
|
|
417
|
+
sbatchScript += `#SBATCH --gres=gpu:${g}\n`;
|
|
418
|
+
if (a) sbatchScript += `#SBATCH -A ${a}\n`;
|
|
419
|
+
sbatchScript += `#SBATCH --output=${name}-%j.out\n`;
|
|
420
|
+
sbatchScript += `#SBATCH --error=${name}-%j.err\n`;
|
|
421
|
+
sbatchScript += '\n';
|
|
422
|
+
sbatchScript += script;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Write script to remote via base64 to preserve newlines and special chars
|
|
426
|
+
const workDir = config.workDir || '~';
|
|
427
|
+
const scriptPath = `${workDir}/.dr-claw-sbatch-${Date.now()}.sh`;
|
|
428
|
+
const b64 = Buffer.from(sbatchScript).toString('base64');
|
|
429
|
+
const remoteCmd = `echo '${b64}' | base64 -d > ${scriptPath} && chmod +x ${scriptPath} && sbatch ${scriptPath} && rm -f ${scriptPath}`;
|
|
430
|
+
return await execSsh(config, remoteCmd);
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
// Cancel a job
|
|
434
|
+
async scancel({ nodeId, jobId }) {
|
|
435
|
+
const config = await resolveNode(nodeId);
|
|
436
|
+
if (config.type !== 'slurm') throw new Error('Node is not a Slurm cluster');
|
|
437
|
+
return await execSsh(config, `scancel ${jobId}`);
|
|
438
|
+
},
|
|
439
|
+
};
|