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,914 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { extractProjectDirectory, ensureProjectSkillLinks } from '../projects.js';
|
|
7
|
+
import { FORBIDDEN_PATHS } from './projects.js';
|
|
8
|
+
|
|
9
|
+
const GLOBAL_SKILLS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills');
|
|
10
|
+
|
|
11
|
+
const router = express.Router();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sync a skill directory + tag config to the global skills/ folder.
|
|
15
|
+
* Failures are logged but never throw — local upload must always succeed.
|
|
16
|
+
*/
|
|
17
|
+
async function syncSkillToGlobal(name, srcDir, tags) {
|
|
18
|
+
try {
|
|
19
|
+
await fs.mkdir(GLOBAL_SKILLS_DIR, { recursive: true });
|
|
20
|
+
const destDir = path.join(GLOBAL_SKILLS_DIR, name);
|
|
21
|
+
// Remove stale copy then copy fresh
|
|
22
|
+
await fs.rm(destDir, { recursive: true, force: true });
|
|
23
|
+
await fs.cp(srcDir, destDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Update global skill-tag-mapping.json
|
|
26
|
+
const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
|
|
27
|
+
let tagMapping = { version: 1, stageOverrides: {}, domainOverrides: {}, platformNativeSkills: [], domainCsAiExceptions: [] };
|
|
28
|
+
try {
|
|
29
|
+
tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
|
|
30
|
+
} catch { /* use defaults */ }
|
|
31
|
+
|
|
32
|
+
if (tags && tags.stageOverride) {
|
|
33
|
+
tagMapping.stageOverrides = tagMapping.stageOverrides || {};
|
|
34
|
+
tagMapping.stageOverrides[name] = tags.stageOverride;
|
|
35
|
+
}
|
|
36
|
+
if (tags && tags.domainOverride) {
|
|
37
|
+
tagMapping.domainOverrides = tagMapping.domainOverrides || {};
|
|
38
|
+
tagMapping.domainOverrides[name] = tags.domainOverride;
|
|
39
|
+
}
|
|
40
|
+
await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
|
|
41
|
+
|
|
42
|
+
// Update global stage-skill-map.json
|
|
43
|
+
const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
|
|
44
|
+
let stageSkillMap = { skillOrigins: {} };
|
|
45
|
+
try {
|
|
46
|
+
stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
|
|
47
|
+
} catch { /* use defaults */ }
|
|
48
|
+
|
|
49
|
+
stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
|
|
50
|
+
stageSkillMap.skillOrigins[name] = (tags && tags.origin) || 'downloaded';
|
|
51
|
+
await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn('[skills] syncSkillToGlobal failed (non-fatal):', err.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Remove a skill directory + clean its entries from global config files.
|
|
59
|
+
*/
|
|
60
|
+
async function removeSkillFromGlobal(name) {
|
|
61
|
+
try {
|
|
62
|
+
const destDir = path.join(GLOBAL_SKILLS_DIR, name);
|
|
63
|
+
await fs.rm(destDir, { recursive: true, force: true });
|
|
64
|
+
|
|
65
|
+
// Clean global skill-tag-mapping.json
|
|
66
|
+
const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
|
|
67
|
+
try {
|
|
68
|
+
const tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
|
|
69
|
+
if (tagMapping.stageOverrides) delete tagMapping.stageOverrides[name];
|
|
70
|
+
if (tagMapping.domainOverrides) delete tagMapping.domainOverrides[name];
|
|
71
|
+
await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
|
|
72
|
+
} catch { /* file missing or unparseable — nothing to clean */ }
|
|
73
|
+
|
|
74
|
+
// Clean global stage-skill-map.json
|
|
75
|
+
const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
|
|
76
|
+
try {
|
|
77
|
+
const stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
|
|
78
|
+
if (stageSkillMap.skillOrigins) delete stageSkillMap.skillOrigins[name];
|
|
79
|
+
await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
|
|
80
|
+
} catch { /* file missing or unparseable — nothing to clean */ }
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.warn('[skills] removeSkillFromGlobal failed (non-fatal):', err.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Update tag overrides for a skill in the global config.
|
|
88
|
+
*/
|
|
89
|
+
async function updateGlobalTags(name, tags) {
|
|
90
|
+
try {
|
|
91
|
+
const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
|
|
92
|
+
let tagMapping = { version: 1, stageOverrides: {}, domainOverrides: {}, platformNativeSkills: [], domainCsAiExceptions: [] };
|
|
93
|
+
try {
|
|
94
|
+
tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
|
|
95
|
+
} catch { /* use defaults */ }
|
|
96
|
+
|
|
97
|
+
const { stageOverride, domainOverride } = tags;
|
|
98
|
+
|
|
99
|
+
if (stageOverride !== undefined) {
|
|
100
|
+
tagMapping.stageOverrides = tagMapping.stageOverrides || {};
|
|
101
|
+
if (stageOverride === null) {
|
|
102
|
+
delete tagMapping.stageOverrides[name];
|
|
103
|
+
} else {
|
|
104
|
+
tagMapping.stageOverrides[name] = stageOverride;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (domainOverride !== undefined) {
|
|
109
|
+
tagMapping.domainOverrides = tagMapping.domainOverrides || {};
|
|
110
|
+
if (domainOverride === null) {
|
|
111
|
+
delete tagMapping.domainOverrides[name];
|
|
112
|
+
} else {
|
|
113
|
+
tagMapping.domainOverrides[name] = domainOverride;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
|
|
118
|
+
|
|
119
|
+
// Also sync origin if provided
|
|
120
|
+
if (tags.origin !== undefined) {
|
|
121
|
+
const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
|
|
122
|
+
let stageSkillMap = { skillOrigins: {} };
|
|
123
|
+
try {
|
|
124
|
+
stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
|
|
125
|
+
} catch { /* use defaults */ }
|
|
126
|
+
|
|
127
|
+
stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
|
|
128
|
+
if (tags.origin === null) {
|
|
129
|
+
delete stageSkillMap.skillOrigins[name];
|
|
130
|
+
} else {
|
|
131
|
+
stageSkillMap.skillOrigins[name] = tags.origin;
|
|
132
|
+
}
|
|
133
|
+
await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn('[skills] updateGlobalTags failed (non-fatal):', err.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Locate SKILL.md inside a zip (root or one level deep).
|
|
142
|
+
* Returns { entry, prefix } where prefix is '' or 'dirname/'.
|
|
143
|
+
*/
|
|
144
|
+
function findSkillMd(zip) {
|
|
145
|
+
// Check root
|
|
146
|
+
const rootEntry = zip.getEntry('SKILL.md');
|
|
147
|
+
if (rootEntry) return { entry: rootEntry, prefix: '' };
|
|
148
|
+
|
|
149
|
+
// Check one level deep
|
|
150
|
+
for (const entry of zip.getEntries()) {
|
|
151
|
+
const parts = entry.entryName.split('/').filter(Boolean);
|
|
152
|
+
if (parts.length === 2 && parts[1] === 'SKILL.md' && entry.isDirectory === false) {
|
|
153
|
+
return { entry, prefix: parts[0] + '/' };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse YAML frontmatter from SKILL.md content using gray-matter.
|
|
161
|
+
*/
|
|
162
|
+
async function parseFrontmatter(content) {
|
|
163
|
+
const matter = (await import('gray-matter')).default;
|
|
164
|
+
const { data, content: body } = matter(content);
|
|
165
|
+
return { data, body };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Sanitize skill name for use as directory name.
|
|
170
|
+
*/
|
|
171
|
+
function safeDirName(name) {
|
|
172
|
+
return name
|
|
173
|
+
.toLowerCase()
|
|
174
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
175
|
+
.replace(/^-+|-+$/g, '')
|
|
176
|
+
.slice(0, 80);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validate that a zip buffer contains a valid skill package.
|
|
181
|
+
*/
|
|
182
|
+
async function validateZipBuffer(buffer) {
|
|
183
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
184
|
+
const zip = new AdmZip(buffer);
|
|
185
|
+
|
|
186
|
+
const result = findSkillMd(zip);
|
|
187
|
+
if (!result) {
|
|
188
|
+
return { valid: false, error: 'No SKILL.md found at root or one directory deep.' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const content = result.entry.getData().toString('utf8');
|
|
192
|
+
const { data: frontmatter, body } = await parseFrontmatter(content);
|
|
193
|
+
|
|
194
|
+
if (!frontmatter || !frontmatter.name) {
|
|
195
|
+
return { valid: false, error: 'SKILL.md must contain YAML frontmatter with a "name" field.' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const entries = zip.getEntries();
|
|
199
|
+
const prefix = result.prefix;
|
|
200
|
+
const relevantEntries = prefix
|
|
201
|
+
? entries.filter(e => e.entryName.startsWith(prefix))
|
|
202
|
+
: entries;
|
|
203
|
+
const fileCount = relevantEntries.filter(e => !e.isDirectory).length;
|
|
204
|
+
const hasPrompts = relevantEntries.some(e => {
|
|
205
|
+
const rel = prefix ? e.entryName.slice(prefix.length) : e.entryName;
|
|
206
|
+
return rel.startsWith('prompts/') || rel.startsWith('prompt/');
|
|
207
|
+
});
|
|
208
|
+
const hasReferences = relevantEntries.some(e => {
|
|
209
|
+
const rel = prefix ? e.entryName.slice(prefix.length) : e.entryName;
|
|
210
|
+
return rel.startsWith('references/') || rel.startsWith('reference/');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
valid: true,
|
|
215
|
+
skillName: frontmatter.name,
|
|
216
|
+
frontmatter,
|
|
217
|
+
description: body.trim().split('\n')[0] || '',
|
|
218
|
+
fileCount,
|
|
219
|
+
hasPrompts,
|
|
220
|
+
hasReferences,
|
|
221
|
+
prefix,
|
|
222
|
+
zip,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// GET / — return the file tree of the global skills/ directory
|
|
227
|
+
router.get('/', async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
await fs.access(GLOBAL_SKILLS_DIR);
|
|
230
|
+
|
|
231
|
+
const { getFileTree } = await import('../file-tree.js').catch(() => ({}));
|
|
232
|
+
// Prefer shared helper when available, but fall back if it throws.
|
|
233
|
+
if (typeof getFileTree === 'function') {
|
|
234
|
+
try {
|
|
235
|
+
const tree = await getFileTree(GLOBAL_SKILLS_DIR, 5, 0, false);
|
|
236
|
+
return res.json(tree);
|
|
237
|
+
} catch (helperError) {
|
|
238
|
+
console.warn('[skills] getFileTree failed, falling back to local walker:', helperError.message);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Manual recursive tree building (mirrors getFileTree from index.js)
|
|
242
|
+
async function buildTree(dirPath, maxDepth, depth) {
|
|
243
|
+
const items = [];
|
|
244
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (entry.name.startsWith('.')) continue;
|
|
247
|
+
if (['node_modules', 'dist', 'build', '.git'].includes(entry.name)) continue;
|
|
248
|
+
const itemPath = path.join(dirPath, entry.name);
|
|
249
|
+
let isDir = entry.isDirectory();
|
|
250
|
+
if (!isDir && entry.isSymbolicLink()) {
|
|
251
|
+
try { isDir = (await fs.stat(itemPath)).isDirectory(); } catch { /* ignore */ }
|
|
252
|
+
}
|
|
253
|
+
const item = { name: entry.name, path: itemPath, type: isDir ? 'directory' : 'file' };
|
|
254
|
+
if (isDir && depth < maxDepth) {
|
|
255
|
+
try {
|
|
256
|
+
item.children = await buildTree(itemPath, maxDepth, depth + 1);
|
|
257
|
+
} catch (childError) {
|
|
258
|
+
console.warn('[skills] Skipping unreadable directory:', itemPath, childError.message);
|
|
259
|
+
item.children = [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
items.push(item);
|
|
263
|
+
}
|
|
264
|
+
return items;
|
|
265
|
+
}
|
|
266
|
+
const tree = await buildTree(GLOBAL_SKILLS_DIR, 5, 0);
|
|
267
|
+
res.json(tree);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error.code === 'ENOENT') {
|
|
270
|
+
return res.status(404).json({ error: 'Global skills directory not found' });
|
|
271
|
+
}
|
|
272
|
+
console.error('[ERROR] Skills tree error:', error.message);
|
|
273
|
+
res.status(500).json({ error: error.message });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// GET /file — read a single file from the global skills/ directory
|
|
278
|
+
router.get('/file', async (req, res) => {
|
|
279
|
+
try {
|
|
280
|
+
const { filePath } = req.query;
|
|
281
|
+
if (!filePath) {
|
|
282
|
+
return res.status(400).json({ error: 'filePath is required' });
|
|
283
|
+
}
|
|
284
|
+
const skillsDir = path.resolve(GLOBAL_SKILLS_DIR);
|
|
285
|
+
const resolved = path.resolve(skillsDir, filePath);
|
|
286
|
+
if (!resolved.startsWith(skillsDir + path.sep) && resolved !== skillsDir) {
|
|
287
|
+
return res.status(403).json({ error: 'Path must be under skills root' });
|
|
288
|
+
}
|
|
289
|
+
const content = await fs.readFile(resolved, 'utf8');
|
|
290
|
+
res.json({ content });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code === 'ENOENT') {
|
|
293
|
+
res.status(404).json({ error: 'File not found' });
|
|
294
|
+
} else {
|
|
295
|
+
console.error('[ERROR] Skill file read error:', error.message);
|
|
296
|
+
res.status(500).json({ error: error.message });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// PUT /file — write a single file in the global skills/ directory
|
|
302
|
+
router.put('/file', async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const { filePath, content } = req.body || {};
|
|
305
|
+
if (!filePath || typeof content !== 'string') {
|
|
306
|
+
return res.status(400).json({ error: 'filePath and content are required' });
|
|
307
|
+
}
|
|
308
|
+
const skillsDir = path.resolve(GLOBAL_SKILLS_DIR);
|
|
309
|
+
const resolved = path.resolve(skillsDir, filePath);
|
|
310
|
+
if (!resolved.startsWith(skillsDir + path.sep) && resolved !== skillsDir) {
|
|
311
|
+
return res.status(403).json({ error: 'Path must be under skills root' });
|
|
312
|
+
}
|
|
313
|
+
await fs.writeFile(resolved, content, 'utf8');
|
|
314
|
+
res.json({ success: true });
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error('[ERROR] Skill file write error:', error.message);
|
|
317
|
+
res.status(500).json({ error: error.message });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// GET /scan-local — scan a local directory for skill subdirectories
|
|
322
|
+
router.get('/scan-local', async (req, res) => {
|
|
323
|
+
try {
|
|
324
|
+
const rawPath = req.query.path || '~/.claude/skills';
|
|
325
|
+
const resolvedPath = rawPath.replace(/^~/, os.homedir());
|
|
326
|
+
const absolutePath = path.resolve(resolvedPath);
|
|
327
|
+
|
|
328
|
+
// Security: reject forbidden system paths
|
|
329
|
+
const normalizedPath = path.normalize(absolutePath);
|
|
330
|
+
for (const forbidden of FORBIDDEN_PATHS) {
|
|
331
|
+
if (normalizedPath === forbidden || normalizedPath.startsWith(forbidden + path.sep)) {
|
|
332
|
+
return res.status(403).json({ error: `Scanning system directory "${forbidden}" is not allowed.` });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Validate the path exists and is a directory
|
|
337
|
+
let stat;
|
|
338
|
+
try {
|
|
339
|
+
stat = await fs.stat(absolutePath);
|
|
340
|
+
} catch {
|
|
341
|
+
return res.status(404).json({ error: `Path does not exist: ${rawPath}` });
|
|
342
|
+
}
|
|
343
|
+
if (!stat.isDirectory()) {
|
|
344
|
+
return res.status(400).json({ error: `Path is not a directory: ${rawPath}` });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Scan for subdirectories (1-level deep)
|
|
348
|
+
const entries = await fs.readdir(absolutePath, { withFileTypes: true });
|
|
349
|
+
const skills = [];
|
|
350
|
+
|
|
351
|
+
for (const entry of entries) {
|
|
352
|
+
if (!entry.isDirectory()) continue;
|
|
353
|
+
if (entry.name.startsWith('.')) continue;
|
|
354
|
+
|
|
355
|
+
const skillDir = path.join(absolutePath, entry.name);
|
|
356
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
357
|
+
let hasSkillMd = false;
|
|
358
|
+
try {
|
|
359
|
+
await fs.access(skillMdPath);
|
|
360
|
+
hasSkillMd = true;
|
|
361
|
+
} catch {
|
|
362
|
+
// No SKILL.md
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check if already imported in GLOBAL_SKILLS_DIR
|
|
366
|
+
let alreadyImported = false;
|
|
367
|
+
try {
|
|
368
|
+
await fs.access(path.join(GLOBAL_SKILLS_DIR, entry.name));
|
|
369
|
+
alreadyImported = true;
|
|
370
|
+
} catch {
|
|
371
|
+
// Not imported
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
skills.push({
|
|
375
|
+
name: entry.name,
|
|
376
|
+
hasSkillMd,
|
|
377
|
+
alreadyImported,
|
|
378
|
+
sourcePath: skillDir,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
res.json({ path: rawPath, resolvedPath: absolutePath, skills });
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error('[skills] scan-local error:', error);
|
|
385
|
+
res.status(500).json({ error: error.message });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// POST /import-from-local — copy selected skills from a local directory into GLOBAL_SKILLS_DIR
|
|
390
|
+
router.post('/import-from-local', async (req, res) => {
|
|
391
|
+
try {
|
|
392
|
+
const { sourcePath: rawPath, skillNames } = req.body || {};
|
|
393
|
+
const resolvedPath = (rawPath || '~/.claude/skills').replace(/^~/, os.homedir());
|
|
394
|
+
const absolutePath = path.resolve(resolvedPath);
|
|
395
|
+
|
|
396
|
+
// Security: reject forbidden system paths
|
|
397
|
+
const normalizedPath = path.normalize(absolutePath);
|
|
398
|
+
for (const forbidden of FORBIDDEN_PATHS) {
|
|
399
|
+
if (normalizedPath === forbidden || normalizedPath.startsWith(forbidden + path.sep)) {
|
|
400
|
+
return res.status(403).json({ error: `Importing from system directory "${forbidden}" is not allowed.` });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Validate the path exists and is a directory
|
|
405
|
+
let stat;
|
|
406
|
+
try {
|
|
407
|
+
stat = await fs.stat(absolutePath);
|
|
408
|
+
} catch {
|
|
409
|
+
return res.status(404).json({ error: `Path does not exist: ${rawPath || '~/.claude/skills'}` });
|
|
410
|
+
}
|
|
411
|
+
if (!stat.isDirectory()) {
|
|
412
|
+
return res.status(400).json({ error: `Path is not a directory: ${rawPath || '~/.claude/skills'}` });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// If skillNames is provided, only import those; otherwise scan all subdirectories
|
|
416
|
+
let dirsToImport = [];
|
|
417
|
+
if (Array.isArray(skillNames) && skillNames.length > 0) {
|
|
418
|
+
for (const name of skillNames) {
|
|
419
|
+
const skillDir = path.join(absolutePath, name);
|
|
420
|
+
try {
|
|
421
|
+
const s = await fs.stat(skillDir);
|
|
422
|
+
if (s.isDirectory()) {
|
|
423
|
+
dirsToImport.push({ name, sourcePath: skillDir });
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
// Skip missing
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
const entries = await fs.readdir(absolutePath, { withFileTypes: true });
|
|
431
|
+
for (const entry of entries) {
|
|
432
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
433
|
+
dirsToImport.push({ name: entry.name, sourcePath: path.join(absolutePath, entry.name) });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
await fs.mkdir(GLOBAL_SKILLS_DIR, { recursive: true });
|
|
438
|
+
|
|
439
|
+
const imported = [];
|
|
440
|
+
const skipped = [];
|
|
441
|
+
const errors = [];
|
|
442
|
+
|
|
443
|
+
for (const { name, sourcePath: srcDir } of dirsToImport) {
|
|
444
|
+
const destDir = path.join(GLOBAL_SKILLS_DIR, name);
|
|
445
|
+
try {
|
|
446
|
+
// Check if already exists
|
|
447
|
+
try {
|
|
448
|
+
await fs.access(destDir);
|
|
449
|
+
skipped.push(name);
|
|
450
|
+
continue;
|
|
451
|
+
} catch {
|
|
452
|
+
// Does not exist — proceed with copy
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
await fs.cp(srcDir, destDir, { recursive: true });
|
|
456
|
+
|
|
457
|
+
// Update stage-skill-map.json with origin 'local-import'
|
|
458
|
+
const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
|
|
459
|
+
let stageSkillMap = { skillOrigins: {} };
|
|
460
|
+
try {
|
|
461
|
+
stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
|
|
462
|
+
} catch { /* use defaults */ }
|
|
463
|
+
stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
|
|
464
|
+
stageSkillMap.skillOrigins[name] = 'local-import';
|
|
465
|
+
await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
|
|
466
|
+
|
|
467
|
+
imported.push(name);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
errors.push(`${name}: ${err.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
res.json({ imported, skipped, errors });
|
|
474
|
+
} catch (error) {
|
|
475
|
+
console.error('[skills] import-from-local error:', error);
|
|
476
|
+
res.status(500).json({ error: error.message });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// POST /:projectName/import-user-skills — import from ~/.claude/skills/ into GLOBAL_SKILLS_DIR then re-sync symlinks
|
|
481
|
+
router.post('/:projectName/import-user-skills', async (req, res) => {
|
|
482
|
+
try {
|
|
483
|
+
const projectName = req.params.projectName;
|
|
484
|
+
const projectRoot = await extractProjectDirectory(projectName);
|
|
485
|
+
if (!projectRoot) {
|
|
486
|
+
return res.status(404).json({ error: 'Project not found.' });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Resolve ~/.claude/skills/
|
|
490
|
+
const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
491
|
+
let stat;
|
|
492
|
+
try {
|
|
493
|
+
stat = await fs.stat(userSkillsDir);
|
|
494
|
+
} catch {
|
|
495
|
+
return res.status(404).json({ error: 'User skills directory not found (~/.claude/skills/).' });
|
|
496
|
+
}
|
|
497
|
+
if (!stat.isDirectory()) {
|
|
498
|
+
return res.status(400).json({ error: '~/.claude/skills/ is not a directory.' });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Scan for subdirs containing SKILL.md
|
|
502
|
+
const entries = await fs.readdir(userSkillsDir, { withFileTypes: true });
|
|
503
|
+
await fs.mkdir(GLOBAL_SKILLS_DIR, { recursive: true });
|
|
504
|
+
|
|
505
|
+
const imported = [];
|
|
506
|
+
const skipped = [];
|
|
507
|
+
const errors = [];
|
|
508
|
+
|
|
509
|
+
for (const entry of entries) {
|
|
510
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
511
|
+
|
|
512
|
+
const srcDir = path.join(userSkillsDir, entry.name);
|
|
513
|
+
|
|
514
|
+
// Verify SKILL.md exists
|
|
515
|
+
try {
|
|
516
|
+
await fs.access(path.join(srcDir, 'SKILL.md'));
|
|
517
|
+
} catch {
|
|
518
|
+
continue; // Skip dirs without SKILL.md
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const destDir = path.join(GLOBAL_SKILLS_DIR, entry.name);
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
// Check if already exists in global skills
|
|
525
|
+
try {
|
|
526
|
+
await fs.access(destDir);
|
|
527
|
+
skipped.push(entry.name);
|
|
528
|
+
continue;
|
|
529
|
+
} catch {
|
|
530
|
+
// Does not exist — proceed
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
await fs.cp(srcDir, destDir, { recursive: true });
|
|
534
|
+
|
|
535
|
+
// Parse frontmatter from SKILL.md for tag extraction
|
|
536
|
+
let frontmatter = {};
|
|
537
|
+
try {
|
|
538
|
+
const skillMdContent = await fs.readFile(path.join(srcDir, 'SKILL.md'), 'utf8');
|
|
539
|
+
const parsed = await parseFrontmatter(skillMdContent);
|
|
540
|
+
frontmatter = parsed.data || {};
|
|
541
|
+
} catch { /* ignore parse errors */ }
|
|
542
|
+
|
|
543
|
+
// Update skill-tag-mapping.json with tag overrides from frontmatter
|
|
544
|
+
const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
|
|
545
|
+
let tagMapping = { version: 1, stageOverrides: {}, domainOverrides: {}, platformNativeSkills: [], domainCsAiExceptions: [] };
|
|
546
|
+
try {
|
|
547
|
+
tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
|
|
548
|
+
} catch { /* use defaults */ }
|
|
549
|
+
if (frontmatter.stage) {
|
|
550
|
+
tagMapping.stageOverrides = tagMapping.stageOverrides || {};
|
|
551
|
+
tagMapping.stageOverrides[entry.name] = typeof frontmatter.stage === 'string'
|
|
552
|
+
? { en: `Stage: ${frontmatter.stage}`, zh: `阶段: ${frontmatter.stage}` }
|
|
553
|
+
: frontmatter.stage;
|
|
554
|
+
}
|
|
555
|
+
if (frontmatter.domain) {
|
|
556
|
+
tagMapping.domainOverrides = tagMapping.domainOverrides || {};
|
|
557
|
+
tagMapping.domainOverrides[entry.name] = typeof frontmatter.domain === 'string'
|
|
558
|
+
? { en: `Domain: ${frontmatter.domain}`, zh: `领域: ${frontmatter.domain}` }
|
|
559
|
+
: frontmatter.domain;
|
|
560
|
+
}
|
|
561
|
+
await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
|
|
562
|
+
|
|
563
|
+
// Update stage-skill-map.json with origin
|
|
564
|
+
const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
|
|
565
|
+
let stageSkillMap = { skillOrigins: {} };
|
|
566
|
+
try {
|
|
567
|
+
stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
|
|
568
|
+
} catch { /* use defaults */ }
|
|
569
|
+
stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
|
|
570
|
+
stageSkillMap.skillOrigins[entry.name] = 'user-import';
|
|
571
|
+
await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
|
|
572
|
+
|
|
573
|
+
imported.push(entry.name);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
errors.push(`${entry.name}: ${err.message}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Re-sync symlinks for the requesting project
|
|
580
|
+
if (imported.length > 0) {
|
|
581
|
+
try {
|
|
582
|
+
await ensureProjectSkillLinks(projectRoot);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
console.warn('[skills] ensureProjectSkillLinks after import failed (non-fatal):', err.message);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
res.json({ imported, skipped, errors });
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error('[skills] import-user-skills error:', error);
|
|
591
|
+
res.status(500).json({ error: error.message });
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// POST /:projectName/validate-skill-zip
|
|
596
|
+
router.post('/:projectName/validate-skill-zip', async (req, res) => {
|
|
597
|
+
try {
|
|
598
|
+
const multer = (await import('multer')).default;
|
|
599
|
+
const upload = multer({
|
|
600
|
+
storage: multer.memoryStorage(),
|
|
601
|
+
limits: { fileSize: 50 * 1024 * 1024 },
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
upload.single('file')(req, res, async (err) => {
|
|
605
|
+
if (err) {
|
|
606
|
+
return res.status(400).json({ error: 'Failed to process uploaded file: ' + err.message });
|
|
607
|
+
}
|
|
608
|
+
if (!req.file) {
|
|
609
|
+
return res.status(400).json({ error: 'No file provided.' });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const result = await validateZipBuffer(req.file.buffer);
|
|
614
|
+
if (!result.valid) {
|
|
615
|
+
return res.status(400).json({ valid: false, error: result.error });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return res.json({
|
|
619
|
+
valid: true,
|
|
620
|
+
skillName: result.skillName,
|
|
621
|
+
frontmatter: result.frontmatter,
|
|
622
|
+
description: result.description,
|
|
623
|
+
fileCount: result.fileCount,
|
|
624
|
+
hasPrompts: result.hasPrompts,
|
|
625
|
+
hasReferences: result.hasReferences,
|
|
626
|
+
});
|
|
627
|
+
} catch (parseErr) {
|
|
628
|
+
return res.status(400).json({ valid: false, error: 'Failed to parse zip: ' + parseErr.message });
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
} catch (error) {
|
|
632
|
+
console.error('[skills] validate-skill-zip error:', error);
|
|
633
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// POST /:projectName/upload-skill
|
|
638
|
+
router.post('/:projectName/upload-skill', async (req, res) => {
|
|
639
|
+
try {
|
|
640
|
+
const multer = (await import('multer')).default;
|
|
641
|
+
const upload = multer({
|
|
642
|
+
storage: multer.memoryStorage(),
|
|
643
|
+
limits: { fileSize: 50 * 1024 * 1024 },
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
upload.single('file')(req, res, async (err) => {
|
|
647
|
+
if (err) {
|
|
648
|
+
return res.status(400).json({ error: 'Failed to process uploaded file: ' + err.message });
|
|
649
|
+
}
|
|
650
|
+
if (!req.file) {
|
|
651
|
+
return res.status(400).json({ error: 'No file provided.' });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
let extractedDir = null;
|
|
655
|
+
|
|
656
|
+
try {
|
|
657
|
+
// Resolve project root
|
|
658
|
+
const projectName = req.params.projectName;
|
|
659
|
+
const projectRoot = await extractProjectDirectory(projectName);
|
|
660
|
+
if (!projectRoot) {
|
|
661
|
+
return res.status(404).json({ error: 'Project not found.' });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Validate zip
|
|
665
|
+
const result = await validateZipBuffer(req.file.buffer);
|
|
666
|
+
if (!result.valid) {
|
|
667
|
+
return res.status(400).json({ error: result.error });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const safeSkillName = safeDirName(result.skillName);
|
|
671
|
+
if (!safeSkillName) {
|
|
672
|
+
return res.status(400).json({ error: 'Invalid skill name.' });
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Determine skills directory (use skills/ relative to project root)
|
|
676
|
+
const skillsDir = path.join(projectRoot, 'skills');
|
|
677
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
678
|
+
|
|
679
|
+
extractedDir = path.join(skillsDir, safeSkillName);
|
|
680
|
+
|
|
681
|
+
// Check for conflicts
|
|
682
|
+
try {
|
|
683
|
+
await fs.access(extractedDir);
|
|
684
|
+
return res.status(409).json({ error: `Skill directory "${safeSkillName}" already exists.` });
|
|
685
|
+
} catch {
|
|
686
|
+
// Directory doesn't exist — good
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
await fs.mkdir(extractedDir, { recursive: true });
|
|
690
|
+
|
|
691
|
+
// Extract files with path traversal protection
|
|
692
|
+
const { zip, prefix } = result;
|
|
693
|
+
for (const entry of zip.getEntries()) {
|
|
694
|
+
if (entry.isDirectory) continue;
|
|
695
|
+
|
|
696
|
+
let relativePath = entry.entryName;
|
|
697
|
+
if (prefix && relativePath.startsWith(prefix)) {
|
|
698
|
+
relativePath = relativePath.slice(prefix.length);
|
|
699
|
+
} else if (prefix) {
|
|
700
|
+
continue; // Skip files outside the skill directory
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Path traversal protection
|
|
704
|
+
const resolved = path.resolve(extractedDir, relativePath);
|
|
705
|
+
if (!resolved.startsWith(extractedDir)) {
|
|
706
|
+
continue; // Skip dangerous paths
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const dir = path.dirname(resolved);
|
|
710
|
+
await fs.mkdir(dir, { recursive: true });
|
|
711
|
+
await fs.writeFile(resolved, entry.getData());
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Parse tags from request body
|
|
715
|
+
let tags = {};
|
|
716
|
+
try {
|
|
717
|
+
if (req.body && req.body.tags) {
|
|
718
|
+
tags = typeof req.body.tags === 'string' ? JSON.parse(req.body.tags) : req.body.tags;
|
|
719
|
+
}
|
|
720
|
+
} catch {
|
|
721
|
+
// Ignore invalid tags JSON
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Update skill-tag-mapping.json
|
|
725
|
+
const tagMappingPath = path.join(projectRoot, 'skills', 'skill-tag-mapping.json');
|
|
726
|
+
let tagMapping = { version: 1, stageOverrides: {}, domainOverrides: {}, platformNativeSkills: [], domainCsAiExceptions: [] };
|
|
727
|
+
try {
|
|
728
|
+
const raw = await fs.readFile(tagMappingPath, 'utf8');
|
|
729
|
+
tagMapping = JSON.parse(raw);
|
|
730
|
+
} catch {
|
|
731
|
+
// Use defaults
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (tags.stageOverride) {
|
|
735
|
+
tagMapping.stageOverrides = tagMapping.stageOverrides || {};
|
|
736
|
+
tagMapping.stageOverrides[safeSkillName] = tags.stageOverride;
|
|
737
|
+
}
|
|
738
|
+
if (tags.domainOverride) {
|
|
739
|
+
tagMapping.domainOverrides = tagMapping.domainOverrides || {};
|
|
740
|
+
tagMapping.domainOverrides[safeSkillName] = tags.domainOverride;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
|
|
744
|
+
|
|
745
|
+
// Update stage-skill-map.json with origin
|
|
746
|
+
const stageSkillMapPath = path.join(projectRoot, 'skills', 'stage-skill-map.json');
|
|
747
|
+
let stageSkillMap = { skillOrigins: {} };
|
|
748
|
+
try {
|
|
749
|
+
const raw = await fs.readFile(stageSkillMapPath, 'utf8');
|
|
750
|
+
stageSkillMap = JSON.parse(raw);
|
|
751
|
+
} catch {
|
|
752
|
+
// Use defaults
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
stageSkillMap.skillOrigins = stageSkillMap.skillOrigins || {};
|
|
756
|
+
stageSkillMap.skillOrigins[safeSkillName] = tags.origin || 'downloaded';
|
|
757
|
+
|
|
758
|
+
await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
|
|
759
|
+
|
|
760
|
+
// Sync to global skills directory
|
|
761
|
+
await syncSkillToGlobal(safeSkillName, extractedDir, tags);
|
|
762
|
+
|
|
763
|
+
return res.json({
|
|
764
|
+
success: true,
|
|
765
|
+
skillName: result.skillName,
|
|
766
|
+
dirName: safeSkillName,
|
|
767
|
+
path: extractedDir,
|
|
768
|
+
frontmatter: result.frontmatter,
|
|
769
|
+
});
|
|
770
|
+
} catch (uploadErr) {
|
|
771
|
+
// Cleanup on failure
|
|
772
|
+
if (extractedDir) {
|
|
773
|
+
try {
|
|
774
|
+
await fs.rm(extractedDir, { recursive: true, force: true });
|
|
775
|
+
} catch {
|
|
776
|
+
// Ignore cleanup errors
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
console.error('[skills] upload-skill error:', uploadErr);
|
|
780
|
+
return res.status(500).json({ error: 'Failed to extract skill: ' + uploadErr.message });
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.error('[skills] upload-skill error:', error);
|
|
785
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// DELETE /global-skill — delete a skill from the global GLOBAL_SKILLS_DIR by relative dirPath
|
|
790
|
+
router.delete('/global-skill', async (req, res) => {
|
|
791
|
+
try {
|
|
792
|
+
const { dirPath } = req.body || {};
|
|
793
|
+
if (!dirPath || typeof dirPath !== 'string') {
|
|
794
|
+
return res.status(400).json({ error: 'dirPath is required' });
|
|
795
|
+
}
|
|
796
|
+
const skillsDir = path.resolve(GLOBAL_SKILLS_DIR);
|
|
797
|
+
const resolved = path.resolve(skillsDir, dirPath);
|
|
798
|
+
if (!resolved.startsWith(skillsDir + path.sep) && resolved !== skillsDir) {
|
|
799
|
+
return res.status(403).json({ error: 'Path must be under skills root' });
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const stat = await fs.stat(resolved);
|
|
803
|
+
if (!stat.isDirectory()) {
|
|
804
|
+
return res.status(400).json({ error: 'Path is not a directory' });
|
|
805
|
+
}
|
|
806
|
+
} catch {
|
|
807
|
+
return res.status(404).json({ error: 'Skill directory not found' });
|
|
808
|
+
}
|
|
809
|
+
await fs.rm(resolved, { recursive: true, force: true });
|
|
810
|
+
|
|
811
|
+
// Clean config files using the skill folder name
|
|
812
|
+
const skillName = path.basename(resolved);
|
|
813
|
+
const tagMappingPath = path.join(GLOBAL_SKILLS_DIR, 'skill-tag-mapping.json');
|
|
814
|
+
try {
|
|
815
|
+
const tagMapping = JSON.parse(await fs.readFile(tagMappingPath, 'utf8'));
|
|
816
|
+
if (tagMapping.stageOverrides) delete tagMapping.stageOverrides[skillName];
|
|
817
|
+
if (tagMapping.domainOverrides) delete tagMapping.domainOverrides[skillName];
|
|
818
|
+
await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
|
|
819
|
+
} catch { /* file missing or unparseable */ }
|
|
820
|
+
|
|
821
|
+
const stageSkillMapPath = path.join(GLOBAL_SKILLS_DIR, 'stage-skill-map.json');
|
|
822
|
+
try {
|
|
823
|
+
const stageSkillMap = JSON.parse(await fs.readFile(stageSkillMapPath, 'utf8'));
|
|
824
|
+
if (stageSkillMap.skillOrigins) delete stageSkillMap.skillOrigins[skillName];
|
|
825
|
+
await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
|
|
826
|
+
} catch { /* file missing or unparseable */ }
|
|
827
|
+
|
|
828
|
+
res.json({ success: true, deleted: dirPath });
|
|
829
|
+
} catch (error) {
|
|
830
|
+
console.error('[skills] delete-global-skill error:', error);
|
|
831
|
+
res.status(500).json({ error: 'Failed to delete skill: ' + error.message });
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// DELETE /:projectName/:skillDirName
|
|
836
|
+
router.delete('/:projectName/:skillDirName', async (req, res) => {
|
|
837
|
+
try {
|
|
838
|
+
const { projectName, skillDirName } = req.params;
|
|
839
|
+
const projectRoot = await extractProjectDirectory(projectName);
|
|
840
|
+
if (!projectRoot) {
|
|
841
|
+
return res.status(404).json({ error: 'Project not found.' });
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Search candidate locations for the skill directory
|
|
845
|
+
const candidateBases = ['skills', '.claude/skills', '.agents/skills', '.cursor/skills'];
|
|
846
|
+
let foundDir = null;
|
|
847
|
+
for (const base of candidateBases) {
|
|
848
|
+
const candidate = path.join(projectRoot, base, skillDirName);
|
|
849
|
+
try {
|
|
850
|
+
const stat = await fs.stat(candidate);
|
|
851
|
+
if (stat.isDirectory()) {
|
|
852
|
+
foundDir = candidate;
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
} catch {
|
|
856
|
+
// Not found in this location
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (!foundDir) {
|
|
861
|
+
return res.status(404).json({ error: `Skill directory "${skillDirName}" not found.` });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Delete the skill directory
|
|
865
|
+
await fs.rm(foundDir, { recursive: true, force: true });
|
|
866
|
+
|
|
867
|
+
// Determine the parent skills dir from the found location for config cleanup
|
|
868
|
+
const foundSkillsBase = path.dirname(foundDir);
|
|
869
|
+
|
|
870
|
+
// Clean up skill-tag-mapping.json in the skills base where the skill was found
|
|
871
|
+
// Also clean from the legacy skills/ location for backwards compat
|
|
872
|
+
const tagMappingCandidates = [
|
|
873
|
+
path.join(foundSkillsBase, 'skill-tag-mapping.json'),
|
|
874
|
+
path.join(projectRoot, 'skills', 'skill-tag-mapping.json'),
|
|
875
|
+
];
|
|
876
|
+
for (const tagMappingPath of [...new Set(tagMappingCandidates)]) {
|
|
877
|
+
try {
|
|
878
|
+
const raw = await fs.readFile(tagMappingPath, 'utf8');
|
|
879
|
+
const tagMapping = JSON.parse(raw);
|
|
880
|
+
if (tagMapping.stageOverrides) delete tagMapping.stageOverrides[skillDirName];
|
|
881
|
+
if (tagMapping.domainOverrides) delete tagMapping.domainOverrides[skillDirName];
|
|
882
|
+
await fs.writeFile(tagMappingPath, JSON.stringify(tagMapping, null, 2), 'utf8');
|
|
883
|
+
} catch {
|
|
884
|
+
// Tag mapping file doesn't exist or can't be parsed; skip cleanup
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Clean up stage-skill-map.json in the skills base + legacy location
|
|
889
|
+
const stageMapCandidates = [
|
|
890
|
+
path.join(foundSkillsBase, 'stage-skill-map.json'),
|
|
891
|
+
path.join(projectRoot, 'skills', 'stage-skill-map.json'),
|
|
892
|
+
];
|
|
893
|
+
for (const stageSkillMapPath of [...new Set(stageMapCandidates)]) {
|
|
894
|
+
try {
|
|
895
|
+
const raw = await fs.readFile(stageSkillMapPath, 'utf8');
|
|
896
|
+
const stageSkillMap = JSON.parse(raw);
|
|
897
|
+
if (stageSkillMap.skillOrigins) delete stageSkillMap.skillOrigins[skillDirName];
|
|
898
|
+
await fs.writeFile(stageSkillMapPath, JSON.stringify(stageSkillMap, null, 2), 'utf8');
|
|
899
|
+
} catch {
|
|
900
|
+
// Stage-skill-map file doesn't exist or can't be parsed; skip cleanup
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Remove from global skills directory
|
|
905
|
+
await removeSkillFromGlobal(skillDirName);
|
|
906
|
+
|
|
907
|
+
return res.json({ success: true, deleted: skillDirName });
|
|
908
|
+
} catch (error) {
|
|
909
|
+
console.error('[skills] delete-skill error:', error);
|
|
910
|
+
res.status(500).json({ error: 'Failed to delete skill: ' + error.message });
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
export default router;
|